Skip to content

Commit

Permalink
feat: patch fetch and xhr inside cy.origin to get resourceType and cr…
Browse files Browse the repository at this point in the history
…edential Level (#23822)

* chore: modify xhr-fetch-requests to handle onload and prep for use in patches tests

* feat: add patches for fetch and xmlhttprequest

* chore: short circuit fetch and xmlHttpRequests if conditions aren't met

* chore: refactor xmlHttpRequest and fetch patches into individual files and add some basic types

* chore: fix typo
  • Loading branch information
AtofStryker authored Sep 19, 2022
1 parent 0c26563 commit f356065
Show file tree
Hide file tree
Showing 10 changed files with 777 additions and 26 deletions.
544 changes: 539 additions & 5 deletions packages/driver/cypress/e2e/e2e/origin/patches.cy.ts

Large diffs are not rendered by default.

5 changes: 3 additions & 2 deletions packages/driver/cypress/e2e/e2e/origin/snapshots.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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!')
})
})

Expand All @@ -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!')
})
})
})
Expand Down
3 changes: 2 additions & 1 deletion packages/driver/cypress/fixtures/primary-origin.html
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@
<li><a data-cy="files-form-link" href="http://www.foobar.com:3500/fixtures/files-form.html">http://www.foobar.com:3500/fixtures/files-form.html</a></li>
<li><a data-cy="errors-link" href="http://www.foobar.com:3500/fixtures/errors.html">http://www.foobar.com:3500/fixtures/errors.html</a></li>
<li><a data-cy="screenshots-link" href="http://www.foobar.com:3500/fixtures/screenshots.html">http://www.foobar.com:3500/fixtures/screenshots.html</a></li>
<li><a data-cy="xhr-fetch-requests" href="http://www.foobar.com:3500/fixtures/xhr-fetch-onload.html">http://www.foobar.com:3500/fixtures/xhr-fetch-onload.html</a></li>
<li><a data-cy="xhr-fetch-requests-onload" href="http://www.foobar.com:3500/fixtures/xhr-fetch-requests.html?fireOnload=true">http://www.foobar.com:3500/fixtures/xhr-fetch-requests.html onLoad</a></li>
<li><a data-cy="xhr-fetch-requests" href="http://www.foobar.com:3500/fixtures/xhr-fetch-requests.html">http://www.foobar.com:3500/fixtures/xhr-fetch-requests.html</a></li>
<li><a data-cy="integrity-link" href="http://www.foobar.com:3500/fixtures/scripts-with-integrity.html">http://www.foobar.com:3500/fixtures/scripts-with-integrity.html</a></li>
<li><a data-cy="cookie-login">Login with Social</a></li>
<li><a data-cy="cookie-login-https">Login with Social (https)</a></li>
Expand Down
18 changes: 0 additions & 18 deletions packages/driver/cypress/fixtures/xhr-fetch-onload.html

This file was deleted.

109 changes: 109 additions & 0 deletions packages/driver/cypress/fixtures/xhr-fetch-requests.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
<!DOCTYPE html>
<html>
<body>
<h1 data-cy="assertion-header">Making XHR and Fetch Requests behind the scenes if fireOnload is true!</h1>
<button data-cy="trigger-fetch" onclick="triggerFetch('/test-request')"> trigger fetch </button>
<button data-cy="trigger-fetch-with-request-object" onclick="triggerFetchWithRequestObject('/test-request')"> trigger fetch with Request Object</button>
<button data-cy="trigger-fetch-with-url-object" onclick="triggerFetchWithUrlObject('/test-request')" > trigger fetch with URL Object</button>
<button data-cy="trigger-fetch-omit" onclick="triggerFetch('/test-request', 'omit')"> trigger fetch w/ omit credentials </button>
<button data-cy="trigger-fetch-with-request-object-omit" onclick="triggerFetchWithRequestObject('/test-request', 'omit')"> trigger fetch with Request Object w/ omit credentials</button>
<button data-cy="trigger-fetch-with-url-object-omit" onclick="triggerFetchWithUrlObject('/test-request', 'omit')" > trigger fetch with URL Object w/ omit credentials</button>
<button data-cy="trigger-fetch-include" onclick="triggerFetch('/test-request', 'include')"> trigger fetch w/ include credentials </button>
<button data-cy="trigger-fetch-with-request-object-include" onclick="triggerFetchWithRequestObject('/test-request', 'include')"> trigger fetch with Request Object w/ include credentials</button>
<button data-cy="trigger-fetch-with-url-object-include" onclick="triggerFetchWithUrlObject('/test-request', 'include')" > trigger fetch with URL Object w/ include credentials</button>
<button data-cy="trigger-fetch-with-bad-options" onclick="triggerFetch(null)">trigger fetch with bad option</button>
<button data-cy="trigger-fetch-with-preflight" onclick="triggerFailingFetchPreflight('/test-request')">trigger fetch w/ preflight</button>
<button data-cy="trigger-xml-http-request" onclick="triggerXmlHttpRequest('/test-request')">trigger xmlHttpRequest</button>
<button data-cy="trigger-xml-http-request-with-credentials" onclick="triggerXmlHttpRequest('/test-request', true)">trigger xmlHttpRequest w/ credentials</button>
<button data-cy="trigger-xml-http-request-with-bad-options" onclick="triggerXmlHttpRequest(null)">trigger xmlHttpRequest w/ bad options</button>
<button data-cy="trigger-xml-http-request-with-preflight" onclick="triggerFailingXmlHttpRequestPreflight('/test-request')">trigger xmlHttpRequest w/ preflight</button>
<script>
function triggerFetch(requestOrUrlObjOrString, credentials){
let fetchReq
if(credentials){
fetchReq = fetch(requestOrUrlObjOrString, {
credentials
})
} else {
fetchReq = fetch(requestOrUrlObjOrString)
}
return fetchReq.then(function(response) {
// throw errors in our application to test when fetch fails
if (!response.ok) {
throw Error(response.status);
}
return response;
})
}

function triggerFetchWithRequestObject(urlString, credentials){
let req = new Request(urlString)
if(credentials){
// credentials must either match options passed into fetch or must exist on the Request object itself
req = new Request(urlString, {
credentials
})
}
return triggerFetch(req)
}

function triggerFetchWithUrlObject(urlString, credentials){
return triggerFetch(new URL(urlString, window.location.origin), credentials)
}

function triggerFailingFetchPreflight(relativeUrl){
let url = new URL(relativeUrl, 'http://app.foobar.com:3500').toString()
return fetch(url, {
headers: {
'foo': 'bar'
},
credentials: 'include'
}).catch(() => {
throw new Error('CORS ERROR');
}).then(() => {
throw new Error('request succeeded when it shouldn\'t have')
})
}

function triggerXmlHttpRequest(relativeUrl, withCredentials = false){
const url = new URL(relativeUrl, window.location.origin)
xhr = new XMLHttpRequest();
xhr.open("GET", url);
xhr.withCredentials = withCredentials
xhr.send();
}

function triggerFailingXmlHttpRequestPreflight(relativeUrl){
// might need a cross origin req here
let url = new URL(relativeUrl, 'http://app.foobar.com:3500').toString()

let xhr = new XMLHttpRequest()

xhr.open('GET', url)
// adding headers to trigger a preflight request. @see https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS#simple_requests
xhr.setRequestHeader('foo', 'bar')
// since the plugin server sets the Access-Control-Allow-Origin to * (wildcard), this request will fail as a CORS errors. The
xhr.withCredentials = true

xhr.onerror = function () {
throw new Error('CORS ERROR')
}

xhr.send()
}

function fireXHRAndFetchRequests() {
if(window.location.search.includes('fireOnload=true')){
xhr = new XMLHttpRequest();
xhr.open("GET", "http://localhost:3500/foo.bar.baz.json");
xhr.responseType = "json";
xhr.send();

fetch("http://localhost:3500/foo.bar.baz.json")
}
}

fireXHRAndFetchRequests()
</script>
</body>
</html>
11 changes: 11 additions & 0 deletions packages/driver/src/cross-origin/cypress.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'

Expand Down Expand Up @@ -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, {
Expand Down
51 changes: 51 additions & 0 deletions packages/driver/src/cross-origin/patches/fetch.ts
Original file line number Diff line number Diff line change
@@ -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)
}
}
}
17 changes: 17 additions & 0 deletions packages/driver/src/cross-origin/patches/utils/index.ts
Original file line number Diff line number Diff line change
@@ -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
}
42 changes: 42 additions & 0 deletions packages/driver/src/cross-origin/patches/xmlHttpRequest.ts
Original file line number Diff line number Diff line change
@@ -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)
}
}
}
3 changes: 3 additions & 0 deletions packages/server/lib/socket-base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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}`)
}
Expand Down

0 comments on commit f356065

Please sign in to comment.