diff --git a/packages/extension/app/background.js b/packages/extension/app/background.js index ed642603b251..a9853eae5b9c 100644 --- a/packages/extension/app/background.js +++ b/packages/extension/app/background.js @@ -61,6 +61,16 @@ const connect = function (host, path, extraOpts) { // adds a header to the request to mark it as a request for the AUT frame // itself, so the proxy can utilize that for injection purposes browser.webRequest.onBeforeSendHeaders.addListener((details) => { + const requestModifications = { + requestHeaders: [ + ...(details.requestHeaders || []), + ...(details.type === 'xmlhttprequest' ? [{ + name: 'X-Cypress-Request', + value: 'true', + }] : []), + ], + } + if ( // parentFrameId: 0 means the parent is the top-level, so if it isn't // 0, it's nested inside the AUT and can't be the AUT itself @@ -69,11 +79,11 @@ const connect = function (host, path, extraOpts) { || details.type !== 'sub_frame' // is the spec frame, not the AUT || details.url.includes('__cypress') - ) return + ) return requestModifications return { requestHeaders: [ - ...details.requestHeaders, + ...requestModifications.requestHeaders, { name: 'X-Cypress-Is-AUT-Frame', value: 'true', diff --git a/packages/extension/test/integration/background_spec.js b/packages/extension/test/integration/background_spec.js index 494cd65bc8db..0742a6952074 100644 --- a/packages/extension/test/integration/background_spec.js +++ b/packages/extension/test/integration/background_spec.js @@ -297,7 +297,7 @@ describe('app/background', () => { const result = browser.webRequest.onBeforeSendHeaders.addListener.lastCall.args[0](details) - expect(result).to.be.undefined + expect(result).to.deep.equal({ requestHeaders: [] }) }) it('does not add header if it is a nested frame', async function () { @@ -311,7 +311,7 @@ describe('app/background', () => { const result = browser.webRequest.onBeforeSendHeaders.addListener.lastCall.args[0](details) - expect(result).to.be.undefined + expect(result).to.deep.equal({ requestHeaders: [] }) }) it('does not add header if it is not a sub frame request', async function () { @@ -326,7 +326,7 @@ describe('app/background', () => { const result = browser.webRequest.onBeforeSendHeaders.addListener.lastCall.args[0](details) - expect(result).to.be.undefined + expect(result).to.deep.equal({ requestHeaders: [] }) }) it('does not add header if it is a spec frame request', async function () { @@ -341,7 +341,7 @@ describe('app/background', () => { await this.connect(withExperimentalFlagOn) const result = browser.webRequest.onBeforeSendHeaders.addListener.lastCall.args[0](details) - expect(result).to.be.undefined + expect(result).to.deep.equal({ requestHeaders: [] }) }) it('appends X-Cypress-Is-AUT-Frame header to AUT iframe request', async function () { @@ -373,6 +373,60 @@ describe('app/background', () => { }) }) + it('appends X-Cypress-Request header to request if the resourceType is "xmlhttprequest"', async function () { + const details = { + parentFrameId: 0, + type: 'xmlhttprequest', + url: 'http://localhost:3000/index.html', + requestHeaders: [ + { name: 'X-Foo', value: 'Bar' }, + ], + } + + sinon.stub(browser.webRequest.onBeforeSendHeaders, 'addListener') + + await this.connect(withExperimentalFlagOn) + const result = browser.webRequest.onBeforeSendHeaders.addListener.lastCall.args[0](details) + + expect(result).to.deep.equal({ + requestHeaders: [ + { + name: 'X-Foo', + value: 'Bar', + }, + { + name: 'X-Cypress-Request', + value: 'true', + }, + ], + }) + }) + + it('does not append X-Cypress-Request header to request if the resourceType is not an "xmlhttprequest"', async function () { + const details = { + parentFrameId: 0, + type: 'sub_frame', + url: 'http://localhost:3000/index.html', + requestHeaders: [ + { name: 'X-Foo', value: 'Bar' }, + ], + } + + sinon.stub(browser.webRequest.onBeforeSendHeaders, 'addListener') + + await this.connect(withExperimentalFlagOn) + const result = browser.webRequest.onBeforeSendHeaders.addListener.lastCall.args[0](details) + + expect(result).to.not.deep.equal({ + requestHeaders: [ + { + name: 'X-Cypress-Request', + value: 'true', + }, + ], + }) + }) + it('does not add before-headers listener if in non-Firefox browser', async function () { browser.runtime.getBrowserInfo = undefined diff --git a/packages/proxy/lib/http/request-middleware.ts b/packages/proxy/lib/http/request-middleware.ts index 0ad90c3144ea..8a49dc430926 100644 --- a/packages/proxy/lib/http/request-middleware.ts +++ b/packages/proxy/lib/http/request-middleware.ts @@ -21,13 +21,18 @@ const LogRequest: RequestMiddleware = function () { this.next() } -const ExtractIsAUTFrameHeader: RequestMiddleware = function () { +const ExtractCypressMetadataHeaders: RequestMiddleware = function () { this.req.isAUTFrame = !!this.req.headers['x-cypress-is-aut-frame'] if (this.req.headers['x-cypress-is-aut-frame']) { delete this.req.headers['x-cypress-is-aut-frame'] } + if (this.req.headers['x-cypress-request']) { + this.debug(`found x-cypress-request header. Deleting x-cypress-request header.`) + delete this.req.headers['x-cypress-request'] + } + this.next() } @@ -247,7 +252,7 @@ const SendRequestOutgoing: RequestMiddleware = function () { export default { LogRequest, - ExtractIsAUTFrameHeader, + ExtractCypressMetadataHeaders, MaybeSimulateSecHeaders, MaybeAttachCrossOriginCookies, MaybeEndRequestWithBufferedResponse, diff --git a/packages/proxy/test/unit/http/request-middleware.spec.ts b/packages/proxy/test/unit/http/request-middleware.spec.ts index bf14243b7542..7a2968dbeb3b 100644 --- a/packages/proxy/test/unit/http/request-middleware.spec.ts +++ b/packages/proxy/test/unit/http/request-middleware.spec.ts @@ -11,7 +11,7 @@ describe('http/request-middleware', () => { it('exports the members in the correct order', () => { expect(_.keys(RequestMiddleware)).to.have.ordered.members([ 'LogRequest', - 'ExtractIsAUTFrameHeader', + 'ExtractCypressMetadataHeaders', 'MaybeSimulateSecHeaders', 'MaybeAttachCrossOriginCookies', 'MaybeEndRequestWithBufferedResponse', @@ -26,8 +26,8 @@ describe('http/request-middleware', () => { ]) }) - describe('ExtractIsAUTFrameHeader', () => { - const { ExtractIsAUTFrameHeader } = RequestMiddleware + describe('ExtractCypressMetadataHeaders', () => { + const { ExtractCypressMetadataHeaders } = RequestMiddleware it('removes x-cypress-is-aut-frame header when it exists, sets in on the req', async () => { const ctx = { @@ -38,7 +38,7 @@ describe('http/request-middleware', () => { } as Partial, } - await testMiddleware([ExtractIsAUTFrameHeader], ctx) + await testMiddleware([ExtractCypressMetadataHeaders], ctx) .then(() => { expect(ctx.req.headers['x-cypress-is-aut-frame']).not.to.exist expect(ctx.req.isAUTFrame).to.be.true @@ -52,12 +52,40 @@ describe('http/request-middleware', () => { } as Partial, } - await testMiddleware([ExtractIsAUTFrameHeader], ctx) + await testMiddleware([ExtractCypressMetadataHeaders], ctx) .then(() => { expect(ctx.req.headers['x-cypress-is-aut-frame']).not.to.exist expect(ctx.req.isAUTFrame).to.be.false }) }) + + it('removes x-cypress-request header when it exists', async () => { + const ctx = { + req: { + headers: { + 'x-cypress-request': 'true', + }, + } as Partial, + } + + await testMiddleware([ExtractCypressMetadataHeaders], ctx) + .then(() => { + expect(ctx.req.headers['x-cypress-request']).not.to.exist + }) + }) + + it('removes x-cypress-request header when it does not exist', async () => { + const ctx = { + req: { + headers: {}, + } as Partial, + } + + await testMiddleware([ExtractCypressMetadataHeaders], ctx) + .then(() => { + expect(ctx.req.headers['x-cypress-request']).not.to.exist + }) + }) }) describe('MaybeSimulateSecHeaders', () => { diff --git a/packages/server/lib/browsers/chrome.ts b/packages/server/lib/browsers/chrome.ts index 5bea2301576f..3c47bb753cb2 100644 --- a/packages/server/lib/browsers/chrome.ts +++ b/packages/server/lib/browsers/chrome.ts @@ -342,19 +342,19 @@ const _listenForFrameTreeChanges = (client) => { client.on('Page.frameDetached', _updateFrameTree(client, 'Page.frameDetached')) } -const _continueRequest = (client, params, header?) => { +const _continueRequest = (client, params, headers?) => { const details: Protocol.Fetch.ContinueRequestRequest = { requestId: params.requestId, } - if (header) { + if (headers && headers.length) { // headers are received as an object but need to be an array // to modify them const currentHeaders = _.map(params.request.headers, (value, name) => ({ name, value })) details.headers = [ ...currentHeaders, - header, + ...headers, ] } @@ -403,20 +403,34 @@ const _handlePausedRequests = async (client) => { // adds a header to the request to mark it as a request for the AUT frame // itself, so the proxy can utilize that for injection purposes client.on('Fetch.requestPaused', async (params: Protocol.Fetch.RequestPausedEvent) => { + const addedHeaders: { + name: string + value: string + }[] = [] + + if (params.resourceType === 'XHR' || params.resourceType === 'Fetch') { + debug('add X-Cypress-Request header to: %s', params.request.url) + addedHeaders.push({ + name: 'X-Cypress-Request', + value: params.resourceType.toLowerCase(), + }) + } + if ( // is a script, stylesheet, image, etc params.resourceType !== 'Document' || !(await _isAUTFrame(params.frameId)) ) { - return _continueRequest(client, params) + return _continueRequest(client, params, addedHeaders) } debug('add X-Cypress-Is-AUT-Frame header to: %s', params.request.url) - - _continueRequest(client, params, { + addedHeaders.push({ name: 'X-Cypress-Is-AUT-Frame', value: 'true', }) + + return _continueRequest(client, params, addedHeaders) }) } diff --git a/packages/server/lib/browsers/electron.ts b/packages/server/lib/browsers/electron.ts index 639c608b0dbf..5c758d53bb72 100644 --- a/packages/server/lib/browsers/electron.ts +++ b/packages/server/lib/browsers/electron.ts @@ -384,6 +384,15 @@ export = { // adds a header to the request to mark it as a request for the AUT frame // itself, so the proxy can utilize that for injection purposes win.webContents.session.webRequest.onBeforeSendHeaders((details, cb) => { + const requestModifications = { + requestHeaders: { + ...details.requestHeaders, + ...(details.resourceType === 'xhr') ? { + 'X-Cypress-Request': 'true', + } : {}, + }, + } + if ( // isn't an iframe details.resourceType !== 'subFrame' @@ -392,14 +401,14 @@ export = { // is the spec frame, not the AUT || details.url.includes('__cypress') ) { - cb({}) + cb(requestModifications) return } cb({ requestHeaders: { - ...details.requestHeaders, + ...requestModifications.requestHeaders, 'X-Cypress-Is-AUT-Frame': 'true', }, }) diff --git a/packages/server/test/unit/browsers/chrome_spec.js b/packages/server/test/unit/browsers/chrome_spec.js index 428b0a6b6a20..63aef387286d 100644 --- a/packages/server/test/unit/browsers/chrome_spec.js +++ b/packages/server/test/unit/browsers/chrome_spec.js @@ -464,6 +464,70 @@ describe('lib/browsers/chrome', () => { }) }) + it('appends X-Cypress-Request header to fetch request', async function () { + await chrome.open('chrome', 'http://', withExperimentalFlagOn, this.automation) + + this.pageCriClient.on.withArgs('Page.frameAttached').yield() + + await this.pageCriClient.on.withArgs('Fetch.requestPaused').args[0][1]({ + frameId: 'aut-frame-id', + requestId: '1234', + resourceType: 'Fetch', + request: { + url: 'http://localhost:3000/test-request', + headers: { + 'X-Foo': 'Bar', + }, + }, + }) + + expect(this.pageCriClient.send).to.be.calledWith('Fetch.continueRequest', { + requestId: '1234', + headers: [ + { + name: 'X-Foo', + value: 'Bar', + }, + { + name: 'X-Cypress-Request', + value: 'fetch', + }, + ], + }) + }) + + it('appends X-Cypress-Request header to xhr request', async function () { + await chrome.open('chrome', 'http://', withExperimentalFlagOn, this.automation) + + this.pageCriClient.on.withArgs('Page.frameAttached').yield() + + await this.pageCriClient.on.withArgs('Fetch.requestPaused').args[0][1]({ + frameId: 'aut-frame-id', + requestId: '1234', + resourceType: 'XHR', + request: { + url: 'http://localhost:3000/test-request', + headers: { + 'X-Foo': 'Bar', + }, + }, + }) + + expect(this.pageCriClient.send).to.be.calledWith('Fetch.continueRequest', { + requestId: '1234', + headers: [ + { + name: 'X-Foo', + value: 'Bar', + }, + { + name: 'X-Cypress-Request', + value: 'xhr', + }, + ], + }) + }) + it('gets frame tree on Page.frameAttached', async function () { await chrome.open('chrome', 'http://', withExperimentalFlagOn, this.automation) diff --git a/packages/server/test/unit/browsers/electron_spec.js b/packages/server/test/unit/browsers/electron_spec.js index c981ce4f43d7..9786805c9bf4 100644 --- a/packages/server/test/unit/browsers/electron_spec.js +++ b/packages/server/test/unit/browsers/electron_spec.js @@ -358,7 +358,9 @@ describe('lib/browsers/electron', () => { this.win.webContents.session.webRequest.onBeforeSendHeaders.lastCall.args[0](details, cb) expect(cb).to.be.calledOnce - expect(cb).to.be.calledWith({}) + expect(cb).to.be.calledWith({ + requestHeaders: {}, + }) }) }) @@ -378,7 +380,9 @@ describe('lib/browsers/electron', () => { this.win.webContents.session.webRequest.onBeforeSendHeaders.lastCall.args[0](details, cb) expect(cb).to.be.calledOnce - expect(cb).to.be.calledWith({}) + expect(cb).to.be.calledWith({ + requestHeaders: {}, + }) }) }) @@ -402,7 +406,9 @@ describe('lib/browsers/electron', () => { this.win.webContents.session.webRequest.onBeforeSendHeaders.lastCall.args[0](details, cb) expect(cb).to.be.calledOnce - expect(cb).to.be.calledWith({}) + expect(cb).to.be.calledWith({ + requestHeaders: {}, + }) }) }) @@ -424,7 +430,9 @@ describe('lib/browsers/electron', () => { this.win.webContents.session.webRequest.onBeforeSendHeaders.lastCall.args[0](details, cb) - expect(cb).to.be.calledWith({}) + expect(cb).to.be.calledWith({ + requestHeaders: {}, + }) }) }) @@ -441,7 +449,9 @@ describe('lib/browsers/electron', () => { this.win.webContents.session.webRequest.onBeforeSendHeaders.lastCall.args[0](details, cb) - expect(cb).to.be.calledWith({}) + expect(cb).to.be.calledWith({ + requestHeaders: {}, + }) }) }) @@ -475,6 +485,37 @@ describe('lib/browsers/electron', () => { }) }) }) + + it('adds X-Cypress-Request header if xhr request (includes fetch)', function () { + sinon.stub(this.win.webContents.session.webRequest, 'onBeforeSendHeaders') + + return electron._launch(this.win, this.url, this.automation, this.options) + .then(() => { + const details = { + resourceType: 'xhr', + frame: { + parent: { + parent: null, + }, + }, + url: 'http://localhost:3000/test-request', + requestHeaders: { + 'X-Foo': 'Bar', + }, + } + const cb = sinon.stub() + + this.win.webContents.session.webRequest.onBeforeSendHeaders.lastCall.args[0](details, cb) + + expect(cb).to.be.calledOnce + expect(cb).to.be.calledWith({ + requestHeaders: { + 'X-Foo': 'Bar', + 'X-Cypress-Request': 'true', + }, + }) + }) + }) }) })