From 44aa09c244c2f9bcc8f3b5fbcfe58467f2a0fee0 Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Thu, 18 Jun 2020 23:12:15 -0700 Subject: [PATCH] fix(cors): allow intercepting cors requests on chromium --- browsers.json | 2 +- src/chromium/crNetworkManager.ts | 24 ++++++++- src/page.ts | 12 +++++ test/interception.spec.js | 89 ++++++++++++++++++++++++++++++++ 4 files changed, 124 insertions(+), 3 deletions(-) diff --git a/browsers.json b/browsers.json index 4d201b65821f3..1667479489562 100644 --- a/browsers.json +++ b/browsers.json @@ -10,7 +10,7 @@ }, { "name": "webkit", - "revision": "1290" + "revision": "1292" } ] } diff --git a/src/chromium/crNetworkManager.ts b/src/chromium/crNetworkManager.ts index 00b4c5634f29c..ad4d63f3bb912 100644 --- a/src/chromium/crNetworkManager.ts +++ b/src/chromium/crNetworkManager.ts @@ -187,8 +187,28 @@ export class CRNetworkManager { } if (!frame) { - if (requestPausedEvent) - this._client._sendMayFail('Fetch.continueRequest', { requestId: requestPausedEvent.requestId }); + if (requestPausedEvent) { + // CORS options request is generated by the network stack, it is not associated with the frame id. + // If URL matches interception pattern, accept it, assuming that this was intended when setting route. + if (requestPausedEvent.request.method === 'OPTIONS' && this._page._isRouted(requestPausedEvent.request.url)) { + const requestHeaders = requestPausedEvent.request.headers; + const responseHeaders: Protocol.Fetch.HeaderEntry[] = [ + { name: 'Access-Control-Allow-Origin', value: requestHeaders['Access-Control-Allow-Methods'] || '*' }, + { name: 'Access-Control-Allow-Methods', value: requestHeaders['Access-Control-Request-Method'] || 'GET, POST, OPTIONS, DELETE' } + ]; + if (requestHeaders['Access-Control-Request-Headers']) + responseHeaders.push({ name: 'Access-Control-Allow-Headers', value: requestHeaders['Access-Control-Request-Headers'] }); + this._client._sendMayFail('Fetch.fulfillRequest', { + requestId: requestPausedEvent.requestId, + responseCode: 204, + responsePhrase: network.STATUS_TEXTS['204'], + responseHeaders, + body: '', + }); + } else { + this._client._sendMayFail('Fetch.continueRequest', { requestId: requestPausedEvent.requestId }); + } + } return; } let allowInterception = this._userRequestInterceptionEnabled; diff --git a/src/page.ts b/src/page.ts index 4ea0d36e2c3d5..2cdf448cee23b 100644 --- a/src/page.ts +++ b/src/page.ts @@ -425,6 +425,18 @@ export class Page extends EventEmitter { route.continue(); } + _isRouted(requestURL: string): boolean { + for (const { url } of this._routes) { + if (helper.urlMatches(requestURL, url)) + return true; + } + for (const { url } of this._browserContext._routes) { + if (helper.urlMatches(requestURL, url)) + return true; + } + return false; + } + async screenshot(options?: types.ScreenshotOptions): Promise { return this._screenshotter.screenshotPage(options); } diff --git a/test/interception.spec.js b/test/interception.spec.js index 1bb2db49a2322..37119daab70f9 100644 --- a/test/interception.spec.js +++ b/test/interception.spec.js @@ -397,6 +397,95 @@ describe('Page.route', function() { }, server.PREFIX + '/redirect_this'); expect(text).toBe(''); }); + + it('should support cors with GET', async({page, server}) => { + await page.goto(server.EMPTY_PAGE); + await page.route('**/cars*', async (route, request) => { + const headers = request.url().endsWith('allow') ? { 'access-control-allow-origin': '*' } : {}; + await route.fulfill({ + contentType: 'application/json', + headers, + status: 200, + body: JSON.stringify(['electric', 'gas']), + }); + }); + { + // Should succeed + const resp = await page.evaluate(async () => { + const response = await fetch('https://example.com/cars?allow', { mode: 'cors' }); + return response.json(); + }); + expect(resp).toEqual(['electric', 'gas']); + } + { + // Should be rejected + const error = await page.evaluate(async () => { + const response = await fetch('https://example.com/cars?reject', { mode: 'cors' }); + return response.json(); + }).catch(e => e); + expect(error.message).toContain('failed'); + } + }); + + it('should support cors with POST', async({page, server}) => { + await page.goto(server.EMPTY_PAGE); + await page.route('**/cars', async (route) => { + await route.fulfill({ + contentType: 'application/json', + headers: { 'Access-Control-Allow-Origin': '*' }, + status: 200, + body: JSON.stringify(['electric', 'gas']), + }); + }); + const resp = await page.evaluate(async () => { + const response = await fetch('https://example.com/cars', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + mode: 'cors', + body: JSON.stringify({ 'number': 1 }) + }); + return response.json(); + }); + expect(resp).toEqual(['electric', 'gas']); + }); + + it('should support cors for different methods', async({page, server}) => { + await page.goto(server.EMPTY_PAGE); + await page.route('**/cars', async (route, request) => { + await route.fulfill({ + contentType: 'application/json', + headers: { 'Access-Control-Allow-Origin': '*' }, + status: 200, + body: JSON.stringify([request.method(), 'electric', 'gas']), + }); + }); + // First POST + { + const resp = await page.evaluate(async () => { + const response = await fetch('https://example.com/cars', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + mode: 'cors', + body: JSON.stringify({ 'number': 1 }) + }); + return response.json(); + }); + expect(resp).toEqual(['POST', 'electric', 'gas']); + } + // Then DELETE + { + const resp = await page.evaluate(async () => { + const response = await fetch('https://example.com/cars', { + method: 'DELETE', + headers: {}, + mode: 'cors', + body: '' + }); + return response.json(); + }); + expect(resp).toEqual(['DELETE', 'electric', 'gas']); + } + }); }); describe('Request.continue', function() {