feat: add resource type header to CDP, extension, and electron (#23821)

* feat: add X-Cypress-Request header in extension

* feat: add X-Cypress-Request header in CDP

* feat: add X-Cypress-Request header in electron

* feat: add ExtractRequestedWithAndCredentialsIfApplicable middleware stub to remove the newly added x-cypress-request header

* chore: change defaultHeaders variable name to requestModifications to more accurately reflect usage

* chore: condense ExtractIsAUTFrameHeader and ExtractRequestedWithAndCredentialsIfApplicable into ExtractCypressMetadataHeaders middleware

* test: add anti assertion for x-cypress-request and remove setting request verbage (as it does nothing yet)
This commit is contained in:
Bill Glesias
2022-09-18 22:28:32 -04:00
committed by GitHub
parent 6ee305ba41
commit 0c265638ce
8 changed files with 251 additions and 26 deletions

View File

@@ -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',

View File

@@ -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

View File

@@ -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,

View File

@@ -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<CypressIncomingRequest>,
}
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<CypressIncomingRequest>,
}
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<CypressIncomingRequest>,
}
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<CypressIncomingRequest>,
}
await testMiddleware([ExtractCypressMetadataHeaders], ctx)
.then(() => {
expect(ctx.req.headers['x-cypress-request']).not.to.exist
})
})
})
describe('MaybeSimulateSecHeaders', () => {

View File

@@ -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)
})
}

View File

@@ -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',
},
})

View File

@@ -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)

View File

@@ -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',
},
})
})
})
})
})