1- import { URLMeta , rewriteUrl } from "./url" ;
2- import { BareHeaders } from "@mercuryworkshop/bare-mux" ;
3- const cspHeaders = [
1+ import type {
2+ default as BareClient ,
3+ BareHeaders ,
4+ } from "@mercuryworkshop/bare-mux" ;
5+ import { rewriteUrl , type URLMeta } from "./url" ;
6+ import { getSiteDirective } from "../security/siteTests" ;
7+
8+ interface StoredReferrerPolicies {
9+ get ( url : string ) : Promise < { policy : string ; referrer : string } | null > ;
10+ set ( url : string , policy : string , referrer : string ) : Promise < void > ;
11+ }
12+
13+ /**
14+ * Headers for security policy features that haven't been emulated yet
15+ */
16+ const SEC_HEADERS = new Set ( [
417 "cross-origin-embedder-policy" ,
518 "cross-origin-opener-policy" ,
619 "cross-origin-resource-policy" ,
@@ -20,29 +33,47 @@ const cspHeaders = [
2033 // This needs to be emulated, but for right now it isn't that important of a feature to be worried about
2134 // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Clear-Site-Data
2235 "clear-site-data" ,
23- ] ;
36+ ] ) ;
2437
25- const urlHeaders = [ "location" , "content-location" , "referer" ] ;
38+ /**
39+ * Headers that are actually URLs that need to be rewritten
40+ */
41+ const URL_HEADERS = new Set ( [ "location" , "content-location" , "referer" ] ) ;
2642
2743function rewriteLinkHeader ( link : string , meta : URLMeta ) {
2844 return link . replace ( / < ( .* ) > / gi, ( match ) => rewriteUrl ( match , meta ) ) ;
2945}
3046
31- export function rewriteHeaders ( rawHeaders : BareHeaders , meta : URLMeta ) {
47+ /**
48+ * Rewrites response headers
49+ * @param rawHeaders Headers before they were rewritten
50+ * @param meta Parsed Proxy URL
51+ * @param client `BareClient` instance used for fetching
52+ * @param isNavigationRequest Whether the request is a navigation request
53+ */
54+ export async function rewriteHeaders (
55+ rawHeaders : BareHeaders ,
56+ meta : URLMeta ,
57+ client : BareClient ,
58+ storedReferrerPolicies : StoredReferrerPolicies
59+ ) {
3260 const headers = { } ;
3361
3462 for ( const key in rawHeaders ) {
3563 headers [ key . toLowerCase ( ) ] = rawHeaders [ key ] ;
3664 }
3765
38- cspHeaders . forEach ( ( header ) => {
39- delete headers [ header ] ;
40- } ) ;
66+ for ( const cspHeader of SEC_HEADERS ) {
67+ delete headers [ cspHeader ] ;
68+ }
4169
42- urlHeaders . forEach ( ( header ) => {
43- if ( headers [ header ] )
44- headers [ header ] = rewriteUrl ( headers [ header ] ?. toString ( ) as string , meta ) ;
45- } ) ;
70+ for ( const urlHeader of URL_HEADERS ) {
71+ if ( headers [ urlHeader ] )
72+ headers [ urlHeader ] = rewriteUrl (
73+ headers [ urlHeader ] ?. toString ( ) as string ,
74+ meta
75+ ) ;
76+ }
4677
4778 if ( typeof headers [ "link" ] === "string" ) {
4879 headers [ "link" ] = rewriteLinkHeader ( headers [ "link" ] , meta ) ;
@@ -52,5 +83,85 @@ export function rewriteHeaders(rawHeaders: BareHeaders, meta: URLMeta) {
5283 ) ;
5384 }
5485
86+ // Emulate the referrer policy to set it back to what it should've been without Force Referrer in place
87+ if ( typeof headers [ "referer" ] === "string" ) {
88+ const referrerUrl = new URL ( headers [ "referer" ] ) ;
89+ const storedPolicyData = await storedReferrerPolicies . get ( referrerUrl . href ) ;
90+ if ( storedPolicyData ) {
91+ const storedReferrerPolicy = storedPolicyData . policy
92+ . toLowerCase ( )
93+ . split ( "," )
94+ . map ( ( rawDir ) => rawDir . trim ( ) ) ;
95+ if (
96+ storedReferrerPolicy . includes ( "no-referrer" ) ||
97+ ( storedReferrerPolicy . includes ( "no-referrer-when-downgrade" ) &&
98+ meta . origin . protocol === "http:" &&
99+ referrerUrl . protocol === "https:" )
100+ ) {
101+ delete headers [ "referer" ] ;
102+ } else if ( storedReferrerPolicy . includes ( "origin" ) ) {
103+ headers [ "referer" ] = referrerUrl . origin ;
104+ } else if ( storedReferrerPolicy . includes ( "origin-when-cross-origin" ) ) {
105+ if ( referrerUrl . origin !== meta . origin . origin ) {
106+ headers [ "referer" ] = referrerUrl . origin ;
107+ } else {
108+ headers [ "referer" ] = referrerUrl . href ;
109+ }
110+ } else if ( storedReferrerPolicy . includes ( "same-origin" ) ) {
111+ if ( referrerUrl . origin === meta . origin . origin ) {
112+ headers [ "referer" ] = referrerUrl . href ;
113+ } else {
114+ delete headers [ "referer" ] ;
115+ }
116+ } else if ( storedReferrerPolicy . includes ( "strict-origin" ) ) {
117+ if (
118+ meta . origin . protocol === "http:" &&
119+ referrerUrl . protocol === "https:"
120+ ) {
121+ delete headers [ "referer" ] ;
122+ } else {
123+ headers [ "referer" ] = referrerUrl . origin ;
124+ }
125+ }
126+ // `strict-origin-when-cross-origin` is the default behavior anyway
127+ else {
128+ if ( referrerUrl . origin === meta . origin . origin ) {
129+ headers [ "referer" ] = referrerUrl . href ;
130+ } else if (
131+ meta . origin . protocol === "http:" &&
132+ referrerUrl . protocol === "https:"
133+ ) {
134+ delete headers [ "referer" ] ;
135+ } else {
136+ headers [ "referer" ] = referrerUrl . origin ;
137+ }
138+ }
139+ }
140+ }
141+ if (
142+ typeof headers [ "sec-fetch-dest" ] === "string" &&
143+ headers [ "sec-fetch-dest" ] === ""
144+ ) {
145+ headers [ "sec-fetch-dest" ] = "empty" ;
146+ }
147+
148+ if (
149+ typeof headers [ "sec-fetch-site" ] === "string" &&
150+ headers [ "sec-fetch-site" ] !== "none"
151+ ) {
152+ if ( typeof headers [ "referer" ] === "string" ) {
153+ headers [ "sec-fetch-site" ] = await getSiteDirective (
154+ meta ,
155+ new URL ( headers [ "referer" ] ) ,
156+ client
157+ ) ;
158+ } else {
159+ console . warn (
160+ "Missing referrer header; can't rewrite sec-fetch-site properly. Falling back to unsafe deletion."
161+ ) ;
162+ delete headers [ "sec-fetch-site" ] ;
163+ }
164+ }
165+
55166 return headers ;
56167}
0 commit comments