1+ use std:: convert:: TryFrom ;
12use std:: fmt;
23use 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
3551derive_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+
4068impl 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
51198error_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+
53253impl 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
62264impl 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