diff --git a/extension/src/background.ts b/extension/src/background.ts index fb1f2f53..d765dbc6 100644 --- a/extension/src/background.ts +++ b/extension/src/background.ts @@ -1,4 +1,4 @@ -import { endpoint } from "./config"; +import { bundle_name, bundle_prev_name, endpoint } from "./config"; import { db } from "./globals"; import { headersListener, @@ -6,6 +6,7 @@ import { requestListener, startupListener, tabCloseListener, + torCircuitListener, } from "./webcat/listeners"; import { FRAME_TYPES } from "./webcat/resources"; import { setErrorIcon } from "./webcat/ui"; @@ -82,6 +83,22 @@ browser.webRequest.onHeadersReceived.addListener( ["blocking", "responseHeaders"], ); +// In TBB we send circuit hints from the BundleFetcher; this listener maps the hints from the internal +// format to the secure header expected by TBB that cannot be set directly via fetch +browser.webRequest.onBeforeSendHeaders.addListener( + torCircuitListener, + { + urls: [ + `http://*${bundle_name}`, + `https://*${bundle_name}`, + `http://*${bundle_prev_name}`, + `https://*${bundle_prev_name}`, + ], + types: ["xmlhttprequest"], + }, + ["blocking", "requestHeaders"], +); + // Not the best performance idea to act on all tab just for this browser.tabs.onUpdated.addListener((tabId, changeInfo, tab) => { const errorUrl = browser.runtime.getURL("pages/error.html"); diff --git a/extension/src/webcat/interfaces/originstate.ts b/extension/src/webcat/interfaces/originstate.ts index 1313b0e1..3cc72959 100644 --- a/extension/src/webcat/interfaces/originstate.ts +++ b/extension/src/webcat/interfaces/originstate.ts @@ -33,19 +33,29 @@ type BundleFetch = { }; export class BundleFetcher implements Iterable { + public static readonly CircuitHeader = "X-WEBCAT-Circuit-Hint"; + public static readonly SecureCircuitHeader = "Sec-Tor-Circuit-Hint"; + public readonly current: BundleFetch; public readonly previous: BundleFetch; - constructor(base: string) { + constructor(base: string, originUrl: string | undefined) { + const origin = new URL(originUrl || "").origin; this.current = { promise: fetch(`${base}${bundle_name}`, { cache: "no-store", + headers: { + [BundleFetcher.CircuitHeader]: origin, + }, }), }; this.previous = { promise: fetch(`${base}${bundle_prev_name}`, { cache: "no-store", + headers: { + [BundleFetcher.CircuitHeader]: origin, + }, }), }; } diff --git a/extension/src/webcat/listeners.ts b/extension/src/webcat/listeners.ts index 6763af16..755f2b55 100644 --- a/extension/src/webcat/listeners.ts +++ b/extension/src/webcat/listeners.ts @@ -3,6 +3,7 @@ import { db, origins, tabs } from "../globals"; import { getHooks } from "./genhooks"; import { hooksType, metadataRequestSource } from "./interfaces/base"; import { WebcatError } from "./interfaces/errors"; +import { BundleFetcher } from "./interfaces/originstate"; import { logger } from "./logger"; import { validateOrigin } from "./request"; import { FRAME_TYPES } from "./resources"; @@ -94,6 +95,7 @@ export async function headersListener( details.url, details.tabId, metadataRequestSource.worker, + details.originUrl, ); if (result instanceof WebcatError) { origins.delete(fqdn); @@ -230,6 +232,7 @@ export async function requestListener( details.url, details.tabId, metadataRequestSource.main_frame, + details.originUrl, ); if (result instanceof WebcatError) { origins.delete(fqdn); @@ -263,3 +266,32 @@ export async function requestListener( // Returning a response here is a very powerful tool, let's think about it later return {}; } + +export async function torCircuitListener( + details: browser.webRequest._OnBeforeSendHeadersDetails, +): Promise { + if (!details.requestHeaders) { + console.error("FATAL: request headers not available"); + return { cancel: true }; + } + if (isExtensionRequest(details)) { + let headers: browser.webRequest.HttpHeaders; + if ((await browser.runtime.getBrowserInfo()).vendor === "Tor Project") { + headers = details.requestHeaders.map((header) => { + if (header.name === BundleFetcher.CircuitHeader) { + return { + name: BundleFetcher.SecureCircuitHeader, + value: header.value, + }; + } + return header; + }); + } else { + headers = details.requestHeaders.filter((header) => { + return header.name !== BundleFetcher.CircuitHeader; + }); + } + return { requestHeaders: headers }; + } + return {}; +} diff --git a/extension/src/webcat/request.ts b/extension/src/webcat/request.ts index 666779a9..b3341cd5 100644 --- a/extension/src/webcat/request.ts +++ b/extension/src/webcat/request.ts @@ -17,6 +17,7 @@ export async function validateOrigin( url: string, tabId: number, type: metadataRequestSource, + originUrl: string | undefined, ) { const enrollment_hash = await db.getFQDNEnrollment(fqdn); if (enrollment_hash.length === 0) { @@ -63,9 +64,30 @@ export async function validateOrigin( fqdn, ); + // Determine the origin URL to pass to the BundleFetcher, i.e. + // the (scheme, hostname, port) triple of the associated top level frame + switch (type) { + case metadataRequestSource.main_frame: + // If this is main frame navigation, the origin string is the origin of + // the target URL + originUrl = urlobj.origin; + break; + case metadataRequestSource.sub_frame: + // If this is sub frame navigation, the origin string is the origin + // of the tab's top frame + originUrl = new URL((await browser.tabs.get(tabId)).url || "").origin; + break; + case metadataRequestSource.worker: + // If this is a request associated with a worker, the origin is the + // origin of the worker + originUrl = new URL(originUrl || "").origin; + break; + } + // Policy hash is checked at the top and then later again const newFetcher = new BundleFetcher( `${urlobj.protocol}//${fqdn}:${urlobj.port}`, + originUrl, ); const newOriginState = new OriginStateInitial( newFetcher, diff --git a/extension/tests/webcat/response.test.ts b/extension/tests/webcat/response.test.ts index 51d6a55a..713f1d9e 100644 --- a/extension/tests/webcat/response.test.ts +++ b/extension/tests/webcat/response.test.ts @@ -25,7 +25,7 @@ import { SHA256 } from "../../src/webcat/utils"; function makeDummyFetcher(): BundleFetcher { // base URL is irrelevant, fetch will never be awaited in these tests - return new BundleFetcher("https://example.com"); + return new BundleFetcher("https://example.com", "https://example.com"); } // --- Mocks ---