we (web engine): Experimental web browser project to understand the limits of Claude
2
fork

Configure Feed

Select the types of activity you want to include in your feed.

Implement Referrer Policy: parsing, computation, and integration

Add W3C Referrer Policy support with all 8 policies: no-referrer,
no-referrer-when-downgrade, origin, origin-when-cross-origin, same-origin,
strict-origin, strict-origin-when-cross-origin (default), and unsafe-url.

- New crates/net/src/referrer.rs: ReferrerPolicy enum, header/attribute
parsing, and compute_referrer() for Referer header computation
- ResourceLoader: tracks document URL and referrer policy, sets Referer
header on all outgoing requests, updates policy from Referrer-Policy
response headers
- HTML integration: extracts <meta name="referrer"> from DOM, reads
referrerpolicy attribute on <script>, <link>, and <img> elements
- Element-level policy overrides document-level policy per spec
- Strips fragment and userinfo from referrer URLs
- HTTPS-to-HTTP downgrade correctly handled for strict/downgrade policies

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

+935 -21
+45 -8
crates/browser/src/css_loader.rs
··· 6 6 7 7 use we_css::parser::{ImportRule, Parser, Rule, Stylesheet}; 8 8 use we_dom::{Document, NodeData, NodeId}; 9 + use we_net::referrer::ReferrerPolicy; 9 10 use we_url::{Origin, Url}; 10 11 11 12 use crate::loader::{LoadError, Resource, ResourceLoader, ResourceRequestType}; ··· 73 74 let resolved = resolve_imports(sheet, loader, base_url, document_origin, 0); 74 75 all_rules.extend(resolved.rules); 75 76 } 76 - StyleSource::ExternalLink { href, media } => { 77 + StyleSource::ExternalLink { 78 + href, 79 + media, 80 + referrer_policy, 81 + } => { 77 82 if !media_matches(&media) { 78 83 continue; 79 84 } 80 - match fetch_stylesheet(loader, &href, base_url, document_origin, 0) { 85 + match fetch_stylesheet_with_policy( 86 + loader, 87 + &href, 88 + base_url, 89 + document_origin, 90 + 0, 91 + referrer_policy, 92 + ) { 81 93 Ok(sheet) => all_rules.extend(sheet.rules), 82 94 Err(_) => { 83 95 // Graceful degradation: skip failed stylesheet loads. ··· 109 121 /// A `<style>` element with inline CSS text. 110 122 InlineStyle(String), 111 123 /// A `<link rel="stylesheet">` element with an `href`. 112 - ExternalLink { href: String, media: Option<String> }, 124 + ExternalLink { 125 + href: String, 126 + media: Option<String>, 127 + referrer_policy: Option<ReferrerPolicy>, 128 + }, 113 129 /// Not a stylesheet source. 114 130 NotStylesheet, 115 131 } ··· 150 166 match doc.get_attribute(node, "href") { 151 167 Some(href) if !href.is_empty() => { 152 168 let media = doc.get_attribute(node, "media").map(|m| m.to_string()); 169 + let referrer_policy = doc 170 + .get_attribute(node, "referrerpolicy") 171 + .and_then(ReferrerPolicy::parse); 153 172 StyleSource::ExternalLink { 154 173 href: href.to_string(), 155 174 media, 175 + referrer_policy, 156 176 } 157 177 } 158 178 _ => StyleSource::NotStylesheet, ··· 202 222 document_origin: &Origin, 203 223 depth: usize, 204 224 ) -> Result<Stylesheet, CssLoadError> { 205 - let resource = loader.fetch_url_subresource( 206 - href, 207 - Some(base_url), 225 + fetch_stylesheet_with_policy(loader, href, base_url, document_origin, depth, None) 226 + } 227 + 228 + /// Fetch an external stylesheet with optional element-level referrer policy override. 229 + fn fetch_stylesheet_with_policy( 230 + loader: &mut ResourceLoader, 231 + href: &str, 232 + base_url: &Url, 233 + document_origin: &Origin, 234 + depth: usize, 235 + element_policy: Option<ReferrerPolicy>, 236 + ) -> Result<Stylesheet, CssLoadError> { 237 + let url = we_url::Url::parse_with_base(href, base_url) 238 + .or_else(|_| we_url::Url::parse(href)) 239 + .map_err(|_| CssLoadError::NotCss { 240 + url: href.to_string(), 241 + })?; 242 + let resource = loader.fetch_subresource_with_referrer_policy( 243 + &url, 208 244 document_origin, 209 245 ResourceRequestType::Stylesheet, 246 + element_policy, 210 247 )?; 211 248 212 249 let (css_text, resolved_url) = match resource { ··· 435 472 let mut nodes = Vec::new(); 436 473 collect_style_nodes(&doc, doc.root(), &mut nodes); 437 474 match classify_style_node(&doc, nodes[0]) { 438 - StyleSource::ExternalLink { href, media } => { 475 + StyleSource::ExternalLink { href, media, .. } => { 439 476 assert_eq!(href, "style.css"); 440 477 assert!(media.is_none()); 441 478 } ··· 455 492 doc.append_child(root, link); 456 493 457 494 match classify_style_node(&doc, link) { 458 - StyleSource::ExternalLink { href, media } => { 495 + StyleSource::ExternalLink { href, media, .. } => { 459 496 assert_eq!(href, "screen.css"); 460 497 assert_eq!(media.as_deref(), Some("screen")); 461 498 }
+15 -2
crates/browser/src/img_loader.rs
··· 11 11 use we_image::jpeg::decode_jpeg; 12 12 use we_image::pixel::{Image, ImageError}; 13 13 use we_image::png::decode_png; 14 + use we_net::referrer::ReferrerPolicy; 14 15 use we_url::Url; 15 16 16 17 use crate::loader::{LoadError, Resource, ResourceLoader}; ··· 121 122 let alt = doc.get_attribute(node, "alt").unwrap_or("").to_string(); 122 123 let attr_width = parse_dimension_attr(doc.get_attribute(node, "width")); 123 124 let attr_height = parse_dimension_attr(doc.get_attribute(node, "height")); 125 + let element_policy = doc 126 + .get_attribute(node, "referrerpolicy") 127 + .and_then(ReferrerPolicy::parse); 124 128 125 - match fetch_and_decode(loader, &src, base_url) { 129 + match fetch_and_decode(loader, &src, base_url, element_policy) { 126 130 Ok(image) => { 127 131 let intrinsic_w = image.width as f32; 128 132 let intrinsic_h = image.height as f32; ··· 220 224 loader: &mut ResourceLoader, 221 225 src: &str, 222 226 base_url: &Url, 227 + element_policy: Option<ReferrerPolicy>, 223 228 ) -> Result<Image, ImgLoadError> { 224 - let resource = loader.fetch_url(src, Some(base_url))?; 229 + // Temporarily override the referrer policy if an element-level policy is set. 230 + let saved_policy = loader.referrer_policy(); 231 + if let Some(policy) = element_policy { 232 + loader.set_referrer_policy(policy); 233 + } 234 + let resource = loader.fetch_url(src, Some(base_url)); 235 + // Restore the document-level policy. 236 + loader.set_referrer_policy(saved_policy); 237 + let resource = resource?; 225 238 226 239 let (data, url_str) = match resource { 227 240 Resource::Image { data, url, .. } => (data, url.to_string()),
+115 -5
crates/browser/src/loader.rs
··· 13 13 self, build_preflight_headers, check_cors_response, needs_preflight, validate_preflight, 14 14 CredentialsMode, PreflightCache, 15 15 }; 16 - use we_net::http::{ContentType, Headers, Method}; 16 + use we_net::http::{ContentType, Headers, HttpResponse, Method}; 17 + use we_net::referrer::{self, ReferrerPolicy}; 17 18 use we_url::data_url::{is_data_url, parse_data_url}; 18 19 use we_url::{Origin, Url}; 19 20 ··· 114 115 pub struct ResourceLoader { 115 116 client: HttpClient, 116 117 preflight_cache: PreflightCache, 118 + /// The document URL used as the referrer source for subresource requests. 119 + document_url: Option<Url>, 120 + /// The active referrer policy for this document. 121 + referrer_policy: ReferrerPolicy, 117 122 } 118 123 119 124 impl ResourceLoader { ··· 122 127 Self { 123 128 client: HttpClient::new(), 124 129 preflight_cache: PreflightCache::new(), 130 + document_url: None, 131 + referrer_policy: ReferrerPolicy::default(), 132 + } 133 + } 134 + 135 + /// Set the document URL (used as the referrer source). 136 + pub fn set_document_url(&mut self, url: &Url) { 137 + self.document_url = Some(url.clone()); 138 + } 139 + 140 + /// Set the referrer policy for this document. 141 + pub fn set_referrer_policy(&mut self, policy: ReferrerPolicy) { 142 + self.referrer_policy = policy; 143 + } 144 + 145 + /// Get the current referrer policy. 146 + pub fn referrer_policy(&self) -> ReferrerPolicy { 147 + self.referrer_policy 148 + } 149 + 150 + /// Compute the Referer header value for a request to `target_url`, 151 + /// using an optional element-level policy override. 152 + fn compute_referer( 153 + &self, 154 + target_url: &Url, 155 + element_policy: Option<ReferrerPolicy>, 156 + ) -> Option<String> { 157 + let doc_url = self.document_url.as_ref()?; 158 + let policy = element_policy.unwrap_or(self.referrer_policy); 159 + referrer::compute_referrer(doc_url, target_url, policy) 160 + } 161 + 162 + /// Add the Referer header to a headers collection if appropriate. 163 + fn add_referer_header( 164 + &self, 165 + headers: &mut Headers, 166 + target_url: &Url, 167 + element_policy: Option<ReferrerPolicy>, 168 + ) { 169 + if let Some(referer) = self.compute_referer(target_url, element_policy) { 170 + headers.set("Referer", &referer); 171 + } 172 + } 173 + 174 + /// Check the response for a `Referrer-Policy` header and update 175 + /// the document-level policy if one is found. 176 + fn update_policy_from_response(&mut self, response: &HttpResponse) { 177 + if let Some(header_val) = response.headers.get("Referrer-Policy") { 178 + if let Some(policy) = ReferrerPolicy::parse_header(header_val) { 179 + self.referrer_policy = policy; 180 + } 125 181 } 126 182 } 127 183 ··· 143 199 return fetch_about_url(url); 144 200 } 145 201 146 - let response = self.client.get(url)?; 202 + let mut headers = Headers::new(); 203 + self.add_referer_header(&mut headers, url, None); 204 + let response = self.client.request(Method::Get, url, &headers, None)?; 205 + self.update_policy_from_response(&response); 147 206 148 207 // Check for HTTP error status codes 149 208 if response.status_code >= 400 { ··· 222 281 document_origin: &Origin, 223 282 request_type: ResourceRequestType, 224 283 ) -> Result<Resource, LoadError> { 225 - self.fetch_subresource_with_cors( 284 + self.fetch_subresource_with_options( 285 + url, 286 + document_origin, 287 + request_type, 288 + Method::Get, 289 + &Headers::new(), 290 + CredentialsMode::SameOrigin, 291 + None, 292 + ) 293 + } 294 + 295 + /// Fetch a subresource with an element-level referrer policy override. 296 + pub fn fetch_subresource_with_referrer_policy( 297 + &mut self, 298 + url: &Url, 299 + document_origin: &Origin, 300 + request_type: ResourceRequestType, 301 + element_policy: Option<ReferrerPolicy>, 302 + ) -> Result<Resource, LoadError> { 303 + self.fetch_subresource_with_options( 226 304 url, 227 305 document_origin, 228 306 request_type, 229 307 Method::Get, 230 308 &Headers::new(), 231 309 CredentialsMode::SameOrigin, 310 + element_policy, 232 311 ) 233 312 } 234 313 ··· 242 321 extra_headers: &Headers, 243 322 credentials_mode: CredentialsMode, 244 323 ) -> Result<Resource, LoadError> { 324 + self.fetch_subresource_with_options( 325 + url, 326 + document_origin, 327 + request_type, 328 + method, 329 + extra_headers, 330 + credentials_mode, 331 + None, 332 + ) 333 + } 334 + 335 + /// Fetch a subresource with full CORS control and optional referrer policy override. 336 + #[allow(clippy::too_many_arguments)] 337 + fn fetch_subresource_with_options( 338 + &mut self, 339 + url: &Url, 340 + document_origin: &Origin, 341 + request_type: ResourceRequestType, 342 + method: Method, 343 + extra_headers: &Headers, 344 + credentials_mode: CredentialsMode, 345 + element_policy: Option<ReferrerPolicy>, 346 + ) -> Result<Resource, LoadError> { 245 347 // data: and about: URLs are always allowed (local, no network). 246 348 if url.scheme() == "data" || url.scheme() == "about" { 247 349 return self.fetch(url); ··· 257 359 let resource_origin = url.origin(); 258 360 if document_origin.same_origin(&resource_origin) { 259 361 // Same-origin: no CORS needed, use provided method and headers. 260 - let response = self.client.request(method, url, extra_headers, None)?; 362 + let mut merged = Headers::new(); 363 + for (name, value) in extra_headers.iter() { 364 + merged.add(name, value); 365 + } 366 + self.add_referer_header(&mut merged, url, element_policy); 367 + let response = self.client.request(method, url, &merged, None)?; 368 + self.update_policy_from_response(&response); 261 369 if response.status_code >= 400 { 262 370 return Err(LoadError::HttpStatus { 263 371 status: response.status_code, ··· 270 378 // Cross-origin: CORS flow. 271 379 let url_str = url.serialize(); 272 380 273 - // Add Origin header to the request. 381 + // Add Origin and Referer headers to the request. 274 382 let mut request_headers = Headers::new(); 275 383 for (name, value) in extra_headers.iter() { 276 384 request_headers.add(name, value); 277 385 } 278 386 request_headers.set("Origin", &document_origin.serialize()); 387 + self.add_referer_header(&mut request_headers, url, element_policy); 279 388 280 389 // Check if a preflight is needed. 281 390 if needs_preflight(method, extra_headers) { ··· 306 415 307 416 // Perform the actual request. 308 417 let response = self.client.request(method, url, &request_headers, None)?; 418 + self.update_policy_from_response(&response); 309 419 310 420 if response.status_code >= 400 { 311 421 return Err(LoadError::HttpStatus {
+47 -1
crates/browser/src/main.rs
··· 11 11 use we_html::parse_html; 12 12 use we_image::pixel::Image; 13 13 use we_layout::layout; 14 + use we_net::referrer::ReferrerPolicy; 14 15 use we_platform::appkit; 15 16 use we_platform::cg::BitmapContext; 16 17 use we_platform::metal::ClearColor; ··· 341 342 struct LoadedHtml { 342 343 text: String, 343 344 base_url: Url, 345 + /// Referrer policy from the HTTP Referrer-Policy response header, if any. 346 + http_referrer_policy: Option<ReferrerPolicy>, 344 347 } 345 348 346 349 /// Load content from a command-line argument. ··· 358 361 let mut loader = ResourceLoader::new(); 359 362 match loader.fetch_url(arg, None) { 360 363 Ok(Resource::Html { text, base_url, .. }) => { 361 - return LoadedHtml { text, base_url }; 364 + // Capture referrer policy from the HTTP response (if loader updated it). 365 + let http_policy = if loader.referrer_policy() != ReferrerPolicy::default() { 366 + Some(loader.referrer_policy()) 367 + } else { 368 + None 369 + }; 370 + return LoadedHtml { 371 + text, 372 + base_url, 373 + http_referrer_policy: http_policy, 374 + }; 362 375 } 363 376 Ok(_) => { 364 377 return error_page(&format!("URL did not return HTML: {arg}")); ··· 382 395 LoadedHtml { 383 396 text: content, 384 397 base_url, 398 + http_referrer_policy: None, 385 399 } 386 400 } 387 401 Err(e) => error_page(&format!("Error reading {arg}: {e}")), ··· 409 423 LoadedHtml { 410 424 text: html, 411 425 base_url, 426 + http_referrer_policy: None, 412 427 } 413 428 } 414 429 430 + /// Extract the referrer policy from `<meta name="referrer" content="...">` in the DOM. 431 + fn extract_meta_referrer_policy(doc: &Document) -> Option<ReferrerPolicy> { 432 + let mut result = None; 433 + for i in 0..doc.len() { 434 + let node_id = NodeId::from_index(i); 435 + if doc.tag_name(node_id) == Some("meta") { 436 + if let Some(name_attr) = doc.get_attribute(node_id, "name") { 437 + if name_attr.eq_ignore_ascii_case("referrer") { 438 + if let Some(content) = doc.get_attribute(node_id, "content") { 439 + if let Some(policy) = ReferrerPolicy::parse(content) { 440 + // Last valid meta referrer wins. 441 + result = Some(policy); 442 + } 443 + } 444 + } 445 + } 446 + } 447 + } 448 + result 449 + } 450 + 415 451 /// Load a page: fetch HTML, parse DOM, execute scripts, collect CSS, load web fonts, and images. 416 452 fn load_page(loaded: LoadedHtml) -> PageState { 417 453 let doc = parse_html(&loaded.text); 418 454 455 + // Determine the referrer policy. Priority: meta tag > HTTP header > default. 456 + let meta_policy = extract_meta_referrer_policy(&doc); 457 + 419 458 // Execute <script> elements. Scripts may modify the DOM, so this must 420 459 // run before collecting CSS and images (which depend on DOM structure). 421 460 let mut loader = ResourceLoader::new(); 461 + loader.set_document_url(&loaded.base_url); 462 + if let Some(policy) = meta_policy { 463 + loader.set_referrer_policy(policy); 464 + } else if let Some(policy) = loaded.http_referrer_policy { 465 + loader.set_referrer_policy(policy); 466 + } 422 467 let doc = execute_page_scripts(doc, &mut loader, &loaded.base_url); 423 468 424 469 // Fetch external stylesheets and merge with inline <style> elements. ··· 461 506 None => LoadedHtml { 462 507 text: ABOUT_BLANK_HTML.to_string(), 463 508 base_url: Url::parse("about:blank").expect("about:blank is always valid"), 509 + http_referrer_policy: None, 464 510 }, 465 511 }; 466 512
+20 -4
crates/browser/src/script_loader.rs
··· 8 8 use we_js::compiler; 9 9 use we_js::parser::Parser; 10 10 use we_js::vm::Vm; 11 + use we_net::referrer::ReferrerPolicy; 11 12 use we_url::{Origin, Url}; 12 13 13 14 /// Information about a `<script>` element extracted from the DOM. ··· 22 23 async_attr: bool, 23 24 /// The `type` attribute value, if any. 24 25 type_attr: Option<String>, 26 + /// Element-level referrer policy override (`referrerpolicy` attribute). 27 + referrer_policy: Option<ReferrerPolicy>, 25 28 } 26 29 27 30 /// Returns true if a script's `type` attribute indicates it should execute. ··· 70 73 let defer = doc.get_attribute(node, "defer").is_some(); 71 74 let async_attr = doc.get_attribute(node, "async").is_some(); 72 75 let type_attr = doc.get_attribute(node, "type").map(|s| s.to_string()); 76 + let referrer_policy = doc 77 + .get_attribute(node, "referrerpolicy") 78 + .and_then(ReferrerPolicy::parse); 73 79 74 80 let text = if src.is_none() { 75 81 let content = collect_text_content(doc, node); ··· 88 94 defer, 89 95 async_attr, 90 96 type_attr, 97 + referrer_policy, 91 98 } 92 99 } 93 100 ··· 97 104 src: &str, 98 105 base_url: &Url, 99 106 document_origin: &Origin, 107 + element_policy: Option<ReferrerPolicy>, 100 108 ) -> Option<String> { 101 - match loader.fetch_url_subresource( 102 - src, 103 - Some(base_url), 109 + let url = match we_url::Url::parse_with_base(src, base_url).or_else(|_| we_url::Url::parse(src)) 110 + { 111 + Ok(u) => u, 112 + Err(_) => { 113 + eprintln!("[script] invalid URL: {src}"); 114 + return None; 115 + } 116 + }; 117 + match loader.fetch_subresource_with_referrer_policy( 118 + &url, 104 119 document_origin, 105 120 ResourceRequestType::Script, 121 + element_policy, 106 122 ) { 107 123 Ok(Resource::Script { text, .. }) => Some(text), 108 124 Ok(Resource::Other { data, .. }) => { ··· 207 223 // Resolve the script source text. 208 224 let (source, label) = if let Some(ref src) = info.src { 209 225 // External script: fetch it. 210 - match fetch_script_text(loader, src, base_url, document_origin) { 226 + match fetch_script_text(loader, src, base_url, document_origin, info.referrer_policy) { 211 227 Some(text) => (text, src.clone()), 212 228 None => continue, 213 229 }
+2 -1
crates/net/src/lib.rs
··· 1 - //! TCP, DNS, pure-Rust TLS 1.3, HTTP/1.1, HTTP/2, CORS. 1 + //! TCP, DNS, pure-Rust TLS 1.3, HTTP/1.1, HTTP/2, CORS, Referrer Policy. 2 2 3 3 pub mod client; 4 4 pub mod cookie; 5 5 pub mod cors; 6 6 pub mod dns; 7 7 pub mod http; 8 + pub mod referrer; 8 9 pub mod tcp; 9 10 pub mod tls;
+691
crates/net/src/referrer.rs
··· 1 + //! Referrer Policy implementation (W3C Referrer Policy). 2 + //! 3 + //! Computes the value of the `Referer` header sent with HTTP requests based 4 + //! on the active referrer policy, the document URL, and the request URL. 5 + //! 6 + //! Supported policies: 7 + //! - `no-referrer` 8 + //! - `no-referrer-when-downgrade` (the default) 9 + //! - `origin` 10 + //! - `origin-when-cross-origin` 11 + //! - `same-origin` 12 + //! - `strict-origin` 13 + //! - `strict-origin-when-cross-origin` 14 + //! - `unsafe-url` 15 + 16 + use we_url::{Origin, Url}; 17 + 18 + /// The eight referrer policies defined by the W3C Referrer Policy spec. 19 + #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] 20 + pub enum ReferrerPolicy { 21 + /// Never send referrer information. 22 + NoReferrer, 23 + /// Send full URL for same-protocol requests; no referrer on downgrade (HTTPS→HTTP). 24 + /// This is the historical default. 25 + NoReferrerWhenDowngrade, 26 + /// Always send origin only (scheme + host + port). 27 + Origin, 28 + /// Send full URL for same-origin; origin only for cross-origin. 29 + OriginWhenCrossOrigin, 30 + /// Send full URL for same-origin requests only. 31 + SameOrigin, 32 + /// Send origin for same-protocol; no referrer on downgrade. 33 + StrictOrigin, 34 + /// Send full URL for same-origin; origin for cross-origin same-protocol; 35 + /// no referrer on downgrade. This is the modern default. 36 + #[default] 37 + StrictOriginWhenCrossOrigin, 38 + /// Always send the full URL (stripped of fragment and userinfo). 39 + UnsafeUrl, 40 + } 41 + 42 + impl ReferrerPolicy { 43 + /// Parse a referrer policy from a string (case-insensitive). 44 + /// 45 + /// Returns `None` for unrecognized values (per spec, unknown tokens are 46 + /// ignored and the existing policy remains in effect). 47 + pub fn parse(value: &str) -> Option<Self> { 48 + match value.trim().to_ascii_lowercase().as_str() { 49 + "no-referrer" => Some(Self::NoReferrer), 50 + "no-referrer-when-downgrade" => Some(Self::NoReferrerWhenDowngrade), 51 + "origin" => Some(Self::Origin), 52 + "origin-when-cross-origin" => Some(Self::OriginWhenCrossOrigin), 53 + "same-origin" => Some(Self::SameOrigin), 54 + "strict-origin" => Some(Self::StrictOrigin), 55 + "strict-origin-when-cross-origin" => Some(Self::StrictOriginWhenCrossOrigin), 56 + "unsafe-url" => Some(Self::UnsafeUrl), 57 + "" => Some(Self::default()), 58 + _ => None, 59 + } 60 + } 61 + 62 + /// Parse a `Referrer-Policy` HTTP response header. 63 + /// 64 + /// Per spec, the header may contain a comma-separated list of policy tokens. 65 + /// The *last* valid token wins (allows graceful degradation for older clients). 66 + pub fn parse_header(header_value: &str) -> Option<Self> { 67 + let mut result = None; 68 + for token in header_value.split(',') { 69 + if let Some(policy) = Self::parse(token) { 70 + result = Some(policy); 71 + } 72 + } 73 + result 74 + } 75 + 76 + /// Return the policy name as it appears in headers and attributes. 77 + pub fn as_str(&self) -> &'static str { 78 + match self { 79 + Self::NoReferrer => "no-referrer", 80 + Self::NoReferrerWhenDowngrade => "no-referrer-when-downgrade", 81 + Self::Origin => "origin", 82 + Self::OriginWhenCrossOrigin => "origin-when-cross-origin", 83 + Self::SameOrigin => "same-origin", 84 + Self::StrictOrigin => "strict-origin", 85 + Self::StrictOriginWhenCrossOrigin => "strict-origin-when-cross-origin", 86 + Self::UnsafeUrl => "unsafe-url", 87 + } 88 + } 89 + } 90 + 91 + /// Compute the `Referer` header value for a request. 92 + /// 93 + /// Given the document (referrer) URL, the request (destination) URL, and 94 + /// the active referrer policy, returns the appropriate `Referer` header 95 + /// value, or `None` if the referrer should be suppressed entirely. 96 + pub fn compute_referrer( 97 + referrer_url: &Url, 98 + request_url: &Url, 99 + policy: ReferrerPolicy, 100 + ) -> Option<String> { 101 + // Per spec: referrer URLs with non-HTTP(S) schemes don't produce a referrer. 102 + let referrer_scheme = referrer_url.scheme(); 103 + if referrer_scheme != "http" && referrer_scheme != "https" { 104 + return None; 105 + } 106 + 107 + match policy { 108 + ReferrerPolicy::NoReferrer => None, 109 + 110 + ReferrerPolicy::NoReferrerWhenDowngrade => { 111 + if is_downgrade(referrer_url, request_url) { 112 + None 113 + } else { 114 + Some(strip_referrer_url(referrer_url)) 115 + } 116 + } 117 + 118 + ReferrerPolicy::Origin => Some(origin_only(referrer_url)), 119 + 120 + ReferrerPolicy::OriginWhenCrossOrigin => { 121 + if is_same_origin(referrer_url, request_url) { 122 + Some(strip_referrer_url(referrer_url)) 123 + } else { 124 + Some(origin_only(referrer_url)) 125 + } 126 + } 127 + 128 + ReferrerPolicy::SameOrigin => { 129 + if is_same_origin(referrer_url, request_url) { 130 + Some(strip_referrer_url(referrer_url)) 131 + } else { 132 + None 133 + } 134 + } 135 + 136 + ReferrerPolicy::StrictOrigin => { 137 + if is_downgrade(referrer_url, request_url) { 138 + None 139 + } else { 140 + Some(origin_only(referrer_url)) 141 + } 142 + } 143 + 144 + ReferrerPolicy::StrictOriginWhenCrossOrigin => { 145 + if is_same_origin(referrer_url, request_url) { 146 + Some(strip_referrer_url(referrer_url)) 147 + } else if is_downgrade(referrer_url, request_url) { 148 + None 149 + } else { 150 + Some(origin_only(referrer_url)) 151 + } 152 + } 153 + 154 + ReferrerPolicy::UnsafeUrl => Some(strip_referrer_url(referrer_url)), 155 + } 156 + } 157 + 158 + /// Check if the request is a "downgrade" (HTTPS referrer → HTTP destination). 159 + fn is_downgrade(referrer: &Url, request: &Url) -> bool { 160 + referrer.scheme() == "https" && request.scheme() == "http" 161 + } 162 + 163 + /// Check if two URLs are same-origin. 164 + fn is_same_origin(a: &Url, b: &Url) -> bool { 165 + a.origin().same_origin(&b.origin()) 166 + } 167 + 168 + /// Strip a URL for use as a referrer: remove fragment and userinfo, 169 + /// keep scheme, host, port, path, and query. 170 + fn strip_referrer_url(url: &Url) -> String { 171 + let mut result = String::new(); 172 + result.push_str(url.scheme()); 173 + result.push_str("://"); 174 + if let Some(host) = url.host_str() { 175 + result.push_str(&host); 176 + } 177 + if let Some(port) = url.port() { 178 + result.push(':'); 179 + result.push_str(&port.to_string()); 180 + } 181 + result.push_str(&url.path()); 182 + if let Some(query) = url.query() { 183 + result.push('?'); 184 + result.push_str(query); 185 + } 186 + result 187 + } 188 + 189 + /// Return origin-only form of a URL: scheme + "://" + host [+ ":" + port] + "/". 190 + fn origin_only(url: &Url) -> String { 191 + match url.origin() { 192 + Origin::Tuple(scheme, host, port) => { 193 + let mut s = String::new(); 194 + s.push_str(&scheme); 195 + s.push_str("://"); 196 + s.push_str(&host.serialize()); 197 + if let Some(p) = port { 198 + s.push(':'); 199 + s.push_str(&p.to_string()); 200 + } 201 + s.push('/'); 202 + s 203 + } 204 + Origin::Opaque => "null".to_string(), 205 + } 206 + } 207 + 208 + // --------------------------------------------------------------------------- 209 + // Tests 210 + // --------------------------------------------------------------------------- 211 + 212 + #[cfg(test)] 213 + mod tests { 214 + use super::*; 215 + 216 + // -- Policy parsing -- 217 + 218 + #[test] 219 + fn parse_no_referrer() { 220 + assert_eq!( 221 + ReferrerPolicy::parse("no-referrer"), 222 + Some(ReferrerPolicy::NoReferrer) 223 + ); 224 + } 225 + 226 + #[test] 227 + fn parse_no_referrer_when_downgrade() { 228 + assert_eq!( 229 + ReferrerPolicy::parse("no-referrer-when-downgrade"), 230 + Some(ReferrerPolicy::NoReferrerWhenDowngrade) 231 + ); 232 + } 233 + 234 + #[test] 235 + fn parse_origin() { 236 + assert_eq!( 237 + ReferrerPolicy::parse("origin"), 238 + Some(ReferrerPolicy::Origin) 239 + ); 240 + } 241 + 242 + #[test] 243 + fn parse_origin_when_cross_origin() { 244 + assert_eq!( 245 + ReferrerPolicy::parse("origin-when-cross-origin"), 246 + Some(ReferrerPolicy::OriginWhenCrossOrigin) 247 + ); 248 + } 249 + 250 + #[test] 251 + fn parse_same_origin() { 252 + assert_eq!( 253 + ReferrerPolicy::parse("same-origin"), 254 + Some(ReferrerPolicy::SameOrigin) 255 + ); 256 + } 257 + 258 + #[test] 259 + fn parse_strict_origin() { 260 + assert_eq!( 261 + ReferrerPolicy::parse("strict-origin"), 262 + Some(ReferrerPolicy::StrictOrigin) 263 + ); 264 + } 265 + 266 + #[test] 267 + fn parse_strict_origin_when_cross_origin() { 268 + assert_eq!( 269 + ReferrerPolicy::parse("strict-origin-when-cross-origin"), 270 + Some(ReferrerPolicy::StrictOriginWhenCrossOrigin) 271 + ); 272 + } 273 + 274 + #[test] 275 + fn parse_unsafe_url() { 276 + assert_eq!( 277 + ReferrerPolicy::parse("unsafe-url"), 278 + Some(ReferrerPolicy::UnsafeUrl) 279 + ); 280 + } 281 + 282 + #[test] 283 + fn parse_unknown_returns_none() { 284 + assert_eq!(ReferrerPolicy::parse("bogus"), None); 285 + } 286 + 287 + #[test] 288 + fn parse_case_insensitive() { 289 + assert_eq!( 290 + ReferrerPolicy::parse("No-Referrer"), 291 + Some(ReferrerPolicy::NoReferrer) 292 + ); 293 + assert_eq!( 294 + ReferrerPolicy::parse("STRICT-ORIGIN"), 295 + Some(ReferrerPolicy::StrictOrigin) 296 + ); 297 + } 298 + 299 + #[test] 300 + fn parse_with_whitespace() { 301 + assert_eq!( 302 + ReferrerPolicy::parse(" no-referrer "), 303 + Some(ReferrerPolicy::NoReferrer) 304 + ); 305 + } 306 + 307 + #[test] 308 + fn parse_empty_returns_default() { 309 + assert_eq!( 310 + ReferrerPolicy::parse(""), 311 + Some(ReferrerPolicy::StrictOriginWhenCrossOrigin) 312 + ); 313 + } 314 + 315 + // -- Header parsing (comma-separated, last valid wins) -- 316 + 317 + #[test] 318 + fn parse_header_single() { 319 + assert_eq!( 320 + ReferrerPolicy::parse_header("no-referrer"), 321 + Some(ReferrerPolicy::NoReferrer) 322 + ); 323 + } 324 + 325 + #[test] 326 + fn parse_header_multiple_last_wins() { 327 + assert_eq!( 328 + ReferrerPolicy::parse_header("no-referrer, origin"), 329 + Some(ReferrerPolicy::Origin) 330 + ); 331 + } 332 + 333 + #[test] 334 + fn parse_header_unknown_tokens_ignored() { 335 + assert_eq!( 336 + ReferrerPolicy::parse_header("bogus, no-referrer"), 337 + Some(ReferrerPolicy::NoReferrer) 338 + ); 339 + } 340 + 341 + #[test] 342 + fn parse_header_all_unknown() { 343 + assert_eq!(ReferrerPolicy::parse_header("bogus, also-bogus"), None); 344 + } 345 + 346 + #[test] 347 + fn parse_header_graceful_degradation() { 348 + // Newer policy listed first, older fallback listed last. 349 + // Older client ignores unknown, picks fallback. 350 + assert_eq!( 351 + ReferrerPolicy::parse_header( 352 + "strict-origin-when-cross-origin, no-referrer-when-downgrade" 353 + ), 354 + Some(ReferrerPolicy::NoReferrerWhenDowngrade) 355 + ); 356 + } 357 + 358 + // -- as_str round-trip -- 359 + 360 + #[test] 361 + fn as_str_round_trip() { 362 + let policies = [ 363 + ReferrerPolicy::NoReferrer, 364 + ReferrerPolicy::NoReferrerWhenDowngrade, 365 + ReferrerPolicy::Origin, 366 + ReferrerPolicy::OriginWhenCrossOrigin, 367 + ReferrerPolicy::SameOrigin, 368 + ReferrerPolicy::StrictOrigin, 369 + ReferrerPolicy::StrictOriginWhenCrossOrigin, 370 + ReferrerPolicy::UnsafeUrl, 371 + ]; 372 + for policy in &policies { 373 + assert_eq!(ReferrerPolicy::parse(policy.as_str()), Some(*policy)); 374 + } 375 + } 376 + 377 + // -- Default -- 378 + 379 + #[test] 380 + fn default_policy() { 381 + assert_eq!( 382 + ReferrerPolicy::default(), 383 + ReferrerPolicy::StrictOriginWhenCrossOrigin 384 + ); 385 + } 386 + 387 + // -- Referrer stripping helpers -- 388 + 389 + #[test] 390 + fn strip_removes_fragment() { 391 + let url = Url::parse("https://example.com/page#section").unwrap(); 392 + let stripped = strip_referrer_url(&url); 393 + assert_eq!(stripped, "https://example.com/page"); 394 + assert!(!stripped.contains('#')); 395 + } 396 + 397 + #[test] 398 + fn strip_removes_userinfo() { 399 + let url = Url::parse("https://user:pass@example.com/page").unwrap(); 400 + let stripped = strip_referrer_url(&url); 401 + assert_eq!(stripped, "https://example.com/page"); 402 + assert!(!stripped.contains("user")); 403 + assert!(!stripped.contains("pass")); 404 + } 405 + 406 + #[test] 407 + fn strip_preserves_query() { 408 + let url = Url::parse("https://example.com/page?key=value").unwrap(); 409 + let stripped = strip_referrer_url(&url); 410 + assert_eq!(stripped, "https://example.com/page?key=value"); 411 + } 412 + 413 + #[test] 414 + fn strip_preserves_path() { 415 + let url = Url::parse("https://example.com/a/b/c").unwrap(); 416 + let stripped = strip_referrer_url(&url); 417 + assert_eq!(stripped, "https://example.com/a/b/c"); 418 + } 419 + 420 + #[test] 421 + fn strip_preserves_port() { 422 + let url = Url::parse("https://example.com:8443/page").unwrap(); 423 + let stripped = strip_referrer_url(&url); 424 + assert_eq!(stripped, "https://example.com:8443/page"); 425 + } 426 + 427 + #[test] 428 + fn origin_only_format() { 429 + let url = Url::parse("https://example.com/page?q=1#frag").unwrap(); 430 + let origin = origin_only(&url); 431 + assert_eq!(origin, "https://example.com/"); 432 + } 433 + 434 + #[test] 435 + fn origin_only_with_port() { 436 + let url = Url::parse("https://example.com:8443/page").unwrap(); 437 + let origin = origin_only(&url); 438 + assert_eq!(origin, "https://example.com:8443/"); 439 + } 440 + 441 + // -- compute_referrer: NoReferrer -- 442 + 443 + #[test] 444 + fn no_referrer_always_none() { 445 + let doc = Url::parse("https://example.com/page").unwrap(); 446 + let req = Url::parse("https://example.com/other").unwrap(); 447 + assert_eq!( 448 + compute_referrer(&doc, &req, ReferrerPolicy::NoReferrer), 449 + None 450 + ); 451 + } 452 + 453 + // -- compute_referrer: NoReferrerWhenDowngrade -- 454 + 455 + #[test] 456 + fn no_referrer_when_downgrade_same_protocol() { 457 + let doc = Url::parse("https://example.com/page").unwrap(); 458 + let req = Url::parse("https://other.com/resource").unwrap(); 459 + assert_eq!( 460 + compute_referrer(&doc, &req, ReferrerPolicy::NoReferrerWhenDowngrade), 461 + Some("https://example.com/page".to_string()) 462 + ); 463 + } 464 + 465 + #[test] 466 + fn no_referrer_when_downgrade_https_to_http() { 467 + let doc = Url::parse("https://example.com/page").unwrap(); 468 + let req = Url::parse("http://other.com/resource").unwrap(); 469 + assert_eq!( 470 + compute_referrer(&doc, &req, ReferrerPolicy::NoReferrerWhenDowngrade), 471 + None 472 + ); 473 + } 474 + 475 + #[test] 476 + fn no_referrer_when_downgrade_http_to_http() { 477 + let doc = Url::parse("http://example.com/page").unwrap(); 478 + let req = Url::parse("http://other.com/resource").unwrap(); 479 + assert_eq!( 480 + compute_referrer(&doc, &req, ReferrerPolicy::NoReferrerWhenDowngrade), 481 + Some("http://example.com/page".to_string()) 482 + ); 483 + } 484 + 485 + #[test] 486 + fn no_referrer_when_downgrade_http_to_https() { 487 + let doc = Url::parse("http://example.com/page").unwrap(); 488 + let req = Url::parse("https://other.com/resource").unwrap(); 489 + assert_eq!( 490 + compute_referrer(&doc, &req, ReferrerPolicy::NoReferrerWhenDowngrade), 491 + Some("http://example.com/page".to_string()) 492 + ); 493 + } 494 + 495 + // -- compute_referrer: Origin -- 496 + 497 + #[test] 498 + fn origin_always_sends_origin() { 499 + let doc = Url::parse("https://example.com/page?secret=1").unwrap(); 500 + let req = Url::parse("https://other.com/resource").unwrap(); 501 + assert_eq!( 502 + compute_referrer(&doc, &req, ReferrerPolicy::Origin), 503 + Some("https://example.com/".to_string()) 504 + ); 505 + } 506 + 507 + #[test] 508 + fn origin_on_downgrade() { 509 + let doc = Url::parse("https://example.com/page").unwrap(); 510 + let req = Url::parse("http://other.com/resource").unwrap(); 511 + assert_eq!( 512 + compute_referrer(&doc, &req, ReferrerPolicy::Origin), 513 + Some("https://example.com/".to_string()) 514 + ); 515 + } 516 + 517 + // -- compute_referrer: OriginWhenCrossOrigin -- 518 + 519 + #[test] 520 + fn origin_when_cross_origin_same() { 521 + let doc = Url::parse("https://example.com/page?q=1").unwrap(); 522 + let req = Url::parse("https://example.com/other").unwrap(); 523 + assert_eq!( 524 + compute_referrer(&doc, &req, ReferrerPolicy::OriginWhenCrossOrigin), 525 + Some("https://example.com/page?q=1".to_string()) 526 + ); 527 + } 528 + 529 + #[test] 530 + fn origin_when_cross_origin_different() { 531 + let doc = Url::parse("https://example.com/page?q=1").unwrap(); 532 + let req = Url::parse("https://other.com/resource").unwrap(); 533 + assert_eq!( 534 + compute_referrer(&doc, &req, ReferrerPolicy::OriginWhenCrossOrigin), 535 + Some("https://example.com/".to_string()) 536 + ); 537 + } 538 + 539 + // -- compute_referrer: SameOrigin -- 540 + 541 + #[test] 542 + fn same_origin_sends_for_same() { 543 + let doc = Url::parse("https://example.com/page").unwrap(); 544 + let req = Url::parse("https://example.com/other").unwrap(); 545 + assert_eq!( 546 + compute_referrer(&doc, &req, ReferrerPolicy::SameOrigin), 547 + Some("https://example.com/page".to_string()) 548 + ); 549 + } 550 + 551 + #[test] 552 + fn same_origin_suppresses_for_cross() { 553 + let doc = Url::parse("https://example.com/page").unwrap(); 554 + let req = Url::parse("https://other.com/resource").unwrap(); 555 + assert_eq!( 556 + compute_referrer(&doc, &req, ReferrerPolicy::SameOrigin), 557 + None 558 + ); 559 + } 560 + 561 + // -- compute_referrer: StrictOrigin -- 562 + 563 + #[test] 564 + fn strict_origin_same_protocol() { 565 + let doc = Url::parse("https://example.com/page").unwrap(); 566 + let req = Url::parse("https://other.com/resource").unwrap(); 567 + assert_eq!( 568 + compute_referrer(&doc, &req, ReferrerPolicy::StrictOrigin), 569 + Some("https://example.com/".to_string()) 570 + ); 571 + } 572 + 573 + #[test] 574 + fn strict_origin_downgrade() { 575 + let doc = Url::parse("https://example.com/page").unwrap(); 576 + let req = Url::parse("http://other.com/resource").unwrap(); 577 + assert_eq!( 578 + compute_referrer(&doc, &req, ReferrerPolicy::StrictOrigin), 579 + None 580 + ); 581 + } 582 + 583 + #[test] 584 + fn strict_origin_http_to_http() { 585 + let doc = Url::parse("http://example.com/page").unwrap(); 586 + let req = Url::parse("http://other.com/resource").unwrap(); 587 + assert_eq!( 588 + compute_referrer(&doc, &req, ReferrerPolicy::StrictOrigin), 589 + Some("http://example.com/".to_string()) 590 + ); 591 + } 592 + 593 + // -- compute_referrer: StrictOriginWhenCrossOrigin -- 594 + 595 + #[test] 596 + fn strict_origin_cross_origin_same_origin() { 597 + let doc = Url::parse("https://example.com/page?secret=1").unwrap(); 598 + let req = Url::parse("https://example.com/other").unwrap(); 599 + assert_eq!( 600 + compute_referrer(&doc, &req, ReferrerPolicy::StrictOriginWhenCrossOrigin), 601 + Some("https://example.com/page?secret=1".to_string()) 602 + ); 603 + } 604 + 605 + #[test] 606 + fn strict_origin_cross_origin_cross_origin_no_downgrade() { 607 + let doc = Url::parse("https://example.com/page?secret=1").unwrap(); 608 + let req = Url::parse("https://other.com/resource").unwrap(); 609 + assert_eq!( 610 + compute_referrer(&doc, &req, ReferrerPolicy::StrictOriginWhenCrossOrigin), 611 + Some("https://example.com/".to_string()) 612 + ); 613 + } 614 + 615 + #[test] 616 + fn strict_origin_cross_origin_downgrade() { 617 + let doc = Url::parse("https://example.com/page").unwrap(); 618 + let req = Url::parse("http://other.com/resource").unwrap(); 619 + assert_eq!( 620 + compute_referrer(&doc, &req, ReferrerPolicy::StrictOriginWhenCrossOrigin), 621 + None 622 + ); 623 + } 624 + 625 + // -- compute_referrer: UnsafeUrl -- 626 + 627 + #[test] 628 + fn unsafe_url_always_sends_full() { 629 + let doc = Url::parse("https://example.com/page?q=1").unwrap(); 630 + let req = Url::parse("http://other.com/resource").unwrap(); 631 + assert_eq!( 632 + compute_referrer(&doc, &req, ReferrerPolicy::UnsafeUrl), 633 + Some("https://example.com/page?q=1".to_string()) 634 + ); 635 + } 636 + 637 + #[test] 638 + fn unsafe_url_strips_fragment() { 639 + let doc = Url::parse("https://example.com/page#frag").unwrap(); 640 + let req = Url::parse("https://other.com/resource").unwrap(); 641 + let result = compute_referrer(&doc, &req, ReferrerPolicy::UnsafeUrl).unwrap(); 642 + assert!(!result.contains('#')); 643 + assert_eq!(result, "https://example.com/page"); 644 + } 645 + 646 + // -- Non-HTTP referrer URLs -- 647 + 648 + #[test] 649 + fn non_http_referrer_suppressed() { 650 + let doc = Url::parse("file:///home/user/page.html").unwrap(); 651 + let req = Url::parse("https://example.com/resource").unwrap(); 652 + assert_eq!( 653 + compute_referrer(&doc, &req, ReferrerPolicy::UnsafeUrl), 654 + None 655 + ); 656 + } 657 + 658 + #[test] 659 + fn data_url_referrer_suppressed() { 660 + let doc = Url::parse("data:text/html,Hello").unwrap(); 661 + let req = Url::parse("https://example.com/resource").unwrap(); 662 + assert_eq!( 663 + compute_referrer(&doc, &req, ReferrerPolicy::UnsafeUrl), 664 + None 665 + ); 666 + } 667 + 668 + // -- Edge cases -- 669 + 670 + #[test] 671 + fn same_origin_different_port() { 672 + let doc = Url::parse("https://example.com:8443/page").unwrap(); 673 + let req = Url::parse("https://example.com/resource").unwrap(); 674 + // Different ports → different origin 675 + assert_eq!( 676 + compute_referrer(&doc, &req, ReferrerPolicy::SameOrigin), 677 + None 678 + ); 679 + } 680 + 681 + #[test] 682 + fn same_origin_different_scheme() { 683 + let doc = Url::parse("http://example.com/page").unwrap(); 684 + let req = Url::parse("https://example.com/resource").unwrap(); 685 + // Different schemes → different origin 686 + assert_eq!( 687 + compute_referrer(&doc, &req, ReferrerPolicy::SameOrigin), 688 + None 689 + ); 690 + } 691 + }