diff --git a/packages/driver/cypress/e2e/e2e/origin/patches.cy.ts b/packages/driver/cypress/e2e/e2e/origin/patches.cy.ts index 5111fcfdae..d87511a5fe 100644 --- a/packages/driver/cypress/e2e/e2e/origin/patches.cy.ts +++ b/packages/driver/cypress/e2e/e2e/origin/patches.cy.ts @@ -1,10 +1,10 @@ describe('src/cross-origin/patches', () => { - beforeEach(() => { - cy.visit('/fixtures/primary-origin.html') - cy.get('a[data-cy="cross-origin-secondary-link"]').click() - }) - context('submit', () => { + beforeEach(() => { + cy.visit('/fixtures/primary-origin.html') + cy.get('a[data-cy="cross-origin-secondary-link"]').click() + }) + it('correctly submits a form when the target is _top for HTMLFormElement', () => { cy.origin('http://www.foobar.com:3500', () => { cy.get('form').then(($form) => { @@ -18,6 +18,11 @@ describe('src/cross-origin/patches', () => { }) context('setAttribute', () => { + beforeEach(() => { + cy.visit('/fixtures/primary-origin.html') + cy.get('a[data-cy="cross-origin-secondary-link"]').click() + }) + it('renames integrity to cypress-stripped-integrity for HTMLScriptElement', () => { cy.origin('http://www.foobar.com:3500', () => { cy.window().then((win: Window) => { @@ -52,4 +57,533 @@ describe('src/cross-origin/patches', () => { }) }) }) + + context('fetch', () => { + describe('from the AUT', () => { + beforeEach(() => { + cy.intercept('/test-request').as('testRequest') + cy.origin('http://www.foobar.com:3500', () => { + cy.stub(Cypress, 'backend').callThrough() + }) + + cy.visit('/fixtures/primary-origin.html') + cy.get('a[data-cy="xhr-fetch-requests"]').click() + }) + + describe('patches fetch in the AUT when going cross origin and sends credential status to server socket', () => { + [undefined, 'same-origin', 'omit', 'include'].forEach((credentialOption) => { + describe(`for credential option ${credentialOption || 'default'}`, () => { + const postfixedSelector = !credentialOption || credentialOption === 'same-origin' ? '' : `-${credentialOption}` + const assertCredentialStatus = credentialOption || 'same-origin' + + it('with a url string', () => { + cy.origin('http://www.foobar.com:3500', { + args: { + postfixedSelector, + assertCredentialStatus, + }, + }, + ({ postfixedSelector, assertCredentialStatus }) => { + cy.get(`[data-cy="trigger-fetch${postfixedSelector}"]`).click() + cy.wait('@testRequest') + cy.then(() => { + expect(Cypress.backend).to.have.been.calledWith('request:sent:with:credentials', { + url: 'http://www.foobar.com:3500/test-request', + resourceType: 'fetch', + credentialStatus: assertCredentialStatus, + }) + }) + }) + }) + + it('with a request object', () => { + cy.origin('http://www.foobar.com:3500', { + args: { + postfixedSelector, + assertCredentialStatus, + }, + }, + ({ postfixedSelector, assertCredentialStatus }) => { + cy.get(`[data-cy="trigger-fetch-with-request-object${postfixedSelector}"]`).click() + cy.wait('@testRequest') + cy.then(() => { + expect(Cypress.backend).to.have.been.calledWith('request:sent:with:credentials', { + url: 'http://www.foobar.com:3500/test-request', + resourceType: 'fetch', + credentialStatus: assertCredentialStatus, + }) + }) + }) + }) + + it('with a url object', () => { + cy.origin('http://www.foobar.com:3500', { + args: { + postfixedSelector, + assertCredentialStatus, + }, + }, + ({ postfixedSelector, assertCredentialStatus }) => { + cy.get(`[data-cy="trigger-fetch-with-url-object${postfixedSelector}"]`).click() + cy.wait('@testRequest') + cy.then(() => { + expect(Cypress.backend).to.have.been.calledWith('request:sent:with:credentials', { + url: 'http://www.foobar.com:3500/test-request', + resourceType: 'fetch', + credentialStatus: assertCredentialStatus, + }) + }) + }) + }) + }) + }) + + it('fails gracefully if fetch is called with Bad arguments and we don\'t single to the socket (must match the fetch api spec), but fetch request still proceeds', () => { + cy.origin('http://www.foobar.com:3500', + () => { + cy.on('uncaught:exception', (err) => { + expect(err.message).to.contain('404') + expect(Cypress.backend).not.to.have.been.calledWithMatch('request:sent:with:credentials') + + return false + }) + + cy.get(`[data-cy="trigger-fetch-with-bad-options"]`).click() + }) + }) + + it('works as expected with requests that require preflight that ultimately fail and the request does not succeed', () => { + cy.origin('http://www.foobar.com:3500', + () => { + cy.on('uncaught:exception', (err) => { + expect(err.message).to.contain('CORS ERROR') + expect(Cypress.backend).to.have.been.calledWith('request:sent:with:credentials', { + url: 'http://app.foobar.com:3500/test-request', + resourceType: 'fetch', + credentialStatus: 'include', + }) + + return false + }) + + cy.get(`[data-cy="trigger-fetch-with-preflight"]`).click() + }) + }) + }) + }) + + describe('from the spec bridge', () => { + beforeEach(() => { + cy.intercept('/test-request').as('testRequest') + cy.stub(Cypress, 'backend').callThrough() + cy.origin('http://www.foobar.com:3500', () => { + cy.stub(Cypress, 'backend').callThrough() + }) + + cy.visit('/fixtures/primary-origin.html') + cy.get('a[data-cy="xhr-fetch-requests"]').click() + }) + + describe('patches fetch in the AUT when going cross origin and sends credential status to server socket', () => { + [undefined, 'same-origin', 'omit', 'include'].forEach((credentialOption) => { + const assertCredentialStatus = credentialOption || 'same-origin' + + // NOTE: Even if the request fails, this should be popped off the queue in the proxy and not be an issue moving forward. + // only thing we should be concerned with is urls that go over the socket but somehow do NOT make a request. + // This MIGHT be an issue for preflight requests failing if the browser fails the request and the request doesn't actually make it through the proxy + describe(`for credential option ${credentialOption || 'default'}`, () => { + it('with a url string', () => { + cy.origin('http://www.foobar.com:3500', { + args: { + credentialOption, + assertCredentialStatus, + }, + }, ({ credentialOption, assertCredentialStatus }) => { + cy.then(() => { + if (credentialOption) { + return fetch('http://www.foobar.com:3500/test-request-credentials', { + credentials: credentialOption as RequestCredentials, + }) + } + + return fetch('http://www.foobar.com:3500/test-request-credentials') + }) + + cy.then(() => { + expect(Cypress.backend).to.have.been.calledWith('request:sent:with:credentials', { + url: 'http://www.foobar.com:3500/test-request-credentials', + resourceType: 'fetch', + credentialStatus: assertCredentialStatus, + }) + }) + }) + }) + + it('with a request object', () => { + cy.origin('http://www.foobar.com:3500', { + args: { + credentialOption, + assertCredentialStatus, + }, + }, ({ credentialOption, assertCredentialStatus }) => { + cy.then(() => { + let req + + if (credentialOption) { + req = new Request('http://www.foobar.com:3500/test-request-credentials', { + credentials: credentialOption as RequestCredentials, + }) + } else { + req = new Request('http://www.foobar.com:3500/test-request-credentials') + } + + return fetch(req) + }) + + cy.then(() => { + expect(Cypress.backend).to.have.been.calledWith('request:sent:with:credentials', { + url: 'http://www.foobar.com:3500/test-request-credentials', + resourceType: 'fetch', + credentialStatus: assertCredentialStatus, + }) + }) + }) + }) + + it('with a url object', () => { + cy.origin('http://www.foobar.com:3500', { + args: { + credentialOption, + assertCredentialStatus, + }, + }, ({ credentialOption, assertCredentialStatus }) => { + cy.then(() => { + let urlObj = new URL('/test-request-credentials', 'http://www.foobar.com:3500') + + if (credentialOption) { + return fetch(urlObj, { + credentials: credentialOption as RequestCredentials, + }) + } + + return fetch(urlObj) + }) + + cy.then(() => { + expect(Cypress.backend).to.have.been.calledWith('request:sent:with:credentials', { + url: 'http://www.foobar.com:3500/test-request-credentials', + resourceType: 'fetch', + credentialStatus: assertCredentialStatus, + }) + }) + }) + }) + }) + }) + + it('fails gracefully if fetch is called with Bad arguments and we don\'t single to the socket (must match the fetch api spec), but fetch request still proceeds', () => { + cy.origin('http://www.foobar.com:3500', + () => { + cy.on('uncaught:exception', (err) => { + expect(err.message).to.contain('404') + expect(Cypress.backend).not.to.have.been.calledWithMatch('request:sent:with:credentials') + + return false + }) + + cy.get(`[data-cy="trigger-fetch-with-bad-options"]`).click() + }) + }) + }) + + it('works as expected with requests that require preflight that ultimately fail and the request does not succeed', () => { + cy.origin('http://www.foobar.com:3500', + () => { + cy.then(() => { + let url = new URL('/test-request', 'http://app.foobar.com:3500').toString() + + return new Promise((resolve, reject) => { + fetch(url, { + credentials: 'include', + headers: { + 'foo': 'bar', + }, + }).catch(() => { + expect(Cypress.backend).to.have.been.calledWith('request:sent:with:credentials', { + url: 'http://app.foobar.com:3500/test-request', + resourceType: 'fetch', + credentialStatus: 'include', + }) + + resolve() + }).then(() => { + // if this fetch does not fail, fail the test + reject() + }) + }) + }) + }) + }) + }) + + it('does not patch fetch in the spec window or the AUT if the AUT is on the primary', () => { + cy.stub(Cypress, 'backend').callThrough() + cy.visit('fixtures/xhr-fetch-requests.html') + + cy.window().then((win) => { + win.location.href = 'http://www.foobar.com:3500/fixtures/secondary-origin.html' + }) + + cy.origin('http://www.foobar.com:3500', () => { + cy.window().then((win) => { + win.location.href = 'http://localhost:3500/fixtures/xhr-fetch-requests.html' + }) + }) + + // expect spec to NOT be patched in primary + cy.then(async () => { + await fetch('/test-request') + + expect(Cypress.backend).not.to.have.been.calledWithMatch('request:sent:with:credentials') + }) + + // expect AUT to NOT be patched in primary + cy.window().then(async (win) => { + await win.fetch('/test-request') + + expect(Cypress.backend).not.to.have.been.calledWithMatch('request:sent:with:credentials') + }) + }) + }) + + context('xmlHttpRequest', () => { + describe('from the AUT', () => { + beforeEach(() => { + cy.intercept('/test-request').as('testRequest') + cy.origin('http://www.foobar.com:3500', () => { + cy.stub(Cypress, 'backend').callThrough() + }) + + cy.visit('/fixtures/primary-origin.html') + cy.get('a[data-cy="xhr-fetch-requests"]').click() + }) + + describe('patches xmlHttpRequest in the AUT when going cross origin and sends credential status to server socket', () => { + [false, true].forEach((withCredentials) => { + it(`for withCredentials option ${withCredentials}`, () => { + const postfixedSelector = withCredentials ? '-with-credentials' : '' + + cy.origin('http://www.foobar.com:3500', { + args: { + postfixedSelector, + withCredentials, + }, + }, + ({ postfixedSelector, withCredentials = false }) => { + cy.get(`[data-cy="trigger-xml-http-request${postfixedSelector}"]`).click() + cy.wait('@testRequest') + cy.then(() => { + expect(Cypress.backend).to.have.been.calledWith('request:sent:with:credentials', { + url: 'http://www.foobar.com:3500/test-request', + resourceType: 'xhr', + credentialStatus: withCredentials, + }) + }) + }) + }) + }) + + it('fails gracefully if xhr is called with Bad arguments and we don\'t signal to the socket (must match the legit URL), but xhr request still proceeds', () => { + cy.origin('http://www.foobar.com:3500', + () => { + cy.on('uncaught:exception', (err) => { + expect(err.message).to.contain('404') + expect(Cypress.backend).not.to.have.been.calledWithMatch('request:sent:with:credentials') + + return false + }) + + cy.get(`[data-cy="trigger-xml-http-request-with-bad-options"]`).click() + }) + }) + }) + + it('works as expected with requests that require preflight that ultimately fail and the request does not succeed', () => { + cy.origin('http://www.foobar.com:3500', + () => { + cy.on('uncaught:exception', (err) => { + expect(err.message).to.contain('CORS ERROR') + expect(Cypress.backend).to.have.been.calledWith('request:sent:with:credentials', { + url: 'http://app.foobar.com:3500/test-request', + resourceType: 'xhr', + credentialStatus: true, + }) + + return false + }) + + cy.get(`[data-cy="trigger-xml-http-request-with-preflight"]`).click() + }) + }) + }) + + describe('from the spec bridge', () => { + beforeEach(() => { + cy.intercept('/test-request').as('testRequest') + cy.stub(Cypress, 'backend').callThrough() + cy.origin('http://www.foobar.com:3500', () => { + cy.stub(Cypress, 'backend').callThrough() + }) + + cy.visit('/fixtures/primary-origin.html') + cy.get('a[data-cy="xhr-fetch-requests"]').click() + }) + + describe('patches xmlHttpRequest in the spec bridge', () => { + [false, true].forEach((withCredentials) => { + it(`for withCredentials option ${withCredentials}`, () => { + cy.origin('http://www.foobar.com:3500', { + args: { + withCredentials, + }, + }, + ({ withCredentials = false }) => { + cy.then(() => { + let url = new URL('/test-request-credentials', 'http://www.foobar.com:3500').toString() + + return new Promise((resolve, reject) => { + let xhr = new XMLHttpRequest() + + xhr.open('GET', url) + xhr.withCredentials = withCredentials + xhr.onload = function () { + resolve(xhr.response) + } + + xhr.onerror = function () { + reject(xhr.response) + } + + xhr.send() + }) + }) + + cy.then(() => { + expect(Cypress.backend).to.have.been.calledWith('request:sent:with:credentials', { + url: 'http://www.foobar.com:3500/test-request-credentials', + resourceType: 'xhr', + credentialStatus: withCredentials, + }) + }) + }) + }) + }) + + it('fails gracefully if xmlHttpRequest is called with bad arguments and we don\'t signal to the socket (must match the legit URL), but xhr request still proceeds', () => { + cy.origin('http://www.foobar.com:3500', + () => { + cy.on('uncaught:exception', (err) => { + expect(err.message).to.contain('404') + expect(Cypress.backend).not.to.have.been.calledWithMatch('request:sent:with:credentials') + + return false + }) + + cy.get(`[data-cy="trigger-xml-http-request-with-bad-options"]`).click() + }) + }) + + it('works as expected with requests that require preflight that ultimately fail and the request does not succeed', () => { + cy.origin('http://www.foobar.com:3500', + () => { + cy.then(() => { + let url = new URL('/test-request', 'http://app.foobar.com:3500').toString() + + return new Promise((resolve, reject) => { + let xhr = new XMLHttpRequest() + + xhr.open('GET', url) + xhr.withCredentials = true + xhr.onload = function () { + // if this request passes, fail the test + reject(xhr.response) + } + + xhr.onerror = function () { + expect(Cypress.backend).to.have.been.calledWith('request:sent:with:credentials', { + url: 'http://app.foobar.com:3500/test-request', + resourceType: 'xhr', + credentialStatus: true, + }) + + resolve() + } + + xhr.send() + }) + }) + }) + }) + }) + }) + + it('does not patch xmlHttpRequest in the spec window or the AUT if the AUT is on the primary', () => { + cy.stub(Cypress, 'backend').callThrough() + cy.visit('fixtures/xhr-fetch-requests.html') + + cy.window().then((win) => { + win.location.href = 'http://www.foobar.com:3500/fixtures/secondary-origin.html' + }) + + cy.origin('http://www.foobar.com:3500', () => { + cy.window().then((win) => { + win.location.href = 'http://localhost:3500/fixtures/xhr-fetch-requests.html' + }) + }) + + // expect spec to NOT be patched in primary + cy.then(async () => { + let url = new URL('/test-request-credentials', 'http://www.foobar.com:3500').toString() + + await new Promise((resolve, reject) => { + let xhr = new XMLHttpRequest() + + xhr.open('GET', url) + xhr.onload = function () { + resolve(xhr.response) + } + + xhr.onerror = function () { + reject(xhr.response) + } + + xhr.send() + }) + + expect(Cypress.backend).not.to.have.been.calledWithMatch('request:sent:with:credentials') + }) + + // expect AUT to NOT be patched in primary + cy.window().then(async (win) => { + let url = new URL('/test-request-credentials', 'http://www.foobar.com:3500').toString() + + await new Promise((resolve, reject) => { + let xhr = new win.XMLHttpRequest() + + xhr.open('GET', url) + xhr.onload = function () { + resolve(xhr.response) + } + + xhr.onerror = function () { + reject(xhr.response) + } + + xhr.send() + }) + + expect(Cypress.backend).not.to.have.been.calledWithMatch('request:sent:with:credentials') + }) + }) + }) }) diff --git a/packages/driver/cypress/e2e/e2e/origin/snapshots.cy.ts b/packages/driver/cypress/e2e/e2e/origin/snapshots.cy.ts index 2e9b703f1b..a9dd067573 100644 --- a/packages/driver/cypress/e2e/e2e/origin/snapshots.cy.ts +++ b/packages/driver/cypress/e2e/e2e/origin/snapshots.cy.ts @@ -23,6 +23,7 @@ describe('cy.origin - snapshots', () => { }) cy.visit('/fixtures/primary-origin.html') + cy.get('a[data-cy="xhr-fetch-requests-onload"]').click() }) // TODO: the xhr event is showing up twice in the log, which is wrong and causing flake. skipping until: https://github.com/cypress-io/cypress/issues/23840 is addressed. @@ -47,7 +48,7 @@ describe('cy.origin - snapshots', () => { // TODO: Since we have two events, one of them does not have a request snapshot - expect(snapshots[1].querySelector(`[data-cy="assertion-header"]`)).to.have.property('innerText').that.equals('Making XHR and Fetch Requests behind the scenes!') + expect(snapshots[1].querySelector(`[data-cy="assertion-header"]`)).to.have.property('innerText').that.equals('Making XHR and Fetch Requests behind the scenes if fireOnload is true!') }) }) @@ -70,7 +71,7 @@ describe('cy.origin - snapshots', () => { const snapshots = xhrLogFromSecondaryOrigin.snapshots.map((snapshot) => snapshot.body.get()[0]) snapshots.forEach((snapshot) => { - expect(snapshot.querySelector(`[data-cy="assertion-header"]`)).to.have.property('innerText').that.equals('Making XHR and Fetch Requests behind the scenes!') + expect(snapshot.querySelector(`[data-cy="assertion-header"]`)).to.have.property('innerText').that.equals('Making XHR and Fetch Requests behind the scenes if fireOnload is true!') }) }) }) diff --git a/packages/driver/cypress/fixtures/primary-origin.html b/packages/driver/cypress/fixtures/primary-origin.html index a4046cde4b..7d98511e9a 100644 --- a/packages/driver/cypress/fixtures/primary-origin.html +++ b/packages/driver/cypress/fixtures/primary-origin.html @@ -13,7 +13,8 @@
  • http://www.foobar.com:3500/fixtures/files-form.html
  • http://www.foobar.com:3500/fixtures/errors.html
  • http://www.foobar.com:3500/fixtures/screenshots.html
  • -
  • http://www.foobar.com:3500/fixtures/xhr-fetch-onload.html
  • +
  • http://www.foobar.com:3500/fixtures/xhr-fetch-requests.html onLoad
  • +
  • http://www.foobar.com:3500/fixtures/xhr-fetch-requests.html
  • http://www.foobar.com:3500/fixtures/scripts-with-integrity.html
  • Login with Social
  • Login with Social (https)
  • diff --git a/packages/driver/cypress/fixtures/xhr-fetch-onload.html b/packages/driver/cypress/fixtures/xhr-fetch-onload.html deleted file mode 100644 index 65fa87ea0c..0000000000 --- a/packages/driver/cypress/fixtures/xhr-fetch-onload.html +++ /dev/null @@ -1,18 +0,0 @@ - - - -

    Making XHR and Fetch Requests behind the scenes!

    - - - \ No newline at end of file diff --git a/packages/driver/cypress/fixtures/xhr-fetch-requests.html b/packages/driver/cypress/fixtures/xhr-fetch-requests.html new file mode 100644 index 0000000000..0692419a74 --- /dev/null +++ b/packages/driver/cypress/fixtures/xhr-fetch-requests.html @@ -0,0 +1,109 @@ + + + +

    Making XHR and Fetch Requests behind the scenes if fireOnload is true!

    + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/packages/driver/src/cross-origin/cypress.ts b/packages/driver/src/cross-origin/cypress.ts index 2aa3833fb7..5823003180 100644 --- a/packages/driver/src/cross-origin/cypress.ts +++ b/packages/driver/src/cross-origin/cypress.ts @@ -21,6 +21,8 @@ import { handleTestEvents } from './events/test' import { handleMiscEvents } from './events/misc' import { handleUnsupportedAPIs } from './unsupported_apis' import { patchFormElementSubmit } from './patches/submit' +import { patchFetch } from './patches/fetch' +import { patchXmlHttpRequest } from './patches/xmlHttpRequest' import $Mocha from '../cypress/mocha' import * as cors from '@packages/network/lib/cors' @@ -173,6 +175,15 @@ const attachToWindow = (autWindow: Window) => { cy.overrides.wrapNativeMethods(autWindow) + // place after override incase fetch is polyfilled in the AUT injection + // this can be in the beforeLoad code as we only want to patch fetch/xmlHttpRequest + // when the cy.origin block is active to track credential use + patchFetch(Cypress, autWindow) + patchXmlHttpRequest(Cypress, autWindow) + // also patch it in the spec bridge as well + patchFetch(Cypress, window) + patchXmlHttpRequest(Cypress, window) + // TODO: DRY this up with the mostly-the-same code in src/cypress/cy.js // https://github.com/cypress-io/cypress/issues/20972 bindToListeners(autWindow, { diff --git a/packages/driver/src/cross-origin/patches/fetch.ts b/packages/driver/src/cross-origin/patches/fetch.ts new file mode 100644 index 0000000000..28596d35d4 --- /dev/null +++ b/packages/driver/src/cross-origin/patches/fetch.ts @@ -0,0 +1,51 @@ +import { captureFullRequestUrl } from './utils' + +export const patchFetch = (Cypress: Cypress.Cypress, window) => { + // if fetch is available in the browser, or is polyfilled by whatwg fetch + // intercept method calls and add cypress headers to determine cookie applications in the proxy + // for simulated top. @see https://github.github.io/fetch/ for default options + if (!Cypress.config('experimentalSessionAndOrigin') || !window.fetch) { + return + } + + const originalFetch = window.fetch + + window.fetch = function (...args) { + try { + let url: string | undefined = undefined + let credentials: string | undefined = undefined + + const resource = args[0] + + // @see https://developer.mozilla.org/en-US/docs/Web/API/fetch#parameters for fetch resource options. We will only support Request, URL, and strings + if (resource instanceof window.Request) { + ({ url, credentials } = resource) + } else if (resource instanceof window.URL) { + // should be a no-op for URL + url = resource.toString() + + ;({ credentials } = args[1] || {}) + } else if (Cypress._.isString(resource)) { + url = captureFullRequestUrl(resource, window) + + ;({ credentials } = args[1] || {}) + } + + 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 resource type in the proxy + if (url) { + // @ts-expect-error + Cypress.backend('request:sent:with:credentials', { + // TODO: might need to go off more information here or at least make collisions less likely + url, + resourceType: 'fetch', + credentialStatus: credentials, + }) + } + } finally { + // if our internal logic errors for whatever reason, do NOT block the end user and continue the request + return originalFetch.apply(this, args) + } + } +} diff --git a/packages/driver/src/cross-origin/patches/utils/index.ts b/packages/driver/src/cross-origin/patches/utils/index.ts new file mode 100644 index 0000000000..cb3d5aacc8 --- /dev/null +++ b/packages/driver/src/cross-origin/patches/utils/index.ts @@ -0,0 +1,17 @@ +export const captureFullRequestUrl = (relativeOrAbsoluteUrlString: string, window: Window) => { + // need to pass the window here by reference to generate the correct absolute URL if needed. Spec Bridge does NOT contain sub domain + let url + + try { + url = new URL(relativeOrAbsoluteUrlString).toString() + } catch (err1) { + try { + // likely a relative path, construct the full url + url = new URL(relativeOrAbsoluteUrlString, window.location.origin).toString() + } catch (err2) { + return undefined + } + } + + return url +} diff --git a/packages/driver/src/cross-origin/patches/xmlHttpRequest.ts b/packages/driver/src/cross-origin/patches/xmlHttpRequest.ts new file mode 100644 index 0000000000..92cb07b204 --- /dev/null +++ b/packages/driver/src/cross-origin/patches/xmlHttpRequest.ts @@ -0,0 +1,42 @@ +import { captureFullRequestUrl } from './utils' + +export const patchXmlHttpRequest = (Cypress: Cypress.Cypress, window: Window) => { + // intercept method calls and add cypress headers to determine cookie applications in the proxy + // for simulated top + + if (!Cypress.config('experimentalSessionAndOrigin')) { + return + } + + const originalXmlHttpRequestOpen = window.XMLHttpRequest.prototype.open + const originalXmlHttpRequestSend = window.XMLHttpRequest.prototype.send + + window.XMLHttpRequest.prototype.open = function (...args) { + try { + // since the send method does NOT have access to the arguments passed into open or have the request information, + // we need to store a reference here to what we need in the send method + this._url = captureFullRequestUrl(args[1], window) + } finally { + return originalXmlHttpRequestOpen.apply(this, args as any) + } + } + + window.XMLHttpRequest.prototype.send = 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 resource type in the proxy + if (this._url) { + // @ts-expect-error + Cypress.backend('request:sent:with:credentials', { + // TODO: might need to go off more information here or at least make collisions less likely + url: this._url, + resourceType: 'xhr', + credentialStatus: this.withCredentials, + }) + } + } finally { + // if our internal logic errors for whatever reason, do NOT block the end user and continue the request + return originalXmlHttpRequestSend.apply(this, args) + } + } +} diff --git a/packages/server/lib/socket-base.ts b/packages/server/lib/socket-base.ts index 388203cd2b..ae86d73e0c 100644 --- a/packages/server/lib/socket-base.ts +++ b/packages/server/lib/socket-base.ts @@ -484,6 +484,9 @@ export class SocketBase { } case 'cross:origin:automation:cookies:received': return this.localBus.emit('cross:origin:automation:cookies:received') + case 'request:sent:with:credentials': + // NOTE: this is currently a no-op until the server logic is implemented + return this.localBus.emit('request:sent:with:credentials', args[0]) default: throw new Error(`You requested a backend event we cannot handle: ${eventName}`) }