perf: no longer pause every single request through CDP and only pause requests needed for AUT document [run ci] (#26623)

This commit is contained in:
Bill Glesias
2023-07-21 13:38:35 -04:00
committed by GitHub
parent 79116ef2dc
commit 2e092add12
26 changed files with 335 additions and 619 deletions

View File

@@ -30,7 +30,7 @@ mainBuildFilters: &mainBuildFilters
- /^release\/\d+\.\d+\.\d+$/
# use the following branch as well to ensure that v8 snapshot cache updates are fully tested
- 'update-v8-snapshot-cache-on-develop'
- 'revert-runner'
- 'reduce_requests_paused'
# usually we don't build Mac app - it takes a long time
# but sometimes we want to really confirm we are doing the right thing
@@ -41,7 +41,7 @@ macWorkflowFilters: &darwin-workflow-filters
- equal: [ develop, << pipeline.git.branch >> ]
# use the following branch as well to ensure that v8 snapshot cache updates are fully tested
- equal: [ 'update-v8-snapshot-cache-on-develop', << pipeline.git.branch >> ]
- equal: [ 'revert-runner', << pipeline.git.branch >> ]
- equal: [ 'reduce_requests_paused', << pipeline.git.branch >> ]
- matches:
pattern: /^release\/\d+\.\d+\.\d+$/
value: << pipeline.git.branch >>
@@ -52,7 +52,7 @@ linuxArm64WorkflowFilters: &linux-arm64-workflow-filters
- equal: [ develop, << pipeline.git.branch >> ]
# use the following branch as well to ensure that v8 snapshot cache updates are fully tested
- equal: [ 'update-v8-snapshot-cache-on-develop', << pipeline.git.branch >> ]
- equal: [ 'revert-runner', << pipeline.git.branch >> ]
- equal: [ 'reduce_requests_paused', << pipeline.git.branch >> ]
- matches:
pattern: /^release\/\d+\.\d+\.\d+$/
value: << pipeline.git.branch >>
@@ -72,7 +72,7 @@ windowsWorkflowFilters: &windows-workflow-filters
- equal: [ develop, << pipeline.git.branch >> ]
# use the following branch as well to ensure that v8 snapshot cache updates are fully tested
- equal: [ 'update-v8-snapshot-cache-on-develop', << pipeline.git.branch >> ]
- equal: [ 'revert-runner', << pipeline.git.branch >> ]
- equal: [ 'reduce_requests_paused', << pipeline.git.branch >> ]
- matches:
pattern: /^release\/\d+\.\d+\.\d+$/
value: << pipeline.git.branch >>
@@ -139,7 +139,7 @@ commands:
- run:
name: Check current branch to persist artifacts
command: |
if [[ "$CIRCLE_BRANCH" != "develop" && "$CIRCLE_BRANCH" != "release/"* && "$CIRCLE_BRANCH" != "update-v8-snapshot-cache-on-develop" && "$CIRCLE_BRANCH" != "revert-runner" ]]; then
if [[ "$CIRCLE_BRANCH" != "develop" && "$CIRCLE_BRANCH" != "release/"* && "$CIRCLE_BRANCH" != "update-v8-snapshot-cache-on-develop" && "$CIRCLE_BRANCH" != "reduce_requests_paused" ]]; then
echo "Not uploading artifacts or posting install comment for this branch."
circleci-agent step halt
fi

View File

@@ -1,6 +1,13 @@
<!-- See the ../guides/writing-the-cypress-changelog.md for details on writing the changelog. -->
## 12.17.2
_Released 08/01/2023 (PENDING)_
**Performance:**
- Fixed an issue where unnecessary requests were being paused. No longer sends `X-Cypress-Is-XHR-Or-Fetch` header and infers resource type off of the server pre-request object. Fixes [#26620](https://github.com/cypress-io/cypress/issues/26620) and [#26622](https://github.com/cypress-io/cypress/issues/26622).
## 12.17.2
_Released 07/20/2023_
**Bugfixes:**

View File

@@ -89,7 +89,7 @@ describe('src/cross-origin/patches', { browser: '!webkit', defaultCommandTimeout
cy.then(() => {
expect(Cypress.backend).to.have.been.calledWith('request:sent:with:credentials', {
url: 'http://www.foobar.com:3500/test-request',
requestedWith: 'fetch',
resourceType: 'fetch',
credentialStatus: assertCredentialStatus,
})
})
@@ -109,7 +109,7 @@ describe('src/cross-origin/patches', { browser: '!webkit', defaultCommandTimeout
cy.then(() => {
expect(Cypress.backend).to.have.been.calledWith('request:sent:with:credentials', {
url: 'http://www.foobar.com:3500/test-request',
requestedWith: 'fetch',
resourceType: 'fetch',
credentialStatus: assertCredentialStatus,
})
})
@@ -129,7 +129,7 @@ describe('src/cross-origin/patches', { browser: '!webkit', defaultCommandTimeout
cy.then(() => {
expect(Cypress.backend).to.have.been.calledWith('request:sent:with:credentials', {
url: 'http://www.foobar.com:3500/test-request',
requestedWith: 'fetch',
resourceType: 'fetch',
credentialStatus: assertCredentialStatus,
})
})
@@ -173,7 +173,7 @@ describe('src/cross-origin/patches', { browser: '!webkit', defaultCommandTimeout
cy.then(() => {
expect(Cypress.backend).to.have.been.calledWith('request:sent:with:credentials', {
url: 'http://app.foobar.com:3500/test-request',
requestedWith: 'fetch',
resourceType: 'fetch',
credentialStatus: 'include',
})
})
@@ -218,7 +218,7 @@ describe('src/cross-origin/patches', { browser: '!webkit', defaultCommandTimeout
cy.then(() => {
expect(Cypress.backend).to.have.been.calledWith('request:sent:with:credentials', {
url: 'http://www.foobar.com:3500/test-request-credentials',
requestedWith: 'fetch',
resourceType: 'fetch',
credentialStatus: assertCredentialStatus,
})
})
@@ -248,7 +248,7 @@ describe('src/cross-origin/patches', { browser: '!webkit', defaultCommandTimeout
cy.then(() => {
expect(Cypress.backend).to.have.been.calledWith('request:sent:with:credentials', {
url: 'http://www.foobar.com:3500/test-request-credentials',
requestedWith: 'fetch',
resourceType: 'fetch',
credentialStatus: assertCredentialStatus,
})
})
@@ -276,7 +276,7 @@ describe('src/cross-origin/patches', { browser: '!webkit', defaultCommandTimeout
cy.then(() => {
expect(Cypress.backend).to.have.been.calledWith('request:sent:with:credentials', {
url: 'http://www.foobar.com:3500/test-request-credentials',
requestedWith: 'fetch',
resourceType: 'fetch',
credentialStatus: assertCredentialStatus,
})
})
@@ -327,7 +327,7 @@ describe('src/cross-origin/patches', { browser: '!webkit', defaultCommandTimeout
cy.then(() => {
expect(Cypress.backend).to.have.been.calledWith('request:sent:with:credentials', {
url: 'http://app.foobar.com:3500/test-request',
requestedWith: 'fetch',
resourceType: 'fetch',
credentialStatus: 'include',
})
})
@@ -346,7 +346,7 @@ describe('src/cross-origin/patches', { browser: '!webkit', defaultCommandTimeout
cy.then(() => {
expect(Cypress.backend).to.have.been.calledWith('request:sent:with:credentials', {
url: 'http://localhost:3500/foo.bar.baz.json',
requestedWith: 'fetch',
resourceType: 'fetch',
credentialStatus: 'same-origin',
})
})
@@ -410,7 +410,7 @@ describe('src/cross-origin/patches', { browser: '!webkit', defaultCommandTimeout
cy.then(() => {
expect(Cypress.backend).to.have.been.calledWith('request:sent:with:credentials', {
url: 'http://www.foobar.com:3500/test-request',
requestedWith: 'xhr',
resourceType: 'xhr',
credentialStatus: withCredentials,
})
})
@@ -450,7 +450,7 @@ describe('src/cross-origin/patches', { browser: '!webkit', defaultCommandTimeout
cy.then(() => {
expect(Cypress.backend).to.have.been.calledWith('request:sent:with:credentials', {
url: 'http://app.foobar.com:3500/test-request',
requestedWith: 'xhr',
resourceType: 'xhr',
credentialStatus: true,
})
})
@@ -502,7 +502,7 @@ describe('src/cross-origin/patches', { browser: '!webkit', defaultCommandTimeout
cy.then(() => {
expect(Cypress.backend).to.have.been.calledWith('request:sent:with:credentials', {
url: 'http://www.foobar.com:3500/test-request-credentials',
requestedWith: 'xhr',
resourceType: 'xhr',
credentialStatus: withCredentials,
})
})
@@ -554,7 +554,7 @@ describe('src/cross-origin/patches', { browser: '!webkit', defaultCommandTimeout
cy.then(() => {
expect(Cypress.backend).to.have.been.calledWith('request:sent:with:credentials', {
url: 'http://app.foobar.com:3500/test-request',
requestedWith: 'xhr',
resourceType: 'xhr',
credentialStatus: true,
})
})
@@ -574,7 +574,7 @@ describe('src/cross-origin/patches', { browser: '!webkit', defaultCommandTimeout
cy.then(() => {
expect(Cypress.backend).to.have.been.calledWith('request:sent:with:credentials', {
url: 'http://localhost:3500/foo.bar.baz.json',
requestedWith: 'xhr',
resourceType: 'xhr',
credentialStatus: false,
})
})

View File

@@ -63,43 +63,24 @@ 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 || []),
/**
* Unlike CDP, the web extensions onBeforeSendHeaders resourceType cannot discern the difference
* between fetch or xhr resource types, but classifies both as 'xmlhttprequest'. Because of this,
* we set X-Cypress-Is-XHR-Or-Fetch to true if the request is made with 'xhr' or 'fetch' so the
* middleware doesn't incorrectly assume which request type is being sent
* @see https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/webRequest/ResourceType
*/
...(details.type === 'xmlhttprequest' ? [{
name: 'X-Cypress-Is-XHR-Or-Fetch',
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
details.parentFrameId !== 0
// isn't an iframe
|| details.type !== 'sub_frame'
// is the spec frame, not the AUT
|| details.url.includes('__cypress')
) return requestModifications
) return
return {
requestHeaders: [
...requestModifications.requestHeaders,
...details.requestHeaders,
{
name: 'X-Cypress-Is-AUT-Frame',
value: 'true',
},
],
}
}, { urls: ['<all_urls>'] }, ['blocking', 'requestHeaders'])
}, { urls: ['<all_urls>'], types: ['sub_frame'] }, ['blocking', 'requestHeaders'])
})
const fail = (id, err) => {

View File

@@ -285,7 +285,7 @@ describe('app/background', () => {
const result = browser.webRequest.onBeforeSendHeaders.addListener.lastCall.args[0](details)
expect(result).to.deep.equal({ requestHeaders: [] })
expect(result).to.be.undefined
})
it('does not add header if it is a nested frame', async function () {
@@ -299,22 +299,7 @@ describe('app/background', () => {
const result = browser.webRequest.onBeforeSendHeaders.addListener.lastCall.args[0](details)
expect(result).to.deep.equal({ requestHeaders: [] })
})
it('does not add header if it is not a sub frame request', async function () {
const details = {
parentFrameId: 0,
type: 'stylesheet',
}
sinon.stub(browser.webRequest.onBeforeSendHeaders, 'addListener')
await this.connect()
const result = browser.webRequest.onBeforeSendHeaders.addListener.lastCall.args[0](details)
expect(result).to.deep.equal({ requestHeaders: [] })
expect(result).to.be.undefined
})
it('does not add header if it is a spec frame request', async function () {
@@ -329,7 +314,7 @@ describe('app/background', () => {
await this.connect()
const result = browser.webRequest.onBeforeSendHeaders.addListener.lastCall.args[0](details)
expect(result).to.deep.equal({ requestHeaders: [] })
expect(result).to.be.undefined
})
it('appends X-Cypress-Is-AUT-Frame header to AUT iframe request', async function () {
@@ -361,60 +346,6 @@ describe('app/background', () => {
})
})
it('appends X-Cypress-Is-XHR-Or-Fetch 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()
const result = browser.webRequest.onBeforeSendHeaders.addListener.lastCall.args[0](details)
expect(result).to.deep.equal({
requestHeaders: [
{
name: 'X-Foo',
value: 'Bar',
},
{
name: 'X-Cypress-Is-XHR-Or-Fetch',
value: 'true',
},
],
})
})
it('does not append X-Cypress-Is-XHR-Or-Fetch 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()
const result = browser.webRequest.onBeforeSendHeaders.addListener.lastCall.args[0](details)
expect(result).to.not.deep.equal({
requestHeaders: [
{
name: 'X-Cypress-Is-XHR-Or-Fetch',
value: 'true',
},
],
})
})
it('does not add before-headers listener if in non-Firefox browser', async function () {
browser.runtime.getBrowserInfo = undefined

View File

@@ -24,7 +24,7 @@ import type { Readable } from 'stream'
import type { Request, Response } from 'express'
import type { RemoteStates } from '@packages/server/lib/remote_states'
import type { CookieJar, SerializableAutomationCookie } from '@packages/server/lib/util/cookies'
import type { RequestedWithAndCredentialManager } from '@packages/server/lib/util/requestedWithAndCredentialManager'
import type { ResourceTypeAndCredentialManager } from '@packages/server/lib/util/resourceTypeAndCredentialManager'
function getRandomColorFn () {
return chalk.hex(`#${Number(
@@ -82,7 +82,7 @@ export type ServerCtx = Readonly<{
getFileServerToken: () => string | undefined
getCookieJar: () => CookieJar
remoteStates: RemoteStates
requestedWithAndCredentialManager: RequestedWithAndCredentialManager
resourceTypeAndCredentialManager: ResourceTypeAndCredentialManager
getRenderedHTMLOrigins: Http['getRenderedHTMLOrigins']
netStubbingState: NetStubbingState
middleware: HttpMiddlewareStacks
@@ -258,7 +258,7 @@ export class Http {
request: any
socket: CyServer.Socket
serverBus: EventEmitter
requestedWithAndCredentialManager: RequestedWithAndCredentialManager
resourceTypeAndCredentialManager: ResourceTypeAndCredentialManager
renderedHTMLOrigins: {[key: string]: boolean} = {}
autUrl?: string
getCookieJar: () => CookieJar
@@ -275,7 +275,7 @@ export class Http {
this.socket = opts.socket
this.request = opts.request
this.serverBus = opts.serverBus
this.requestedWithAndCredentialManager = opts.requestedWithAndCredentialManager
this.resourceTypeAndCredentialManager = opts.resourceTypeAndCredentialManager
this.getCookieJar = opts.getCookieJar
if (typeof opts.middleware === 'undefined') {
@@ -303,7 +303,7 @@ export class Http {
netStubbingState: this.netStubbingState,
socket: this.socket,
serverBus: this.serverBus,
requestedWithAndCredentialManager: this.requestedWithAndCredentialManager,
resourceTypeAndCredentialManager: this.resourceTypeAndCredentialManager,
getCookieJar: this.getCookieJar,
simulatedCookies: [],
debug: (formatter, ...args) => {

View File

@@ -31,7 +31,6 @@ const ExtractCypressMetadataHeaders: RequestMiddleware = function () {
const span = telemetry.startSpan({ name: 'extract:cypress:metadata:headers', parentSpan: this.reqMiddlewareSpan, isVerbose })
this.req.isAUTFrame = !!this.req.headers['x-cypress-is-aut-frame']
const requestIsXhrOrFetch = this.req.headers['x-cypress-is-xhr-or-fetch']
span?.setAttributes({
isAUTFrame: this.req.isAUTFrame,
@@ -41,34 +40,6 @@ const ExtractCypressMetadataHeaders: RequestMiddleware = function () {
delete this.req.headers['x-cypress-is-aut-frame']
}
if (this.req.headers['x-cypress-is-xhr-or-fetch']) {
this.debug(`found x-cypress-is-xhr-or-fetch header. Deleting x-cypress-is-xhr-or-fetch header.`)
delete this.req.headers['x-cypress-is-xhr-or-fetch']
}
if (!doesTopNeedToBeSimulated(this) ||
// this should be unreachable, as the x-cypress-is-xhr-or-fetch header is only attached if
// the resource type is 'xhr' or 'fetch or 'true' (in the case of electron|extension).
// This is only needed for defensive purposes.
(requestIsXhrOrFetch !== 'true' && requestIsXhrOrFetch !== 'xhr' && requestIsXhrOrFetch !== 'fetch')) {
this.next()
return
}
this.debug(`looking up credentials for ${this.req.proxiedUrl}`)
const { requestedWith, credentialStatus } = this.requestedWithAndCredentialManager.get(this.req.proxiedUrl, requestIsXhrOrFetch !== 'true' ? requestIsXhrOrFetch : undefined)
this.debug(`credentials calculated for ${requestedWith}:${credentialStatus}`)
this.req.requestedWith = requestedWith
this.req.credentialsLevel = credentialStatus
span?.setAttributes({
calculatedResourceType: this.req.resourceType,
credentialsLevel: credentialStatus,
})
span?.end()
this.next()
}
@@ -103,74 +74,6 @@ const MaybeSimulateSecHeaders: RequestMiddleware = function () {
this.next()
}
const MaybeAttachCrossOriginCookies: RequestMiddleware = function () {
const span = telemetry.startSpan({ name: 'maybe:attach:cross:origin:cookies', parentSpan: this.reqMiddlewareSpan, isVerbose })
const doesTopNeedSimulation = doesTopNeedToBeSimulated(this)
span?.setAttributes({
doesTopNeedToBeSimulated: doesTopNeedSimulation,
resourceType: this.req.resourceType,
})
if (!doesTopNeedSimulation) {
span?.end()
return this.next()
}
// Top needs to be simulated since the AUT is in a cross origin state. Get the "requested with" and credentials and see what cookies need to be attached
const currentAUTUrl = this.getAUTUrl()
const shouldCookiesBeAttachedToRequest = shouldAttachAndSetCookies(this.req.proxiedUrl, currentAUTUrl, this.req.requestedWith, this.req.credentialsLevel, this.req.isAUTFrame)
span?.setAttributes({
currentAUTUrl,
shouldCookiesBeAttachedToRequest,
})
this.debug(`should cookies be attached to request?: ${shouldCookiesBeAttachedToRequest}`)
if (!shouldCookiesBeAttachedToRequest) {
span?.end()
return this.next()
}
const sameSiteContext = getSameSiteContext(
currentAUTUrl,
this.req.proxiedUrl,
this.req.isAUTFrame,
)
span?.setAttributes({
sameSiteContext,
currentAUTUrl,
isAUTFrame: this.req.isAUTFrame,
})
const applicableCookiesInCookieJar = this.getCookieJar().getCookies(this.req.proxiedUrl, sameSiteContext)
const cookiesOnRequest = (this.req.headers['cookie'] || '').split('; ')
const existingCookiesInJar = applicableCookiesInCookieJar.join('; ')
const addedCookiesFromHeader = cookiesOnRequest.join('; ')
this.debug('existing cookies on request from cookie jar: %s', existingCookiesInJar)
this.debug('add cookies to request from header: %s', addedCookiesFromHeader)
// if the cookie header is empty (i.e. ''), set it to undefined for expected behavior
this.req.headers['cookie'] = addCookieJarCookiesToRequest(applicableCookiesInCookieJar, cookiesOnRequest) || undefined
span?.setAttributes({
existingCookiesInJar,
addedCookiesFromHeader,
cookieHeader: this.req.headers['cookie'],
})
this.debug('cookies being sent with request: %s', this.req.headers['cookie'])
span?.end()
this.next()
}
const CorrelateBrowserPreRequest: RequestMiddleware = async function () {
const span = telemetry.startSpan({ name: 'correlate:prerequest', parentSpan: this.reqMiddlewareSpan, isVerbose })
@@ -231,6 +134,93 @@ const CorrelateBrowserPreRequest: RequestMiddleware = async function () {
}))
}
const CalculateCredentialLevelIfApplicable: RequestMiddleware = function () {
if (!doesTopNeedToBeSimulated(this) ||
(this.req.resourceType !== undefined && this.req.resourceType !== 'xhr' && this.req.resourceType !== 'fetch')) {
this.next()
return
}
this.debug(`looking up credentials for ${this.req.proxiedUrl}`)
const { credentialStatus, resourceType } = this.resourceTypeAndCredentialManager.get(this.req.proxiedUrl, this.req.resourceType)
this.debug(`credentials calculated for ${resourceType}:${credentialStatus}`)
// if for some reason the resourceType is not set by the prerequest, have a fallback in place
this.req.resourceType = !this.req.resourceType ? resourceType : this.req.resourceType
this.req.credentialsLevel = credentialStatus
this.next()
}
const MaybeAttachCrossOriginCookies: RequestMiddleware = function () {
const span = telemetry.startSpan({ name: 'maybe:attach:cross:origin:cookies', parentSpan: this.reqMiddlewareSpan, isVerbose })
const doesTopNeedSimulation = doesTopNeedToBeSimulated(this)
span?.setAttributes({
doesTopNeedToBeSimulated: doesTopNeedSimulation,
resourceType: this.req.resourceType,
})
if (!doesTopNeedSimulation) {
span?.end()
return this.next()
}
// Top needs to be simulated since the AUT is in a cross origin state. Get the "requested with" and credentials and see what cookies need to be attached
const currentAUTUrl = this.getAUTUrl()
const shouldCookiesBeAttachedToRequest = shouldAttachAndSetCookies(this.req.proxiedUrl, currentAUTUrl, this.req.resourceType, this.req.credentialsLevel, this.req.isAUTFrame)
span?.setAttributes({
currentAUTUrl,
shouldCookiesBeAttachedToRequest,
})
this.debug(`should cookies be attached to request?: ${shouldCookiesBeAttachedToRequest}`)
if (!shouldCookiesBeAttachedToRequest) {
span?.end()
return this.next()
}
const sameSiteContext = getSameSiteContext(
currentAUTUrl,
this.req.proxiedUrl,
this.req.isAUTFrame,
)
span?.setAttributes({
sameSiteContext,
currentAUTUrl,
isAUTFrame: this.req.isAUTFrame,
})
const applicableCookiesInCookieJar = this.getCookieJar().getCookies(this.req.proxiedUrl, sameSiteContext)
const cookiesOnRequest = (this.req.headers['cookie'] || '').split('; ')
const existingCookiesInJar = applicableCookiesInCookieJar.join('; ')
const addedCookiesFromHeader = cookiesOnRequest.join('; ')
this.debug('existing cookies on request from cookie jar: %s', existingCookiesInJar)
this.debug('add cookies to request from header: %s', addedCookiesFromHeader)
// if the cookie header is empty (i.e. ''), set it to undefined for expected behavior
this.req.headers['cookie'] = addCookieJarCookiesToRequest(applicableCookiesInCookieJar, cookiesOnRequest) || undefined
span?.setAttributes({
existingCookiesInJar,
addedCookiesFromHeader,
cookieHeader: this.req.headers['cookie'],
})
this.debug('cookies being sent with request: %s', this.req.headers['cookie'])
span?.end()
this.next()
}
function shouldLog (req: CypressIncomingRequest) {
// 1. Any matching `cy.intercept()` should cause `req` to be logged by default, unless `log: false` is passed explicitly.
if (req.matchingRoutes?.length) {
@@ -525,9 +515,10 @@ export default {
LogRequest,
ExtractCypressMetadataHeaders,
MaybeSimulateSecHeaders,
CorrelateBrowserPreRequest,
CalculateCredentialLevelIfApplicable,
MaybeAttachCrossOriginCookies,
MaybeEndRequestWithBufferedResponse,
CorrelateBrowserPreRequest,
SetMatchingRoutes,
SendToDriver,
InterceptRequest,

View File

@@ -580,7 +580,7 @@ const MaybeCopyCookiesFromIncomingRes: ResponseMiddleware = async function () {
url: this.req.proxiedUrl,
isAUTFrame: this.req.isAUTFrame,
doesTopNeedSimulating,
requestedWith: this.req.requestedWith,
resourceType: this.req.resourceType,
credentialLevel: this.req.credentialsLevel,
},
})

View File

@@ -4,7 +4,8 @@ import { URL } from 'url'
import { cors } from '@packages/network'
import { urlOriginsMatch, urlSameSiteMatch } from '@packages/network/lib/cors'
import { SerializableAutomationCookie, Cookie, CookieJar, toughCookieToAutomationCookie } from '@packages/server/lib/util/cookies'
import type { RequestCredentialLevel, RequestedWithHeader } from '../../types'
import type { RequestCredentialLevel } from '../../types'
import type { ResourceType } from 'cypress/types/net-stubbing'
type SiteContext = 'same-origin' | 'same-site' | 'cross-site'
@@ -12,7 +13,7 @@ interface RequestDetails {
url: string
isAUTFrame: boolean
doesTopNeedSimulating: boolean
requestedWith?: RequestedWithHeader
resourceType?: ResourceType
credentialLevel?: RequestCredentialLevel
}
@@ -23,18 +24,18 @@ interface RequestDetails {
* which is critical for lax cookies
* @param {string} requestUrl - the url of the request
* @param {string} AUTUrl - The current url of the app under test
* @param {requestedWith} [requestedWith] -
* @param {resourceType} [resourceType] - the request resourceType
* @param {RequestCredentialLevel} [credentialLevel] - The credentialLevel of the request. For `fetch` this is `omit|same-origin|include` (defaults to same-origin)
* and for `XmlHttpRequest` it is `true|false` (defaults to false)
* @param {isAutFrame} [boolean] - whether or not the request is from the AUT Iframe or not
* @returns {boolean}
*/
export const shouldAttachAndSetCookies = (requestUrl: string, AUTUrl: string | undefined, requestedWith?: RequestedWithHeader, credentialLevel?: RequestCredentialLevel, isAutFrame?: boolean): boolean => {
export const shouldAttachAndSetCookies = (requestUrl: string, AUTUrl: string | undefined, resourceType?: ResourceType, credentialLevel?: RequestCredentialLevel, isAutFrame?: boolean): boolean => {
if (!AUTUrl) return false
const siteContext = calculateSiteContext(requestUrl, AUTUrl)
switch (requestedWith) {
switch (resourceType) {
case 'fetch':
// never attach cookies regardless of siteContext if omit is optioned
if (credentialLevel === 'omit') {
@@ -59,7 +60,7 @@ export const shouldAttachAndSetCookies = (requestUrl: string, AUTUrl: string | u
return false
default:
// if we cannot determine a resource level, we likely should store the cookie as it is a navigation or another event as long as the context is same-origin
// if we cannot determine a resource level or it isn't applicable,, we likely should store the cookie as it is a navigation or another event as long as the context is same-origin
if (siteContext === 'same-origin' || isAutFrame) {
return true
}
@@ -220,7 +221,9 @@ export class CookiesHelper {
// cross site cookies cannot set lax/strict cookies in the browser for xhr/fetch requests (but ok with navigation/document requests)
// NOTE: This is allowable in firefox as the default cookie behavior is no_restriction (none). However, this shouldn't
// impact what is happening in the server-side cookie jar as Set-Cookie is still called and firefox will allow it to be set in the browser
if (this.request.requestedWith && this.siteContext === 'cross-site' && toughCookie.sameSite !== 'none') {
const isXhrOrFetchRequest = this.request.resourceType === 'fetch' || this.request.resourceType === 'xhr'
if (isXhrOrFetchRequest && this.siteContext === 'cross-site' && toughCookie.sameSite !== 'none') {
this.debug(`cannot set cookie with SameSite=${toughCookie.sameSite} when site context is ${this.siteContext}`)
return
@@ -228,10 +231,10 @@ export class CookiesHelper {
// don't set the cookie in our own cookie jar if the cookie would otherwise fail being set in the browser if the AUT Url
// was actually top. This prevents cookies from being applied to our cookie jar when they shouldn't, preventing possible security implications.
const shouldSetCookieGivenSiteContext = shouldAttachAndSetCookies(this.request.url, this.currentAUTUrl, this.request.requestedWith, this.request.credentialLevel, this.request.isAUTFrame)
const shouldSetCookieGivenSiteContext = shouldAttachAndSetCookies(this.request.url, this.currentAUTUrl, this.request.resourceType, this.request.credentialLevel, this.request.isAUTFrame)
if (!shouldSetCookieGivenSiteContext) {
this.debug(`not setting cookie for ${this.request.url} with simulated top ${ this.currentAUTUrl} for ${ this.request.requestedWith}:${this.request.credentialLevel}, cookie: ${toughCookie}`)
this.debug(`not setting cookie for ${this.request.url} with simulated top ${ this.currentAUTUrl} for ${ this.request.resourceType}:${this.request.credentialLevel}, cookie: ${toughCookie}`)
return
}

View File

@@ -15,7 +15,6 @@ export type CypressIncomingRequest = Request & {
responseTimeout?: number
followRedirect?: boolean
isAUTFrame: boolean
requestedWith?: RequestedWithHeader
credentialsLevel?: RequestCredentialLevel
/**
* Resource type from browserPreRequest. Copied to req so intercept matching can work.
@@ -27,8 +26,6 @@ export type CypressIncomingRequest = Request & {
matchingRoutes?: BackendRoute[]
}
export type RequestedWithHeader = 'fetch' | 'xhr' | 'true'
export type RequestCredentialLevel = 'same-origin' | 'include' | 'omit' | boolean
export type CypressWantsInjection = 'full' | 'fullCrossOrigin' | 'partial' | false

View File

@@ -49,10 +49,10 @@ context('network stubbing', () => {
request: new Request(),
getRenderedHTMLOrigins: () => ({}),
serverBus: new EventEmitter(),
requestedWithAndCredentialManager: {
resourceTypeAndCredentialManager: {
get () {
return {
requestedWith: 'xhr',
resourceType: 'xhr',
credentialStatus: 'same-origin',
}
},

View File

@@ -15,9 +15,10 @@ describe('http/request-middleware', () => {
'LogRequest',
'ExtractCypressMetadataHeaders',
'MaybeSimulateSecHeaders',
'CorrelateBrowserPreRequest',
'CalculateCredentialLevelIfApplicable',
'MaybeAttachCrossOriginCookies',
'MaybeEndRequestWithBufferedResponse',
'CorrelateBrowserPreRequest',
'SetMatchingRoutes',
'SendToDriver',
'InterceptRequest',
@@ -77,58 +78,16 @@ describe('http/request-middleware', () => {
expect(ctx.req.isAUTFrame).to.be.false
})
})
})
it('removes x-cypress-is-xhr-or-fetch header when it exists', async () => {
const ctx = {
getAUTUrl: sinon.stub().returns('http://localhost:8080'),
remoteStates: {
isPrimarySuperDomainOrigin: sinon.stub().returns(true),
},
req: {
headers: {
'x-cypress-is-xhr-or-fetch': 'true',
},
} as Partial<CypressIncomingRequest>,
res: {
on: (event, listener) => {},
off: (event, listener) => {},
},
}
describe('CalculateCredentialLevelIfApplicable', () => {
const { CalculateCredentialLevelIfApplicable } = RequestMiddleware
await testMiddleware([ExtractCypressMetadataHeaders], ctx)
.then(() => {
expect(ctx.req.headers['x-cypress-is-xhr-or-fetch']).not.to.exist
})
})
it('removes x-cypress-is-xhr-or-fetch header when it does not exist', async () => {
const ctx = {
getAUTUrl: sinon.stub().returns('http://localhost:8080'),
remoteStates: {
isPrimarySuperDomainOrigin: sinon.stub().returns(false),
},
req: {
headers: {},
} as Partial<CypressIncomingRequest>,
res: {
on: (event, listener) => {},
off: (event, listener) => {},
},
}
await testMiddleware([ExtractCypressMetadataHeaders], ctx)
.then(() => {
expect(ctx.req.headers['x-cypress-is-xhr-or-fetch']).not.to.exist
})
})
it('does not set requestedWith or credentialLevel on the request if top does NOT need to be simulated', async () => {
it('does not set credentialLevel on the request if top does NOT need to be simulated', async () => {
const ctx = {
getAUTUrl: sinon.stub().returns(undefined),
req: {
headers: {
'x-cypress-is-xhr-or-fetch': 'true',
},
resourceType: 'xhr',
} as Partial<CypressIncomingRequest>,
res: {
on: (event, listener) => {},
@@ -136,23 +95,20 @@ describe('http/request-middleware', () => {
},
}
await testMiddleware([ExtractCypressMetadataHeaders], ctx)
await testMiddleware([CalculateCredentialLevelIfApplicable], ctx)
.then(() => {
expect(ctx.req.requestedWith).not.to.exist
expect(ctx.req.credentialsLevel).not.to.exist
})
})
it('does not set requestedWith or credentialLevel on the request if x-cypress-is-xhr-or-fetch has invalid values', async () => {
it('does not set credentialLevel on the request if resourceType has invalid value', async () => {
const ctx = {
getAUTUrl: sinon.stub().returns('http://localhost:8080'),
remoteStates: {
isPrimarySuperDomainOrigin: sinon.stub().returns(false),
},
req: {
headers: {
'x-cypress-is-xhr-or-fetch': 'sub_frame',
},
resourceType: 'document',
} as Partial<CypressIncomingRequest>,
res: {
on: (event, listener) => {},
@@ -160,28 +116,25 @@ describe('http/request-middleware', () => {
},
}
await testMiddleware([ExtractCypressMetadataHeaders], ctx)
await testMiddleware([CalculateCredentialLevelIfApplicable], ctx)
.then(() => {
expect(ctx.req.requestedWith).not.to.exist
expect(ctx.req.credentialsLevel).not.to.exist
})
})
// CDP can determine whether or not the request is xhr | fetch, but the extension or electron cannot
it('provides requestedWithAndCredentialManager with requestedWith if able to determine from header (xhr)', async () => {
it('provides resourceTypeAndCredentialManager with resourceType if able to determine from prerequest (xhr)', async () => {
const ctx = {
getAUTUrl: sinon.stub().returns('http://localhost:8080'),
remoteStates: {
isPrimarySuperDomainOrigin: sinon.stub().returns(false),
},
requestedWithAndCredentialManager: {
resourceTypeAndCredentialManager: {
get: sinon.stub().returns({}),
},
req: {
resourceType: 'xhr',
proxiedUrl: 'http://localhost:8080',
headers: {
'x-cypress-is-xhr-or-fetch': 'xhr',
},
} as Partial<CypressIncomingRequest>,
res: {
on: (event, listener) => {},
@@ -189,27 +142,25 @@ describe('http/request-middleware', () => {
},
}
await testMiddleware([ExtractCypressMetadataHeaders], ctx)
await testMiddleware([CalculateCredentialLevelIfApplicable], ctx)
.then(() => {
expect(ctx.requestedWithAndCredentialManager.get).to.have.been.calledWith('http://localhost:8080', `xhr`)
expect(ctx.resourceTypeAndCredentialManager.get).to.have.been.calledWith('http://localhost:8080', `xhr`)
})
})
// CDP can determine whether or not the request is xhr | fetch, but the extension or electron cannot
it('provides requestedWithAndCredentialManager with requestedWith if able to determine from header (fetch)', async () => {
it('provides resourceTypeAndCredentialManager with resourceType if able to determine from prerequest (fetch)', async () => {
const ctx = {
getAUTUrl: sinon.stub().returns('http://localhost:8080'),
remoteStates: {
isPrimarySuperDomainOrigin: sinon.stub().returns(false),
},
requestedWithAndCredentialManager: {
resourceTypeAndCredentialManager: {
get: sinon.stub().returns({}),
},
req: {
resourceType: 'fetch',
proxiedUrl: 'http://localhost:8080',
headers: {
'x-cypress-is-xhr-or-fetch': 'fetch',
},
} as Partial<CypressIncomingRequest>,
res: {
on: (event, listener) => {},
@@ -217,29 +168,27 @@ describe('http/request-middleware', () => {
},
}
await testMiddleware([ExtractCypressMetadataHeaders], ctx)
await testMiddleware([CalculateCredentialLevelIfApplicable], ctx)
.then(() => {
expect(ctx.requestedWithAndCredentialManager.get).to.have.been.calledWith('http://localhost:8080', `fetch`)
expect(ctx.resourceTypeAndCredentialManager.get).to.have.been.calledWith('http://localhost:8080', `fetch`)
})
})
it('sets the requestedWith and credentialsLevel on the request from whatever is returned by requestedWithAndCredentialManager if conditions apply', async () => {
it('sets the resourceType and credentialsLevel on the request from whatever is returned by resourceTypeAndCredentialManager if conditions apply, assuming resourceType does NOT exist on the request', async () => {
const ctx = {
getAUTUrl: sinon.stub().returns('http://localhost:8080'),
remoteStates: {
isPrimarySuperDomainOrigin: sinon.stub().returns(false),
},
requestedWithAndCredentialManager: {
resourceTypeAndCredentialManager: {
get: sinon.stub().returns({
requestedWith: 'fetch',
resourceType: 'fetch',
credentialStatus: 'same-origin',
}),
},
req: {
resourceType: undefined,
proxiedUrl: 'http://localhost:8080',
headers: {
'x-cypress-is-xhr-or-fetch': 'true',
},
} as Partial<CypressIncomingRequest>,
res: {
on: (event, listener) => {},
@@ -247,9 +196,9 @@ describe('http/request-middleware', () => {
},
}
await testMiddleware([ExtractCypressMetadataHeaders], ctx)
await testMiddleware([CalculateCredentialLevelIfApplicable], ctx)
.then(() => {
expect(ctx.req.requestedWith).to.equal('fetch')
expect(ctx.req.resourceType).to.equal('fetch')
expect(ctx.req.credentialsLevel).to.equal('same-origin')
})
})
@@ -373,7 +322,7 @@ describe('http/request-middleware', () => {
it('is a noop if cookies do NOT need to be attached to request', async () => {
const ctx = await getContext(['request=cookie'], ['jar=cookie'], 'http://foobar.com', 'http://app.foobar.com')
ctx.req.requestedWith = 'fetch'
ctx.req.resourceType = 'fetch'
ctx.req.credentialsLevel = 'omit'
await testMiddleware([MaybeAttachCrossOriginCookies], ctx)
@@ -384,7 +333,7 @@ describe('http/request-middleware', () => {
it(`allows setting cookies on request if resource type cannot be determined, but comes from the AUT frame (likely in the case of documents or redirects)`, async function () {
const ctx = await getContext([], ['jar=cookie'], 'http://foobar.com/index.html', 'http://app.foobar.com/index.html')
ctx.req.requestedWith = undefined
ctx.req.resourceType = undefined
ctx.req.credentialsLevel = undefined
ctx.req.isAUTFrame = true
await testMiddleware([MaybeAttachCrossOriginCookies], ctx)
@@ -395,7 +344,7 @@ describe('http/request-middleware', () => {
it(`otherwise, does not allow setting cookies if request type cannot be determined and is not from the AUT and is cross-origin`, async function () {
const ctx = await getContext([], ['jar=cookie'], 'http://foobar.com/index.html', 'http://app.foobar.com/index.html')
ctx.req.requestedWith = undefined
ctx.req.resourceType = undefined
ctx.req.credentialsLevel = undefined
ctx.req.isAUTFrame = false
await testMiddleware([MaybeAttachCrossOriginCookies], ctx)
@@ -406,7 +355,7 @@ describe('http/request-middleware', () => {
it('sets the cookie header to undefined if no cookies exist on the request, none in the jar, but cookies should be attached', async () => {
const ctx = await getContext([], [], 'http://foobar.com', 'http://app.foobar.com')
ctx.req.requestedWith = 'xhr'
ctx.req.resourceType = 'xhr'
ctx.req.credentialsLevel = true
await testMiddleware([MaybeAttachCrossOriginCookies], ctx)
@@ -417,7 +366,7 @@ describe('http/request-middleware', () => {
it('prepends cookie jar cookies to request', async () => {
const ctx = await getContext(['request=cookie'], ['jar=cookie'], 'http://foobar.com', 'http://app.foobar.com')
ctx.req.requestedWith = 'fetch'
ctx.req.resourceType = 'fetch'
ctx.req.credentialsLevel = 'include'
await testMiddleware([MaybeAttachCrossOriginCookies], ctx)

View File

@@ -1104,7 +1104,7 @@ describe('http/response-middleware', function () {
},
req: {
// a same-site request that has the ability to set first-party cookies in the browser
requestedWith: 'fetch',
resourceType: 'fetch',
credentialsLevel: credentialLevel,
proxiedUrl: 'https://www.foobar.com/test-request',
},
@@ -1159,7 +1159,7 @@ describe('http/response-middleware', function () {
},
req: {
// a same-site request that has the ability to set first-party cookies in the browser
requestedWith: 'xhr',
resourceType: 'xhr',
credentialsLevel: credentialLevel,
proxiedUrl: 'https://www.foobar.com/test-request',
},
@@ -1213,7 +1213,7 @@ describe('http/response-middleware', function () {
},
req: {
// a same-site request that has the ability to set first-party cookies in the browser
requestedWith: 'fetch',
resourceType: 'fetch',
credentialsLevel: 'omit',
proxiedUrl: 'https://www.foobar.com/test-request',
},
@@ -1269,7 +1269,7 @@ describe('http/response-middleware', function () {
},
req: {
// a same-site request that has the ability to set first-party cookies in the browser
requestedWith: 'fetch',
resourceType: 'fetch',
credentialsLevel: 'include',
proxiedUrl: 'https://app.foobar.com/test-request',
},
@@ -1322,7 +1322,7 @@ describe('http/response-middleware', function () {
},
req: {
// a same-site request that has the ability to set first-party cookies in the browser
requestedWith: 'xhr',
resourceType: 'xhr',
credentialsLevel: true,
proxiedUrl: 'https://app.foobar.com/test-request',
},
@@ -1376,7 +1376,7 @@ describe('http/response-middleware', function () {
},
req: {
// a same-site request that has the ability to set first-party cookies in the browser
requestedWith: 'fetch',
resourceType: 'fetch',
credentialsLevel: credentialLevel,
proxiedUrl: 'https://app.foobar.com/test-request',
},
@@ -1415,7 +1415,7 @@ describe('http/response-middleware', function () {
},
req: {
// a cross-site request that has the ability to set cookies in the browser
requestedWith: 'fetch',
resourceType: 'fetch',
credentialsLevel: 'include',
proxiedUrl: 'https://www.barbaz.com/test-request',
},
@@ -1466,7 +1466,7 @@ describe('http/response-middleware', function () {
},
req: {
// a cross-site request that has the ability to set cookies in the browser
requestedWith: 'fetch',
resourceType: 'fetch',
credentialsLevel: credentialLevel,
proxiedUrl: 'https://www.barbaz.com/test-request',
},
@@ -1500,7 +1500,7 @@ describe('http/response-middleware', function () {
},
req: {
// a cross-site request that has the ability to set cookies in the browser
requestedWith: 'xhr',
resourceType: 'xhr',
credentialsLevel: true,
proxiedUrl: 'https://www.barbaz.com/test-request',
},
@@ -1550,7 +1550,7 @@ describe('http/response-middleware', function () {
},
req: {
// a cross-site request that has the ability to set cookies in the browser
requestedWith: 'xhr',
resourceType: 'xhr',
credentialsLevel: false,
proxiedUrl: 'https://www.barbaz.com/test-request',
},
@@ -1584,7 +1584,7 @@ describe('http/response-middleware', function () {
},
req: {
// a cross-site request that has the ability to set cookies in the browser
requestedWith: 'xhr',
resourceType: 'xhr',
credentialsLevel: true,
proxiedUrl: 'https://www.barbaz.com/test-request',
},

View File

@@ -33,10 +33,10 @@ export const patchFetch = (window) => {
credentials = credentials || 'same-origin'
// if the option is specified, communicate it to the the server to the proxy can make the request aware if it needs to potentially apply cross origin cookies
// if the option isn't set, we can imply the default as we know the requestedWith in the proxy
// if the option isn't set, we can imply the default as we know the resourceType in the proxy
await requestSentWithCredentials({
url,
requestedWith: 'fetch',
resourceType: 'fetch',
credentialStatus: credentials,
})
} finally {

View File

@@ -57,10 +57,10 @@ export const postMessagePromise = <T>({ event, data = {}, timeout }: {event: str
/**
* Returns a promise from the backend request for the 'request:sent:with:credentials' event.
* @param args - an object containing a url, requestedWith and Credential status.
* @param args - an object containing a url, resourceType and Credential status.
* @returns A Promise or null depending on the url parameter.
*/
export const requestSentWithCredentials = <T>(args: {url?: string, requestedWith: 'xhr' | 'fetch', credentialStatus: string | boolean}): Promise<T> | undefined => {
export const requestSentWithCredentials = <T>(args: {url?: string, resourceType: 'xhr' | 'fetch', credentialStatus: string | boolean}): Promise<T> | undefined => {
if (args.url) {
// If cypress is enabled on the window use that, otherwise use post message to call out to the primary cypress instance.
// cypress may be found on the window if this is either the primary cypress instance or if a spec bridge has already been created for this spec bridge.

View File

@@ -20,10 +20,10 @@ export const patchXmlHttpRequest = (window: Window) => {
window.XMLHttpRequest.prototype.send = async function (...args) {
try {
// if the option is specified, communicate it to the the server to the proxy can make the request aware if it needs to potentially apply cross origin cookies
// if the option isn't set, we can imply the default as we know the "requestedWith" in the proxy
// if the option isn't set, we can imply the default as we know the "resourceType" in the proxy
await requestSentWithCredentials({
url: this._url,
requestedWith: 'xhr',
resourceType: 'xhr',
credentialStatus: this.withCredentials,
})
} finally {

View File

@@ -293,19 +293,19 @@ export class CdpAutomation {
})
}
private _continueRequest = (client, params, headers?) => {
private _continueRequest = (client, params, header?) => {
const details: Protocol.Fetch.ContinueRequestRequest = {
requestId: params.requestId,
}
if (headers && headers.length) {
if (header) {
// 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,
...headers,
header,
]
}
@@ -346,46 +346,26 @@ export class CdpAutomation {
_handlePausedRequests = async (client) => {
// NOTE: only supported in chromium based browsers
await client.send('Fetch.enable')
await client.send('Fetch.enable', {
// only enable request pausing for documents to determine the AUT iframe
patterns: [{
resourceType: 'Document',
}],
})
// 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 (await this._isAUTFrame(params.frameId)) {
debugVerbose('add X-Cypress-Is-AUT-Frame header to: %s', params.request.url)
/**
* Unlike the the web extension or Electrons's onBeforeSendHeaders, CDP can discern the difference
* between fetch or xhr resource types. Because of this, we set X-Cypress-Is-XHR-Or-Fetch to either
* 'xhr' or 'fetch' with CDP so the middleware can assume correct defaults in case credential/resourceTypes
* are not sent to the server.
* @see https://chromedevtools.github.io/devtools-protocol/tot/Network/#type-ResourceType
*/
if (params.resourceType === 'XHR' || params.resourceType === 'Fetch') {
debugVerbose('add X-Cypress-Is-XHR-Or-Fetch header to: %s', params.request.url)
addedHeaders.push({
name: 'X-Cypress-Is-XHR-Or-Fetch',
value: params.resourceType.toLowerCase(),
return this._continueRequest(client, params, {
name: 'X-Cypress-Is-AUT-Frame',
value: 'true',
})
}
if (
// is a script, stylesheet, image, etc
params.resourceType !== 'Document'
|| !(await this._isAUTFrame(params.frameId))
) {
return this._continueRequest(client, params, addedHeaders)
}
debugVerbose('add X-Cypress-Is-AUT-Frame header to: %s', params.request.url)
addedHeaders.push({
name: 'X-Cypress-Is-AUT-Frame',
value: 'true',
})
return this._continueRequest(client, params, addedHeaders)
return this._continueRequest(client, params)
})
}

View File

@@ -33,7 +33,7 @@ import type { FoundSpec } from '@packages/types'
import type { Server as WebSocketServer } from 'ws'
import { RemoteStates } from './remote_states'
import { cookieJar, SerializableAutomationCookie } from './util/cookies'
import { requestedWithAndCredentialManager, RequestedWithAndCredentialManager } from './util/requestedWithAndCredentialManager'
import { resourceTypeAndCredentialManager, ResourceTypeAndCredentialManager } from './util/resourceTypeAndCredentialManager'
const debug = Debug('cypress:server:server-base')
@@ -116,7 +116,7 @@ export abstract class ServerBase<TSocket extends SocketE2E | SocketCt> {
protected request: Request
protected isListening: boolean
protected socketAllowed: SocketAllowed
protected requestedWithAndCredentialManager: RequestedWithAndCredentialManager
protected resourceTypeAndCredentialManager: ResourceTypeAndCredentialManager
protected _fileServer: FileServer | null
protected _baseUrl: string | null
protected _server?: DestroyableHttpServer
@@ -147,7 +147,7 @@ export abstract class ServerBase<TSocket extends SocketE2E | SocketCt> {
}
})
this.requestedWithAndCredentialManager = requestedWithAndCredentialManager
this.resourceTypeAndCredentialManager = resourceTypeAndCredentialManager
}
ensureProp = ensureProp
@@ -189,7 +189,7 @@ export abstract class ServerBase<TSocket extends SocketE2E | SocketCt> {
this.socket.toDriver('cross:origin:cookies', cookies)
})
this.socket.localBus.on('request:sent:with:credentials', this.requestedWithAndCredentialManager.set)
this.socket.localBus.on('request:sent:with:credentials', this.resourceTypeAndCredentialManager.set)
}
abstract createServer (
@@ -229,7 +229,7 @@ export abstract class ServerBase<TSocket extends SocketE2E | SocketCt> {
this.createNetworkProxy({
config,
remoteStates: this._remoteStates,
requestedWithAndCredentialManager: this.requestedWithAndCredentialManager,
resourceTypeAndCredentialManager: this.resourceTypeAndCredentialManager,
shouldCorrelatePreRequests,
})
@@ -323,7 +323,7 @@ export abstract class ServerBase<TSocket extends SocketE2E | SocketCt> {
return e
}
createNetworkProxy ({ config, remoteStates, requestedWithAndCredentialManager, shouldCorrelatePreRequests }) {
createNetworkProxy ({ config, remoteStates, resourceTypeAndCredentialManager, shouldCorrelatePreRequests }) {
const getFileServerToken = () => {
return this._fileServer?.token
}
@@ -340,7 +340,7 @@ export abstract class ServerBase<TSocket extends SocketE2E | SocketCt> {
netStubbingState: this.netStubbingState,
request: this.request,
serverBus: this._eventBus,
requestedWithAndCredentialManager,
resourceTypeAndCredentialManager,
})
}
@@ -354,7 +354,7 @@ export abstract class ServerBase<TSocket extends SocketE2E | SocketCt> {
this.networkProxy.reset()
this.netStubbingState.reset()
this._remoteStates.reset()
this.requestedWithAndCredentialManager.clear()
this.resourceTypeAndCredentialManager.clear()
}
const io = this.socket.startListening(this.server, automation, config, options)
@@ -489,7 +489,7 @@ export abstract class ServerBase<TSocket extends SocketE2E | SocketCt> {
reset () {
this._networkProxy?.reset()
this.requestedWithAndCredentialManager.clear()
this.resourceTypeAndCredentialManager.clear()
const baseUrl = this._baseUrl ?? '<root>'
return this._remoteStates.set(baseUrl)

View File

@@ -1,9 +1,10 @@
import md5 from 'md5'
import Debug from 'debug'
import type { RequestCredentialLevel, RequestedWithHeader } from '@packages/proxy'
import type { RequestCredentialLevel } from '@packages/proxy'
import type { ResourceType } from '@packages/net-stubbing'
type AppliedCredentialByUrlAndResourceMap = Map<string, Array<{
requestedWith: RequestedWithHeader
resourceType: ResourceType
credentialStatus: RequestCredentialLevel
}>>
@@ -16,16 +17,16 @@ const hashUrl = (url: string): string => {
// leverage a singleton Map throughout the server to prevent clashes with this context bindings
const _appliedCredentialByUrlAndResourceMap: AppliedCredentialByUrlAndResourceMap = new Map()
class RequestedWithAndCredentialManagerClass {
get (url: string, optionalRequestedWith?: RequestedWithHeader): {
requestedWith: RequestedWithHeader
class ResourceTypeAndCredentialManagerClass {
get (url: string, optionalResourceType?: ResourceType): {
resourceType: ResourceType
credentialStatus: RequestCredentialLevel
} {
const hashKey = hashUrl(url)
debug(`credentials request received for request url ${url}, hashKey ${hashKey}`)
let value: {
requestedWith: RequestedWithHeader
resourceType: ResourceType
credentialStatus: RequestCredentialLevel
} | undefined
@@ -37,12 +38,12 @@ class RequestedWithAndCredentialManagerClass {
debug(`credential value found ${value}`)
}
// if value is undefined for any reason, apply defaults and assume xhr if no optionalRequestedWith
// optionalRequestedWith should be provided with CDP based browsers, so at least we have a fallback that is more accurate
// if value is undefined for any reason, apply defaults and assume xhr if no optionalResourceType
// optionalResourceType should be provided by the prerequest resourceType, so at least we have a fallback that is more accurate
if (value === undefined) {
value = {
requestedWith: optionalRequestedWith || 'xhr',
credentialStatus: optionalRequestedWith === 'fetch' ? 'same-origin' : false,
resourceType: optionalResourceType || 'xhr',
credentialStatus: optionalResourceType === 'fetch' ? 'same-origin' : false,
}
}
@@ -50,27 +51,27 @@ class RequestedWithAndCredentialManagerClass {
}
set ({ url,
requestedWith,
resourceType,
credentialStatus,
}: {
url: string
requestedWith: RequestedWithHeader
resourceType: ResourceType
credentialStatus: RequestCredentialLevel
}) {
const hashKey = hashUrl(url)
debug(`credentials request stored for request url ${url}, requestedWith ${requestedWith}, hashKey ${hashKey}`)
debug(`credentials request stored for request url ${url}, resourceType ${resourceType}, hashKey ${hashKey}`)
let urlHashValue = _appliedCredentialByUrlAndResourceMap.get(hashKey)
if (!urlHashValue) {
_appliedCredentialByUrlAndResourceMap.set(hashKey, [{
requestedWith,
resourceType,
credentialStatus,
}])
} else {
urlHashValue.push({
requestedWith,
resourceType,
credentialStatus,
})
}
@@ -82,7 +83,7 @@ class RequestedWithAndCredentialManagerClass {
}
// export as a singleton
export const requestedWithAndCredentialManager = new RequestedWithAndCredentialManagerClass()
export const resourceTypeAndCredentialManager = new ResourceTypeAndCredentialManagerClass()
// export but only as a type. We do NOT want others to create instances of the class as it is intended to be a singleton
export type RequestedWithAndCredentialManager = RequestedWithAndCredentialManagerClass
export type ResourceTypeAndCredentialManager = ResourceTypeAndCredentialManagerClass

View File

@@ -399,10 +399,14 @@ describe('lib/browsers/chrome', () => {
this.pageCriClient.send.withArgs('Page.getFrameTree').resolves(frameTree)
})
it('sends Fetch.enable', async function () {
it('sends Fetch.enable only for Document ResourceType', async function () {
await chrome.open('chrome', 'http://', openOpts, this.automation)
expect(this.pageCriClient.send).to.have.been.calledWith('Fetch.enable')
expect(this.pageCriClient.send).to.have.been.calledWith('Fetch.enable', {
patterns: [{
resourceType: 'Document',
}],
})
})
it('does not add header when not a document', async function () {
@@ -413,9 +417,7 @@ describe('lib/browsers/chrome', () => {
resourceType: 'Script',
})
expect(this.pageCriClient.send).to.be.calledWith('Fetch.continueRequest', {
requestId: '1234',
})
expect(this.pageCriClient.send).not.to.be.calledWith('Fetch.continueRequest')
})
it('does not add header when it is a spec frame request', async function () {
@@ -469,70 +471,6 @@ describe('lib/browsers/chrome', () => {
})
})
it('appends X-Cypress-Is-XHR-Or-Fetch header to fetch request', async function () {
await chrome.open('chrome', 'http://', openOpts, 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-Is-XHR-Or-Fetch',
value: 'fetch',
},
],
})
})
it('appends X-Cypress-Is-XHR-Or-Fetch header to xhr request', async function () {
await chrome.open('chrome', 'http://', openOpts, 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-Is-XHR-Or-Fetch',
value: 'xhr',
},
],
})
})
it('gets frame tree on Page.frameAttached', async function () {
await chrome.open('chrome', 'http://', openOpts, this.automation)

View File

@@ -370,10 +370,14 @@ describe('lib/browsers/electron', () => {
this.pageCriClient.send.withArgs('Page.getFrameTree').resolves(frameTree)
})
it('sends Fetch.enable', async function () {
it('sends Fetch.enable only for Document ResourceType', async function () {
await electron._launch(this.win, this.url, this.automation, this.options)
expect(this.pageCriClient.send).to.have.been.calledWith('Fetch.enable')
expect(this.pageCriClient.send).to.have.been.calledWith('Fetch.enable', {
patterns: [{
resourceType: 'Document',
}],
})
})
it('does not add header when not a document', async function () {
@@ -384,9 +388,7 @@ describe('lib/browsers/electron', () => {
resourceType: 'Script',
})
expect(this.pageCriClient.send).to.be.calledWith('Fetch.continueRequest', {
requestId: '1234',
})
expect(this.pageCriClient.send).not.to.be.calledWith('Fetch.continueRequest')
})
it('does not add header when it is a spec frame request', async function () {
@@ -440,70 +442,6 @@ describe('lib/browsers/electron', () => {
})
})
it('appends X-Cypress-Is-XHR-Or-Fetch header to fetch request', async function () {
await electron._launch(this.win, this.url, this.automation, this.options)
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-Is-XHR-Or-Fetch',
value: 'fetch',
},
],
})
})
it('appends X-Cypress-Is-XHR-Or-Fetch header to xhr request', async function () {
await electron._launch(this.win, this.url, this.automation, this.options)
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-Is-XHR-Or-Fetch',
value: 'xhr',
},
],
})
})
it('gets frame tree on Page.frameAttached', async function () {
await electron._launch(this.win, this.url, this.automation, this.options)

View File

@@ -1,86 +0,0 @@
import { expect } from 'chai'
import { requestedWithAndCredentialManager } from '../../../lib/util/requestedWithAndCredentialManager'
context('requestedWithAndCredentialManager Singleton', () => {
beforeEach(() => {
requestedWithAndCredentialManager.clear()
requestedWithAndCredentialManager.set({
url: 'www.foobar.com/test-request',
requestedWith: 'xhr',
credentialStatus: true,
})
requestedWithAndCredentialManager.set({
url: 'www.foobar.com%2Ftest-request-2',
requestedWith: 'fetch',
credentialStatus: 'same-origin',
})
requestedWithAndCredentialManager.set({
url: 'www.foobar.com/test-request-2',
requestedWith: 'fetch',
credentialStatus: 'include',
})
requestedWithAndCredentialManager.set({
url: 'www.foobar.com/test-request',
requestedWith: 'fetch',
credentialStatus: 'omit',
})
requestedWithAndCredentialManager.set({
url: 'www.foobar.com/test-request',
requestedWith: 'fetch',
credentialStatus: 'include',
})
})
it('gets the first record out of the queue matching the absolute url and removes it', () => {
expect(requestedWithAndCredentialManager.get('www.foobar.com/test-request')).to.deep.equal({
requestedWith: 'xhr',
credentialStatus: true,
})
expect(requestedWithAndCredentialManager.get('www.foobar.com/test-request')).to.deep.equal({
requestedWith: 'fetch',
credentialStatus: 'omit',
})
expect(requestedWithAndCredentialManager.get('www.foobar.com/test-request')).to.deep.equal({
requestedWith: 'fetch',
credentialStatus: 'include',
})
// the default as no other records should exist in the map for this URL
expect(requestedWithAndCredentialManager.get('www.foobar.com/test-request')).to.deep.equal({
requestedWith: 'xhr',
credentialStatus: false,
})
})
it('can locate a record hash even when the URL is encoded', () => {
expect(requestedWithAndCredentialManager.get('www.foobar.com%2Ftest-request')).to.deep.equal({
requestedWith: 'xhr',
credentialStatus: true,
})
})
it('applies defaults if a record cannot be found without a requestedWith', () => {
expect(requestedWithAndCredentialManager.get('www.barbaz.com/test-request')).to.deep.equal({
requestedWith: 'xhr',
credentialStatus: false,
})
})
it('applies defaults if a record cannot be found with a requestedWith', () => {
expect(requestedWithAndCredentialManager.get('www.barbaz.com/test-request', 'xhr')).to.deep.equal({
requestedWith: 'xhr',
credentialStatus: false,
})
expect(requestedWithAndCredentialManager.get('www.barbaz.com/test-request', 'fetch')).to.deep.equal({
requestedWith: 'fetch',
credentialStatus: 'same-origin',
})
})
})

View File

@@ -0,0 +1,86 @@
import { expect } from 'chai'
import { resourceTypeAndCredentialManager } from '../../../lib/util/resourceTypeAndCredentialManager'
context('resourceTypeAndCredentialManager Singleton', () => {
beforeEach(() => {
resourceTypeAndCredentialManager.clear()
resourceTypeAndCredentialManager.set({
url: 'www.foobar.com/test-request',
resourceType: 'xhr',
credentialStatus: true,
})
resourceTypeAndCredentialManager.set({
url: 'www.foobar.com%2Ftest-request-2',
resourceType: 'fetch',
credentialStatus: 'same-origin',
})
resourceTypeAndCredentialManager.set({
url: 'www.foobar.com/test-request-2',
resourceType: 'fetch',
credentialStatus: 'include',
})
resourceTypeAndCredentialManager.set({
url: 'www.foobar.com/test-request',
resourceType: 'fetch',
credentialStatus: 'omit',
})
resourceTypeAndCredentialManager.set({
url: 'www.foobar.com/test-request',
resourceType: 'fetch',
credentialStatus: 'include',
})
})
it('gets the first record out of the queue matching the absolute url and removes it', () => {
expect(resourceTypeAndCredentialManager.get('www.foobar.com/test-request')).to.deep.equal({
resourceType: 'xhr',
credentialStatus: true,
})
expect(resourceTypeAndCredentialManager.get('www.foobar.com/test-request')).to.deep.equal({
resourceType: 'fetch',
credentialStatus: 'omit',
})
expect(resourceTypeAndCredentialManager.get('www.foobar.com/test-request')).to.deep.equal({
resourceType: 'fetch',
credentialStatus: 'include',
})
// the default as no other records should exist in the map for this URL
expect(resourceTypeAndCredentialManager.get('www.foobar.com/test-request')).to.deep.equal({
resourceType: 'xhr',
credentialStatus: false,
})
})
it('can locate a record hash even when the URL is encoded', () => {
expect(resourceTypeAndCredentialManager.get('www.foobar.com%2Ftest-request')).to.deep.equal({
resourceType: 'xhr',
credentialStatus: true,
})
})
it('applies defaults if a record cannot be found without a resourceType', () => {
expect(resourceTypeAndCredentialManager.get('www.barbaz.com/test-request')).to.deep.equal({
resourceType: 'xhr',
credentialStatus: false,
})
})
it('applies defaults if a record cannot be found with a resourceType', () => {
expect(resourceTypeAndCredentialManager.get('www.barbaz.com/test-request', 'xhr')).to.deep.equal({
resourceType: 'xhr',
credentialStatus: false,
})
expect(resourceTypeAndCredentialManager.get('www.barbaz.com/test-request', 'fetch')).to.deep.equal({
resourceType: 'fetch',
credentialStatus: 'same-origin',
})
})
})

View File

@@ -4435,7 +4435,7 @@
"./packages/server/lib/util/print-run.ts",
"./packages/server/lib/util/proxy.ts",
"./packages/server/lib/util/random.js",
"./packages/server/lib/util/requestedWithAndCredentialManager.ts",
"./packages/server/lib/util/resourceTypeAndCredentialManager.ts",
"./packages/server/lib/util/server_destroy.ts",
"./packages/server/lib/util/shell.js",
"./packages/server/lib/util/socket_allowed.ts",

View File

@@ -4434,7 +4434,7 @@
"./packages/server/lib/util/print-run.ts",
"./packages/server/lib/util/proxy.ts",
"./packages/server/lib/util/random.js",
"./packages/server/lib/util/requestedWithAndCredentialManager.ts",
"./packages/server/lib/util/resourceTypeAndCredentialManager.ts",
"./packages/server/lib/util/server_destroy.ts",
"./packages/server/lib/util/shell.js",
"./packages/server/lib/util/socket_allowed.ts",

View File

@@ -4434,7 +4434,7 @@
"./packages/server/lib/util/print-run.ts",
"./packages/server/lib/util/proxy.ts",
"./packages/server/lib/util/random.js",
"./packages/server/lib/util/requestedWithAndCredentialManager.ts",
"./packages/server/lib/util/resourceTypeAndCredentialManager.ts",
"./packages/server/lib/util/server_destroy.ts",
"./packages/server/lib/util/shell.js",
"./packages/server/lib/util/socket_allowed.ts",