Skip to content

Commit

Permalink
Merge pull request #292 from ppopth/change-cloudflare-issue-request-flow
Browse files Browse the repository at this point in the history
Change cloudflare issue request flow
  • Loading branch information
ppopth committed Feb 4, 2022
2 parents 14cfab9 + ac60db3 commit 013485d
Show file tree
Hide file tree
Showing 5 changed files with 286 additions and 85 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "privacy-pass",
"version": "3.0.0",
"version": "3.0.1",
"contributors": [
"Suphanat Chunhapanya <pop@cloudflare.com>",
"Armando Faz <armfazh@cloudflare.com>"
Expand Down
2 changes: 1 addition & 1 deletion public/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"name": "Privacy Pass",
"manifest_version": 2,
"description": "Client support for Privacy Pass anonymous authorization protocol.",
"version": "3.0.0",
"version": "3.0.1",
"icons": {
"32": "icons/32/gold.png",
"48": "icons/48/gold.png",
Expand Down
25 changes: 24 additions & 1 deletion src/background/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,28 @@ declare global {
}
}

declare let browser: any;

window.ACTIVE_TAB_ID = chrome.tabs.TAB_ID_NONE;
window.TABS = new Map<number, Tab>();

const BROWSERS = {
CHROME: 'Chrome',
FIREFOX: 'Firefox',
EDGE: 'Edge',
} as const;
type BROWSERS = typeof BROWSERS[keyof typeof BROWSERS];

function getBrowser(): BROWSERS {
if (typeof chrome !== 'undefined') {
if (typeof browser !== 'undefined') {
return BROWSERS.FIREFOX;
}
return BROWSERS.CHROME;
}
return BROWSERS.EDGE;
}

/* Listeners for navigator */

chrome.tabs.onActivated.addListener(handleActivated);
Expand Down Expand Up @@ -57,10 +76,14 @@ chrome.webRequest.onBeforeRequest.addListener(handleBeforeRequest, { urls: ['<al
'blocking',
]);

const extraInfos = ['requestHeaders', 'blocking'];
if (getBrowser() === BROWSERS.CHROME) {
extraInfos.push('extraHeaders');
}
chrome.webRequest.onBeforeSendHeaders.addListener(
handleBeforeSendHeaders,
{ urls: ['<all_urls>'] },
['requestHeaders', 'blocking'],
extraInfos,
);

chrome.webRequest.onHeadersReceived.addListener(handleHeadersReceived, { urls: ['<all_urls>'] }, [
Expand Down
205 changes: 175 additions & 30 deletions src/background/providers/cloudflare.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,17 +57,32 @@ test('getBadgeText', () => {
});

/*
* The issuance involves handleBeforeRequest listener.
* The issuance involves handleBeforeRequest and handleBeforeSendHeaders
* listeners. In handleBeforeRequest listener,
* 1. Firstly, the listener check if the request looks like the one that we
* should send an issuance request.
* 2. If it passes the check, the listener returns the cancel command to
* cancel the request. If not, it returns nothing and let the request
* continue.
* 3. At the same time the listener returns, it calls a private method
* 2. If it passes the check, The listener sets "issueInfo" property which
* includes the request id and the form data of the request. The property
* will be used by handleBeforeSendHeaders again to issue the tokens. If not,
* it returns nothing and let the request continue.
*
* In handleBeforeSendHeaders,
* 1. The listener will check if the provided request id matches the
* request id in "issueInfo". If so, it means that we are issuing the tokens.
* If not, it returns nothing and let the request continue.
* 2. If it passes the check, the listener extract the form data from
* "issueInfo" clears the "issueInfo" property because "issueInfo" is used
* already. If not, it returns nothing and let the request continue.
* 3. The listener tries to look for the Referer header to get
* the (not PP) token from __cf_chl_tk query param in the Referer url.
* 4. The listener returns the cancel command to cancel the request.
* 5. At the same time the listener returns, it calls a private method
* "issue" to send an issuance request to the server and the method return
* an array of issued tokens.
* 4. The listener stored the issued tokens in the storage.
* 5. The listener reloads the tab to get the proper web page for the tab.);
* an array of issued tokens. In the issuance request, the body will be the
* form data extracted from "issueInfo" earlier and also include the
* __cf_chl_f_tk query param with the token it got from Step 3 (if any).
* 6. The listener stored the issued tokens in the storage.
* 7. The listener reloads the tab to get the proper web page for the tab.
*/
describe('issuance', () => {
describe('handleBeforeRequest', () => {
Expand All @@ -77,12 +92,9 @@ describe('issuance', () => {
const navigateUrl = jest.fn();

const provider = new CloudflareProvider(storage, { updateIcon, navigateUrl });
const tokens = [new Token(), new Token(), new Token()];
const issue = jest.fn(async () => {
return tokens;
});
const issue = jest.fn(async () => []);
provider['issue'] = issue;
const url = 'https://captcha.website/?__cf_chl_captcha_tk__=query-param';
const url = 'https://captcha.website';
const details = {
method: 'POST',
url,
Expand All @@ -92,18 +104,105 @@ describe('issuance', () => {
tabId: 1,
type: 'xmlhttprequest' as chrome.webRequest.ResourceType,
timeStamp: 1,
requestBody: {
formData: {
['h-captcha-response']: ['body-param'],
['cf_captcha_kind']: ['body-param'],
},
},
};
const result = await provider.handleBeforeRequest(details);
expect(result).toBeUndefined();
expect(issue).not.toHaveBeenCalled();
expect(navigateUrl).not.toHaveBeenCalled();

const issueInfo = provider['issueInfo'];
expect(issueInfo!.requestId).toEqual(details.requestId);
expect(issueInfo!.formData).toStrictEqual({
['h-captcha-response']: 'body-param',
['cf_captcha_kind']: 'body-param',
});
});

/*
* The request is invalid only if the body has both
* 'h-captcha-response' and 'cf_captcha_kind' params.
*/
test('invalid request', async () => {
const storage = new StorageMock();
const updateIcon = jest.fn();
const navigateUrl = jest.fn();

const provider = new CloudflareProvider(storage, { updateIcon, navigateUrl });
const issue = jest.fn(async () => []);
provider['issue'] = issue;
const details = {
method: 'GET',
url: 'https://cloudflare.com/',
requestId: 'xxx',
frameId: 1,
parentFrameId: 1,
tabId: 1,
type: 'xmlhttprequest' as chrome.webRequest.ResourceType,
timeStamp: 1,
requestBody: {
formData: {
['h-captcha-response']: ['body-param'],
},
},
};
const result = await provider.handleBeforeRequest(details);
expect(result).toEqual({ cancel: true });
expect(result).toBeUndefined();
expect(issue).not.toHaveBeenCalled();
expect(navigateUrl).not.toHaveBeenCalled();
});
});

describe('handleBeforeSendHeaders', () => {
test('with issueInfo with Referer header', async () => {
const storage = new StorageMock();
const updateIcon = jest.fn();
const navigateUrl = jest.fn();

const provider = new CloudflareProvider(storage, { updateIcon, navigateUrl });
const tokens = [new Token(), new Token(), new Token()];
const issue = jest.fn(async () => {
return tokens;
});
provider['issue'] = issue;
const issueInfo = {
requestId: 'xxx',
formData: {
['h-captcha-response']: 'body-param',
['cf_captcha_kind']: 'body-param',
},
};
provider['issueInfo'] = issueInfo;
const details = {
method: 'POST',
url: 'https://captcha.website',
requestId: 'xxx',
frameId: 1,
parentFrameId: 1,
tabId: 1,
type: 'xmlhttprequest' as chrome.webRequest.ResourceType,
timeStamp: 1,
requestHeaders: [
{
name: 'Referer',
value: 'https://captcha.website/?__cf_chl_tk=token',
},
],
};
const result = await provider.handleBeforeSendHeaders(details);
expect(result).toStrictEqual({ cancel: true });
const newIssueInfo = provider['issueInfo'];
expect(newIssueInfo).toBeNull();

expect(issue.mock.calls.length).toBe(1);
expect(issue).toHaveBeenCalledWith(url, {
expect(issue).toHaveBeenCalledWith('https://captcha.website/?__cf_chl_f_tk=token', {
['h-captcha-response']: 'body-param',
['cf_captcha_kind']: 'body-param',
});

expect(navigateUrl.mock.calls.length).toBe(1);
Expand All @@ -116,17 +215,58 @@ describe('issuance', () => {
);
});

/*
* The request is invalid if any of the followings is true:
* 1. It has no url param of any of the followings:
* a. '__cf_chl_captcha_tk__'
* b. '__cf_chl_managed_tk__'
* 2. It has no body param of any of the followings:
* a. 'g-recaptcha-response'
* b. 'h-captcha-response'
* c. 'cf_captcha_kind'
*/
test('invalid request', async () => {
test('with issueInfo without Referer header', async () => {
const storage = new StorageMock();
const updateIcon = jest.fn();
const navigateUrl = jest.fn();

const provider = new CloudflareProvider(storage, { updateIcon, navigateUrl });
const tokens = [new Token(), new Token(), new Token()];
const issue = jest.fn(async () => {
return tokens;
});
provider['issue'] = issue;
const issueInfo = {
requestId: 'xxx',
formData: {
['h-captcha-response']: 'body-param',
['cf_captcha_kind']: 'body-param',
},
};
provider['issueInfo'] = issueInfo;
const details = {
method: 'POST',
url: 'https://captcha.website/?__cf_chl_f_tk=token',
requestId: 'xxx',
frameId: 1,
parentFrameId: 1,
tabId: 1,
type: 'xmlhttprequest' as chrome.webRequest.ResourceType,
timeStamp: 1,
requestHeaders: [],
};
const result = await provider.handleBeforeSendHeaders(details);
expect(result).toStrictEqual({ cancel: true });
const newIssueInfo = provider['issueInfo'];
expect(newIssueInfo).toBeNull();

expect(issue.mock.calls.length).toBe(1);
expect(issue).toHaveBeenCalledWith('https://captcha.website/?__cf_chl_f_tk=token', {
['h-captcha-response']: 'body-param',
['cf_captcha_kind']: 'body-param',
});

expect(navigateUrl.mock.calls.length).toBe(1);
expect(navigateUrl).toHaveBeenCalledWith('https://captcha.website/');

// Expect the tokens are added.
const storedTokens = provider['getStoredTokens']();
expect(storedTokens.map((token) => token.toString())).toEqual(
tokens.map((token) => token.toString()),
);
});

test('without issueInfo', async () => {
const storage = new StorageMock();
const updateIcon = jest.fn();
const navigateUrl = jest.fn();
Expand All @@ -135,17 +275,22 @@ describe('issuance', () => {
const issue = jest.fn(async () => []);
provider['issue'] = issue;
const details = {
method: 'GET',
url: 'https://cloudflare.com/',
method: 'POST',
url: 'https://captcha.website',
requestId: 'xxx',
frameId: 1,
parentFrameId: 1,
tabId: 1,
type: 'xmlhttprequest' as chrome.webRequest.ResourceType,
timeStamp: 1,
requestBody: {},
requestHeaders: [
{
name: 'Referer',
value: 'https://captcha.website/?__cf_chl_tk=token',
},
],
};
const result = await provider.handleBeforeRequest(details);
const result = await provider.handleBeforeSendHeaders(details);
expect(result).toBeUndefined();
expect(issue).not.toHaveBeenCalled();
expect(navigateUrl).not.toHaveBeenCalled();
Expand Down
Loading

0 comments on commit 013485d

Please sign in to comment.