diff --git a/src/background/providers/cloudflare.ts b/src/background/providers/cloudflare.ts index 91d7d907..70de5be7 100644 --- a/src/background/providers/cloudflare.ts +++ b/src/background/providers/cloudflare.ts @@ -30,11 +30,17 @@ interface RedeemInfo { token: Token; } +interface IssueInfo { + requestId: string; + formData: { [key: string]: string[] | string }; +} + export class CloudflareProvider implements Provider { static readonly ID: number = 1; private callbacks: Callbacks; private storage: Storage; + private issueInfo: IssueInfo | null; private redeemInfo: RedeemInfo | null; constructor(storage: Storage, callbacks: Callbacks) { @@ -44,6 +50,7 @@ export class CloudflareProvider implements Provider { this.callbacks = callbacks; this.storage = storage; + this.issueInfo = null; this.redeemInfo = null; } @@ -113,6 +120,7 @@ export class CloudflareProvider implements Provider { private async issue( url: string, + headers: { [name: string]: string }, formData: { [key: string]: string[] | string }, ): Promise { const tokens = Array.from(Array(NUMBER_OF_REQUESTED_TOKENS).keys()).map(() => new Token()); @@ -127,13 +135,14 @@ export class CloudflareProvider implements Provider { [ISSUANCE_BODY_PARAM_NAME]: param, }); - const headers = { + const newHeaders = { + ...headers, 'content-type': 'application/x-www-form-urlencoded', [ISSUE_HEADER_NAME]: CloudflareProvider.ID.toString(), }; const response = await axios.post(url, body, { - headers, + headers: newHeaders, responseType: 'text', }); @@ -190,44 +199,76 @@ export class CloudflareProvider implements Provider { handleBeforeSendHeaders( details: chrome.webRequest.WebRequestHeadersDetails, ): chrome.webRequest.BlockingResponse | void { - if (this.redeemInfo === null || details.requestId !== this.redeemInfo.requestId) { - return; + // If we suppose to redeem a token with this request + if (this.redeemInfo !== null && details.requestId === this.redeemInfo.requestId) { + const url = new URL(details.url); + + const token = this.redeemInfo.token; + // Clear the redeem info to indicate that we are already redeeming the token. + this.redeemInfo = null; + + const key = token.getMacKey(); + const binding = voprf.createRequestBinding(key, [ + voprf.getBytesFromString(url.hostname), + voprf.getBytesFromString(details.method + ' ' + url.pathname), + ]); + + const contents = [ + voprf.getBase64FromBytes(token.getInput()), + binding, + voprf.getBase64FromString(JSON.stringify(voprf.defaultECSettings)), + ]; + const redemption = btoa(JSON.stringify({ type: 'Redeem', contents })); + + const headers = details.requestHeaders ?? []; + headers.push({ name: 'challenge-bypass-token', value: redemption }); + + this.callbacks.updateIcon(this.getBadgeText()); + + return { + requestHeaders: headers, + }; } - const url = new URL(details.url); - - const token = this.redeemInfo!.token; - // Clear the redeem info to indicate that we are already redeeming the token. - this.redeemInfo = null; - - const key = token.getMacKey(); - const binding = voprf.createRequestBinding(key, [ - voprf.getBytesFromString(url.hostname), - voprf.getBytesFromString(details.method + ' ' + url.pathname), - ]); - - const contents = [ - voprf.getBase64FromBytes(token.getInput()), - binding, - voprf.getBase64FromString(JSON.stringify(voprf.defaultECSettings)), - ]; - const redemption = btoa(JSON.stringify({ type: 'Redeem', contents })); - - const headers = details.requestHeaders ?? []; - headers.push({ name: 'challenge-bypass-token', value: redemption }); - - this.callbacks.updateIcon(this.getBadgeText()); + // If we suppose to issue tokens with this request + if (this.issueInfo !== null && details.requestId === this.issueInfo.requestId) { + const formData = this.issueInfo.formData; + // Clear the issue info to indicate that we are already issuing tokens. + this.issueInfo = null; + + const headers: { [name: string]: string } = {}; + + if (details.requestHeaders !== undefined) { + details.requestHeaders.forEach((header) => { + // Filter only for Referrer header. + if (header.name === 'Referer' && header.value !== undefined) { + headers[header.name] = header.value; + } + }); + } - return { - requestHeaders: headers, - }; + (async () => { + // Issue tokens. + const tokens = await this.issue(details.url, headers, formData); + // Store tokens. + const cached = this.getStoredTokens(); + this.setStoredTokens(cached.concat(tokens)); + + const url = new URL(details.url); + this.callbacks.navigateUrl(`${url.origin}${url.pathname}`); + })(); + + // TODO I tried to use redirectUrl with data URL or text/html and text/plain but it didn't work, so I continue + // cancelling the request. However, it seems that we can use image/* except image/svg+html. Let's figure how to + // use image data URL later. + // https://blog.mozilla.org/security/2017/11/27/blocking-top-level-navigations-data-urls-firefox-59/ + return { cancel: true }; + } } handleBeforeRequest( details: chrome.webRequest.WebRequestBodyDetails, ): chrome.webRequest.BlockingResponse | void { - const url = new URL(details.url); - if ( details.requestBody === null || details.requestBody === undefined || @@ -253,21 +294,7 @@ export class CloudflareProvider implements Provider { } } - (async () => { - // Issue tokens. - const tokens = await this.issue(details.url, flattenFormData); - // Store tokens. - const cached = this.getStoredTokens(); - this.setStoredTokens(cached.concat(tokens)); - - this.callbacks.navigateUrl(`${url.origin}${url.pathname}`); - })(); - - // TODO I tried to use redirectUrl with data URL or text/html and text/plain but it didn't work, so I continue - // cancelling the request. However, it seems that we can use image/* except image/svg+html. Let's figure how to - // use image data URL later. - // https://blog.mozilla.org/security/2017/11/27/blocking-top-level-navigations-data-urls-firefox-59/ - return { cancel: true }; + this.issueInfo = { requestId: details.requestId, formData: flattenFormData }; } handleHeadersReceived(