Skip to content

Commit 3c72502

Browse files
committed
improve referer header ergonomics
1 parent 8db1b78 commit 3c72502

File tree

1 file changed

+323
-9
lines changed

1 file changed

+323
-9
lines changed

src/common/referer.rs

Lines changed: 323 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,13 @@
1+
use std::convert::TryFrom;
12
use std::fmt;
23
use std::str::FromStr;
34

4-
use crate::util::HeaderValueString;
5+
use bytes::Bytes;
6+
use http::uri::{Authority, PathAndQuery, Scheme, Uri};
7+
use http::HeaderValue;
8+
9+
use crate::util::{HeaderValueString, IterExt, TryFromValues};
10+
use crate::Error;
511

612
/// `Referer` header, defined in
713
/// [RFC7231](https://datatracker.ietf.org/doc/html/rfc7231#section-5.5.2)
@@ -21,46 +27,354 @@ use crate::util::HeaderValueString;
2127
/// ## Example values
2228
///
2329
/// * `http://www.example.org/hypertext/Overview.html`
30+
/// * `/People.html`
2431
///
2532
/// # Examples
2633
///
2734
/// ```
2835
/// use headers::Referer;
36+
/// use std::str::FromStr;
37+
///
38+
/// let r = Referer::from_str("http://www.example.org/hypertext/Overview.html").unwrap();
39+
/// assert_eq!(r.scheme(), Some("http"));
40+
/// assert_eq!(r.hostname(), Some("www.example.org"));
41+
/// assert_eq!(r.path(), "/hypertext/Overview.html");
2942
///
30-
/// let r = Referer::from_static("/People.html#tim");
43+
/// // Partial URIs work too
44+
/// let r2 = Referer::from_str("/People.html").unwrap();
45+
/// assert_eq!(r2.scheme(), None);
46+
/// assert_eq!(r2.path(), "/People.html");
3147
/// ```
3248
#[derive(Debug, Clone, PartialEq)]
33-
pub struct Referer(HeaderValueString);
49+
pub struct Referer(RefererUri);
3450

3551
derive_header! {
3652
Referer(_),
3753
name: REFERER
3854
}
3955

56+
#[derive(Debug, Clone, PartialEq)]
57+
enum RefererUri {
58+
/// Absolute URI with scheme and authority
59+
Absolute {
60+
scheme: Scheme,
61+
authority: Authority,
62+
path_and_query: Option<PathAndQuery>,
63+
},
64+
/// Partial URI (relative reference)
65+
Partial(HeaderValueString),
66+
}
67+
4068
impl Referer {
4169
/// Create a `Referer` with a static string.
4270
///
4371
/// # Panic
4472
///
45-
/// Panics if the string is not a legal header value.
73+
/// Panics if the string is not a legal header value or contains
74+
/// forbidden components (fragment or userinfo).
4675
pub const fn from_static(s: &'static str) -> Referer {
47-
Referer(HeaderValueString::from_static(s))
76+
Referer(RefererUri::Partial(HeaderValueString::from_static(s)))
77+
}
78+
79+
/// Tries to build a `Referer` from components for absolute URIs.
80+
///
81+
/// This method constructs an absolute URI referer from scheme, host,
82+
/// optional port, and optional path with query.
83+
pub fn try_from_parts(
84+
scheme: &str,
85+
host: &str,
86+
port: impl Into<Option<u16>>,
87+
path_and_query: Option<&str>,
88+
) -> Result<Self, InvalidReferer> {
89+
struct MaybePort(Option<u16>);
90+
91+
impl fmt::Display for MaybePort {
92+
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
93+
if let Some(port) = self.0 {
94+
write!(f, ":{}", port)
95+
} else {
96+
Ok(())
97+
}
98+
}
99+
}
100+
101+
let path_part = path_and_query.unwrap_or("");
102+
let uri_string = format!("{}://{}{}{}", scheme, host, MaybePort(port.into()), path_part);
103+
let bytes = Bytes::from(uri_string);
104+
105+
HeaderValue::from_maybe_shared(bytes)
106+
.ok()
107+
.and_then(|val| Self::try_from_value(&val))
108+
.ok_or(InvalidReferer { _inner: () })
109+
}
110+
111+
/// Get the "scheme" part of this referer, if it's an absolute URI.
112+
#[inline]
113+
pub fn scheme(&self) -> Option<&str> {
114+
match &self.0 {
115+
RefererUri::Absolute { scheme, .. } => Some(scheme.as_str()),
116+
RefererUri::Partial(_) => None,
117+
}
118+
}
119+
120+
/// Get the "hostname" part of this referer, if it's an absolute URI.
121+
#[inline]
122+
pub fn hostname(&self) -> Option<&str> {
123+
match &self.0 {
124+
RefererUri::Absolute { authority, .. } => Some(authority.host()),
125+
RefererUri::Partial(_) => None,
126+
}
127+
}
128+
129+
/// Get the "port" part of this referer, if it's an absolute URI.
130+
#[inline]
131+
pub fn port(&self) -> Option<u16> {
132+
match &self.0 {
133+
RefererUri::Absolute { authority, .. } => authority.port_u16(),
134+
RefererUri::Partial(_) => None,
135+
}
136+
}
137+
138+
/// Get the "path" part of this referer.
139+
///
140+
/// For absolute URIs, this extracts the path component.
141+
/// For partial URIs, this returns the entire value if it starts with '/'.
142+
#[inline]
143+
pub fn path(&self) -> &str {
144+
match &self.0 {
145+
RefererUri::Absolute { path_and_query: Some(pq), .. } => pq.path(),
146+
RefererUri::Absolute { path_and_query: None, .. } => "/",
147+
RefererUri::Partial(s) => {
148+
let s_str = s.as_str();
149+
if s_str.starts_with('/') {
150+
// Extract just the path part if it contains query
151+
if let Some(pos) = s_str.find('?') {
152+
&s_str[..pos]
153+
} else {
154+
s_str
155+
}
156+
} else {
157+
""
158+
}
159+
}
160+
}
161+
}
162+
163+
/// Get the "query" part of this referer, if present.
164+
#[inline]
165+
pub fn query(&self) -> Option<&str> {
166+
match &self.0 {
167+
RefererUri::Absolute { path_and_query: Some(pq), .. } => pq.query(),
168+
RefererUri::Absolute { path_and_query: None, .. } => None,
169+
RefererUri::Partial(s) => {
170+
let s_str = s.as_str();
171+
if let Some(pos) = s_str.find('?') {
172+
Some(&s_str[pos + 1..])
173+
} else {
174+
None
175+
}
176+
}
177+
}
178+
}
179+
180+
/// Returns true if this is an absolute URI (has scheme and authority).
181+
#[inline]
182+
pub fn is_absolute(&self) -> bool {
183+
matches!(self.0, RefererUri::Absolute { .. })
184+
}
185+
186+
/// Returns true if this is a partial URI (relative reference).
187+
#[inline]
188+
pub fn is_partial(&self) -> bool {
189+
matches!(self.0, RefererUri::Partial(_))
190+
}
191+
192+
// Used internally and by other modules
193+
pub(super) fn try_from_value(value: &HeaderValue) -> Option<Self> {
194+
RefererUri::try_from_value(value).map(Referer)
48195
}
49196
}
50197

51198
error_type!(InvalidReferer);
52199

200+
impl RefererUri {
201+
fn try_from_value(value: &HeaderValue) -> Option<Self> {
202+
let value_str = value.to_str().ok()?;
203+
204+
// Check for forbidden components
205+
if value_str.contains('#') {
206+
// Contains fragment, which is forbidden
207+
return None;
208+
}
209+
210+
if value_str.contains('@') {
211+
// Might contain userinfo, which is forbidden
212+
// This is a simple check; a more thorough check would parse the URI
213+
if let Ok(uri) = Uri::try_from(value_str) {
214+
if uri.authority().map_or(false, |auth| auth.as_str().contains('@')) {
215+
return None;
216+
}
217+
}
218+
}
219+
220+
// Try to parse as URI first
221+
if let Ok(uri) = Uri::try_from(value_str) {
222+
let parts = uri.into_parts();
223+
224+
// If it has scheme and authority, it's an absolute URI
225+
if let (Some(scheme), Some(authority)) = (parts.scheme, parts.authority) {
226+
return Some(RefererUri::Absolute {
227+
scheme,
228+
authority,
229+
path_and_query: parts.path_and_query,
230+
});
231+
}
232+
}
233+
234+
// Otherwise, treat as partial URI
235+
HeaderValueString::from_str(value_str)
236+
.map(RefererUri::Partial)
237+
.ok()
238+
}
239+
}
240+
241+
impl TryFromValues for RefererUri {
242+
fn try_from_values<'i, I>(values: &mut I) -> Result<Self, Error>
243+
where
244+
I: Iterator<Item = &'i HeaderValue>,
245+
{
246+
values
247+
.just_one()
248+
.and_then(RefererUri::try_from_value)
249+
.ok_or_else(Error::invalid)
250+
}
251+
}
252+
53253
impl FromStr for Referer {
54254
type Err = InvalidReferer;
55255
fn from_str(src: &str) -> Result<Self, Self::Err> {
56-
HeaderValueString::from_str(src)
57-
.map(Referer)
58-
.map_err(|_| InvalidReferer { _inner: () })
256+
// Create a temporary HeaderValue to reuse our parsing logic
257+
HeaderValue::from_str(src)
258+
.ok()
259+
.and_then(|val| Self::try_from_value(&val))
260+
.ok_or(InvalidReferer { _inner: () })
59261
}
60262
}
61263

62264
impl fmt::Display for Referer {
63265
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
64-
fmt::Display::fmt(&self.0, f)
266+
match &self.0 {
267+
RefererUri::Absolute { scheme, authority, path_and_query } => {
268+
write!(f, "{}://{}", scheme, authority)?;
269+
if let Some(pq) = path_and_query {
270+
write!(f, "{}", pq)
271+
} else {
272+
Ok(())
273+
}
274+
}
275+
RefererUri::Partial(s) => fmt::Display::fmt(s, f),
276+
}
277+
}
278+
}
279+
280+
impl<'a> From<&'a RefererUri> for HeaderValue {
281+
fn from(referer: &'a RefererUri) -> HeaderValue {
282+
match referer {
283+
RefererUri::Absolute { scheme, authority, path_and_query } => {
284+
let mut s = format!("{}://{}", scheme, authority);
285+
if let Some(pq) = path_and_query {
286+
s.push_str(pq.as_str());
287+
}
288+
let bytes = Bytes::from(s);
289+
HeaderValue::from_maybe_shared(bytes)
290+
.expect("Scheme, Authority, and PathAndQuery are valid header values")
291+
}
292+
RefererUri::Partial(s) => s.as_str().parse()
293+
.expect("HeaderValueString contains valid header value"),
294+
}
295+
}
296+
}
297+
298+
#[cfg(test)]
299+
mod tests {
300+
use super::super::{test_decode, test_encode};
301+
use super::*;
302+
303+
#[test]
304+
fn absolute_referer() {
305+
let s = "http://www.example.org/hypertext/Overview.html";
306+
let referer = test_decode::<Referer>(&[s]).unwrap();
307+
assert_eq!(referer.scheme(), Some("http"));
308+
assert_eq!(referer.hostname(), Some("www.example.org"));
309+
assert_eq!(referer.port(), None);
310+
assert_eq!(referer.path(), "/hypertext/Overview.html");
311+
assert_eq!(referer.query(), None);
312+
assert!(referer.is_absolute());
313+
assert!(!referer.is_partial());
314+
315+
let headers = test_encode(referer);
316+
assert_eq!(headers["referer"], s);
317+
}
318+
319+
#[test]
320+
fn absolute_referer_with_port_and_query() {
321+
let s = "https://example.com:8443/api/users?page=1";
322+
let referer = test_decode::<Referer>(&[s]).unwrap();
323+
assert_eq!(referer.scheme(), Some("https"));
324+
assert_eq!(referer.hostname(), Some("example.com"));
325+
assert_eq!(referer.port(), Some(8443));
326+
assert_eq!(referer.path(), "/api/users");
327+
assert_eq!(referer.query(), Some("page=1"));
328+
assert!(referer.is_absolute());
329+
330+
let headers = test_encode(referer);
331+
assert_eq!(headers["referer"], s);
332+
}
333+
334+
#[test]
335+
fn partial_referer() {
336+
let s = "/People.html";
337+
let referer = test_decode::<Referer>(&[s]).unwrap();
338+
assert_eq!(referer.scheme(), None);
339+
assert_eq!(referer.hostname(), None);
340+
assert_eq!(referer.port(), None);
341+
assert_eq!(referer.path(), "/People.html");
342+
assert_eq!(referer.query(), None);
343+
assert!(!referer.is_absolute());
344+
assert!(referer.is_partial());
345+
346+
let headers = test_encode(referer);
347+
assert_eq!(headers["referer"], s);
348+
}
349+
350+
#[test]
351+
fn partial_referer_with_query() {
352+
let s = "/search?q=rust";
353+
let referer = test_decode::<Referer>(&[s]).unwrap();
354+
assert_eq!(referer.path(), "/search");
355+
assert_eq!(referer.query(), Some("q=rust"));
356+
assert!(referer.is_partial());
357+
}
358+
359+
#[test]
360+
fn try_from_parts() {
361+
let referer = Referer::try_from_parts("https", "example.com", Some(443), Some("/api/test?v=1")).unwrap();
362+
assert_eq!(referer.scheme(), Some("https"));
363+
assert_eq!(referer.hostname(), Some("example.com"));
364+
assert_eq!(referer.port(), Some(443));
365+
assert_eq!(referer.path(), "/api/test");
366+
assert_eq!(referer.query(), Some("v=1"));
367+
}
368+
369+
#[test]
370+
fn invalid_referer_with_fragment() {
371+
// Should reject URIs with fragments
372+
assert!(test_decode::<Referer>(&["http://example.com/page#section"]).is_none());
373+
}
374+
375+
#[test]
376+
fn invalid_referer_with_userinfo() {
377+
// Should reject URIs with userinfo
378+
assert!(test_decode::<Referer>(&["http://user:[email protected]/page"]).is_none());
65379
}
66380
}

0 commit comments

Comments
 (0)