mirror of
https://github.com/cypress-io/cypress.git
synced 2026-01-21 22:50:13 -06:00
fix: Improve document.cookie patch (#23643)
This commit is contained in:
@@ -3,7 +3,6 @@ import { EventEmitter } from 'events'
|
||||
import type { MobxRunnerStore } from '@packages/app/src/store/mobx-runner-store'
|
||||
import type MobX from 'mobx'
|
||||
import type { LocalBusEmitsMap, LocalBusEventMap, DriverToLocalBus, SocketToDriverMap } from './event-manager-types'
|
||||
|
||||
import type { RunState, CachedTestState, AutomationElementId, FileDetails, ReporterStartInfo, ReporterRunState } from '@packages/types'
|
||||
|
||||
import { logger } from './logger'
|
||||
@@ -40,7 +39,7 @@ const driverToSocketEvents = 'backend:request automation:request mocha recorder:
|
||||
const driverTestEvents = 'test:before:run:async test:after:run'.split(' ')
|
||||
const driverToLocalEvents = 'viewport:changed config stop url:changed page:loading visit:failed visit:blank cypress:in:cypress:runner:event'.split(' ')
|
||||
const socketRerunEvents = 'runner:restart watched:file:changed'.split(' ')
|
||||
const socketToDriverEvents = 'net:stubbing:event request:event script:error cross:origin:automation:cookies'.split(' ')
|
||||
const socketToDriverEvents = 'net:stubbing:event request:event script:error cross:origin:cookies'.split(' ')
|
||||
const localToReporterEvents = 'reporter:log:add reporter:log:state:changed reporter:log:remove'.split(' ')
|
||||
|
||||
/**
|
||||
@@ -698,32 +697,18 @@ export class EventManager {
|
||||
log?.set(attrs)
|
||||
})
|
||||
|
||||
// This message comes from the AUT, not the spec bridge.
|
||||
// This is called in the event that cookies are set in a cross origin AUT prior to attaching a spec bridge.
|
||||
Cypress.primaryOriginCommunicator.on('aut:set:cookie', ({ cookie, href }, _origin, source) => {
|
||||
const { superDomain } = Cypress.Location.create(href)
|
||||
const automationCookie = Cypress.Cookies.toughCookieToAutomationCookie(Cypress.Cookies.parse(cookie), superDomain)
|
||||
|
||||
Cypress.automation('set:cookie', automationCookie).then(() => {
|
||||
// It's possible the source has already unloaded before this event has been processed.
|
||||
source?.postMessage({ event: 'cross:origin:aut:set:cookie' }, '*')
|
||||
})
|
||||
.catch(() => {
|
||||
// unlikely there will be errors, but ignore them in any case, since
|
||||
// they're not user-actionable
|
||||
})
|
||||
})
|
||||
|
||||
// This message comes from the AUT, not the spec bridge.
|
||||
// This is called in the event that cookies are retrieved in a cross origin AUT prior to attaching a spec bridge.
|
||||
Cypress.primaryOriginCommunicator.on('aut:get:cookie', async ({ href }, _origin, source) => {
|
||||
const { superDomain } = Cypress.Location.create(href)
|
||||
|
||||
const cookies = await Cypress.automation('get:cookies', { superDomain })
|
||||
|
||||
// It's possible the source has already unloaded before this event has been processed.
|
||||
source?.postMessage({ event: 'cross:origin:aut:get:cookie', cookies }, '*')
|
||||
})
|
||||
// This message comes from the AUT, not the spec bridge. This is called in
|
||||
// the event that cookies are set via document.cookie in a cross origin
|
||||
// AUT prior to attaching a spec bridge.
|
||||
Cypress.primaryOriginCommunicator.on(
|
||||
'aut:set:cookie',
|
||||
(options: { cookie, url: string, sameSiteContext: string }) => {
|
||||
// unlikely there will be errors, but ignore them in any case, since
|
||||
// they're not user-actionable
|
||||
Cypress.automation('set:cookie', options.cookie).catch(() => {})
|
||||
Cypress.backend('cross:origin:set:cookie', options).catch(() => {})
|
||||
},
|
||||
)
|
||||
|
||||
// The window.top should not change between test reloads, and we only need to bind the message event when Cypress is recreated
|
||||
// Forward all message events to the current instance of the multi-origin communicator
|
||||
|
||||
@@ -453,12 +453,13 @@ describe('cy.origin - cookie login', { browser: '!webkit' }, () => {
|
||||
const expires = (new Date()).toUTCString()
|
||||
|
||||
cy.get('[data-cy="username"]').type(username)
|
||||
cy.get('[data-cy="localhostCookieProps"]').type(`Expires=${expires}`)
|
||||
cy.get('[data-cy="cookieProps"]').type(`Expires=${expires}`)
|
||||
cy.get('[data-cy="login"]').click()
|
||||
})
|
||||
|
||||
cy.origin('http://www.idp.com:3500', () => {
|
||||
cy.clearCookie('user')
|
||||
cy.origin('http://www.idp.com:3501', () => {
|
||||
cy.wait(1000) // give cookie time to expire
|
||||
cy.reload()
|
||||
cy.document().its('cookie').should('not.include', 'user=')
|
||||
})
|
||||
})
|
||||
@@ -502,15 +503,18 @@ describe('cy.origin - cookie login', { browser: '!webkit' }, () => {
|
||||
cy.getCookie('user').should('be.null')
|
||||
})
|
||||
|
||||
it('past max-age -> not accessible via document.cookie', () => {
|
||||
// expiring cookies set by automation don't seem to get unset appropriately
|
||||
// in Firefox. this issue doesn't seem to be specific to cross-origin tests,
|
||||
// as it happens even using cy.setCookie()
|
||||
it('past max-age -> not accessible via document.cookie', { browser: '!firefox' }, () => {
|
||||
cy.get('[data-cy="cookie-login-land-on-idp"]').click()
|
||||
cy.origin('http://www.foobar.com:3500', { args: { username } }, ({ username }) => {
|
||||
cy.get('[data-cy="username"]').type(username)
|
||||
cy.get('[data-cy="localhostCookieProps"]').type('Max-Age=1')
|
||||
cy.get('[data-cy="cookieProps"]').type('Max-Age=1')
|
||||
cy.get('[data-cy="login"]').click()
|
||||
})
|
||||
|
||||
cy.origin('http://www.idp.com:3500', () => {
|
||||
cy.origin('http://www.idp.com:3501', () => {
|
||||
cy.wait(1500) // give cookie time to expire
|
||||
cy.reload()
|
||||
cy.document().its('cookie').should('not.include', 'user=')
|
||||
@@ -664,14 +668,15 @@ describe('cy.origin - cookie login', { browser: '!webkit' }, () => {
|
||||
})
|
||||
|
||||
it('gets cookie set by http request', () => {
|
||||
cy.get('[data-cy="cookie-login-land-on-idp"]').click()
|
||||
cy.get('[data-cy="cookie-login-land-on-document-cookie"]').click()
|
||||
cy.origin('http://www.foobar.com:3500', { args: { username } }, ({ username }) => {
|
||||
cy.get('[data-cy="username"]').type(username)
|
||||
cy.get('[data-cy="login"]').click()
|
||||
})
|
||||
|
||||
cy.origin('http://www.idp.com:3500', { args: { username } }, ({ username }) => {
|
||||
cy.document().its('cookie').should('include', `user=${username}`)
|
||||
cy.origin('http://www.idp.com:3501', { args: { username } }, ({ username }) => {
|
||||
cy.get('[data-cy="doc-cookie"]').invoke('text')
|
||||
.should('include', `user=${username}`)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -698,20 +703,20 @@ describe('cy.origin - cookie login', { browser: '!webkit' }, () => {
|
||||
})
|
||||
|
||||
it('cookie properties are preserved when set via automation', () => {
|
||||
cy.get('[data-cy="cross-origin-secondary-link"]').click()
|
||||
cy.origin('http://www.foobar.com:3500', () => {
|
||||
cy.get('[data-cy="cookie-https"]').click()
|
||||
cy.origin('https://www.foobar.com:3502', () => {
|
||||
cy.document().then((doc) => {
|
||||
doc.cookie = 'key=value; SameSite=Strict; Path=/foo'
|
||||
doc.cookie = 'key=value; SameSite=Strict; Secure; Path=/fixtures'
|
||||
})
|
||||
|
||||
cy.getCookie('key').then((cookie) => {
|
||||
expect(Cypress._.omit(cookie, 'expiry')).to.deep.equal({
|
||||
domain: '.foobar.com',
|
||||
domain: '.www.foobar.com',
|
||||
httpOnly: false,
|
||||
name: 'key',
|
||||
path: '/foo',
|
||||
path: '/fixtures',
|
||||
sameSite: 'strict',
|
||||
secure: false,
|
||||
secure: true,
|
||||
value: 'value',
|
||||
})
|
||||
})
|
||||
@@ -743,7 +748,7 @@ describe('cy.origin - cookie login', { browser: '!webkit' }, () => {
|
||||
doc.cookie = 'key2=value2'
|
||||
})
|
||||
|
||||
cy.document().its('cookie').should('equal', 'key2=value2; key1=value1')
|
||||
cy.document().its('cookie').should('equal', 'key1=value1; key2=value2')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -754,10 +759,29 @@ describe('cy.origin - cookie login', { browser: '!webkit' }, () => {
|
||||
doc.cookie = 'key=value'
|
||||
})
|
||||
|
||||
// it can take a small amount of time for the cookie to make it to
|
||||
// automation, but it's unlikely a user will encounter this issue
|
||||
// since they'd pretty much have to write this exact test. making it
|
||||
// wait a second is probably overkill, but purposefully keeping the
|
||||
// wait long to avoid this test becoming flaky
|
||||
cy.wait(1000)
|
||||
cy.getCookie('key').its('value').should('equal', 'value')
|
||||
})
|
||||
})
|
||||
|
||||
it('returns cookie set by cy.setCookie()', () => {
|
||||
cy.get('[data-cy="cookie-login-land-on-idp"]').click()
|
||||
cy.origin('http://www.foobar.com:3500', { args: { username } }, ({ username }) => {
|
||||
cy.get('[data-cy="username"]').type(username)
|
||||
cy.get('[data-cy="login"]').click()
|
||||
})
|
||||
|
||||
cy.origin('http://www.idp.com:3501', () => {
|
||||
cy.setCookie('foo', 'bar')
|
||||
cy.document().its('cookie').should('include', 'foo=bar')
|
||||
})
|
||||
})
|
||||
|
||||
it('no longer returns cookie after cy.clearCookie()', () => {
|
||||
cy.get('[data-cy="cookie-login-land-on-idp"]').click()
|
||||
cy.origin('http://www.foobar.com:3500', { args: { username } }, ({ username }) => {
|
||||
@@ -765,20 +789,20 @@ describe('cy.origin - cookie login', { browser: '!webkit' }, () => {
|
||||
cy.get('[data-cy="login"]').click()
|
||||
})
|
||||
|
||||
cy.origin('http://www.idp.com:3500', () => {
|
||||
cy.origin('http://www.idp.com:3501', () => {
|
||||
cy.clearCookie('user')
|
||||
cy.document().its('cookie').should('equal', '')
|
||||
})
|
||||
})
|
||||
|
||||
it('no longer returns cookie after cy.clearCookies()', () => {
|
||||
it('no longer returns cookies after cy.clearCookies()', () => {
|
||||
cy.get('[data-cy="cookie-login-land-on-idp"]').click()
|
||||
cy.origin('http://www.foobar.com:3500', { args: { username } }, ({ username }) => {
|
||||
cy.get('[data-cy="username"]').type(username)
|
||||
cy.get('[data-cy="login"]').click()
|
||||
})
|
||||
|
||||
cy.origin('http://www.idp.com:3500', () => {
|
||||
cy.origin('http://www.idp.com:3501', () => {
|
||||
cy.clearCookies()
|
||||
cy.document().its('cookie').should('equal', '')
|
||||
})
|
||||
@@ -791,7 +815,7 @@ describe('cy.origin - cookie login', { browser: '!webkit' }, () => {
|
||||
cy.get('[data-cy="login"]').click()
|
||||
})
|
||||
|
||||
cy.origin('http://www.idp.com:3500', { args: { username } }, ({ username }) => {
|
||||
cy.origin('http://www.idp.com:3501', { args: { username } }, ({ username }) => {
|
||||
cy.document().then((doc) => {
|
||||
doc.cookie = 'key=value'
|
||||
})
|
||||
@@ -810,12 +834,12 @@ describe('cy.origin - cookie login', { browser: '!webkit' }, () => {
|
||||
})
|
||||
|
||||
cy.get('[data-cy="document-cookie"]').click()
|
||||
cy.origin('http://www.foobar.com:3500', { args: { username } }, ({ username }) => {
|
||||
cy.origin('http://www.foobar.com:3500', () => {
|
||||
cy.document().its('cookie').should('include', 'name=value')
|
||||
cy.get('[data-cy="doc-cookie"]').invoke('text').should('equal', 'name=value')
|
||||
cy.getCookie('name').then((cookie) => {
|
||||
expect(Cypress._.omit(cookie, 'expiry')).to.deep.equal({
|
||||
domain: '.foobar.com',
|
||||
domain: '.www.foobar.com',
|
||||
httpOnly: false,
|
||||
name: 'name',
|
||||
path: '/',
|
||||
@@ -826,5 +850,59 @@ describe('cy.origin - cookie login', { browser: '!webkit' }, () => {
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it('preserves duplicate cookie keys', () => {
|
||||
cy.get('[data-cy="cookie-login-land-on-document-cookie"]').click()
|
||||
cy.origin('http://www.foobar.com:3500', { args: { username } }, ({ username }) => {
|
||||
cy.get('[data-cy="username"]').type(username)
|
||||
cy.get('[data-cy="login"]').click()
|
||||
})
|
||||
|
||||
cy.origin('http://www.idp.com:3501', () => {
|
||||
// ensure we've redirected to the right page
|
||||
cy.url().should('not.include', 'http://www.idp.com:3501/verify-cookie-login')
|
||||
cy.document().then((doc) => {
|
||||
doc.cookie = 'key=value1; domain=www.idp.com'
|
||||
doc.cookie = 'key=value2; domain=idp.com'
|
||||
})
|
||||
|
||||
// order of the cookies differs depending on browser, so just
|
||||
// ensure that each one is there
|
||||
cy.document().its('cookie').should('include', 'key=value1')
|
||||
cy.document().its('cookie').should('include', 'key=value2')
|
||||
})
|
||||
})
|
||||
|
||||
it('setting cookie preserves cookies on subsequent page loads', () => {
|
||||
cy.get('[data-cy="cross-origin-secondary-link"]').click()
|
||||
cy.origin('http://www.foobar.com:3500', () => {
|
||||
cy.document().then((doc) => {
|
||||
doc.cookie = 'key=value'
|
||||
})
|
||||
|
||||
cy.document().its('cookie').should('equal', 'key=value')
|
||||
cy.wait(500)
|
||||
cy.reload()
|
||||
cy.document().its('cookie').should('equal', 'key=value')
|
||||
})
|
||||
})
|
||||
|
||||
// the spec bridge will likely already exist in this spec when running
|
||||
// all the tests together, but this ensures the behavior in case it's run
|
||||
// alone or if we implement spec bridge removal in the future
|
||||
it('works when spec bridge is set up prior to page load', () => {
|
||||
cy.origin('http://www.idp.com:3501', () => {})
|
||||
|
||||
cy.get('[data-cy="cookie-login-land-on-document-cookie"]').click()
|
||||
cy.origin('http://www.foobar.com:3500', { args: { username } }, ({ username }) => {
|
||||
cy.get('[data-cy="username"]').type(username)
|
||||
cy.get('[data-cy="login"]').click()
|
||||
})
|
||||
|
||||
cy.origin('http://www.idp.com:3501', { args: { username } }, ({ username }) => {
|
||||
cy.get('[data-cy="doc-cookie"]').invoke('text')
|
||||
.should('include', `user=${username}`)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -3,11 +3,14 @@
|
||||
<head>
|
||||
</head>
|
||||
<body>
|
||||
<p id='test' data-cy="doc-cookie">should be replaced</p>
|
||||
<p>
|
||||
<strong>document.cookie</strong>:
|
||||
<span data-cy="doc-cookie">should be replaced</span>
|
||||
</p>
|
||||
<script>
|
||||
document.cookie = 'name=value'
|
||||
|
||||
document.getElementById('test').innerText = document.cookie
|
||||
document.querySelector('[data-cy="doc-cookie"]').innerText = document.cookie
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -16,15 +16,16 @@
|
||||
<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-http" href="http://www.foobar.com:3500/fixtures/secondary-origin.html">Visit foobar.com over http</a></li>
|
||||
<li><a data-cy="cookie-https" href="https://www.foobar.com:3502/fixtures/secondary-origin.html">Visit foobar.com over https</a></li>
|
||||
<li><a data-cy="document-cookie" href="http://www.foobar.com:3500/fixtures/auth/document-cookie.html">http://www.foobar.com:3500/fixtures/auth/document-cookie.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>
|
||||
<li><a data-cy="cookie-login-subdomain">Login with Social (subdomain)</a></li>
|
||||
<li><a data-cy="cookie-login-alias">Login with Social (aliased localhost)</a></li>
|
||||
<li><a data-cy="cookie-login-override">Login with Social (cookie override)</a></li>
|
||||
<li><a data-cy="cookie-login-land-on-idp">Login with Social (lands on idp)</a></li>
|
||||
<li><a data-cy="cookie-http" href="http://www.foobar.com:3500/fixtures/secondary-origin.html">Visit foobar.com over http</a></li>
|
||||
<li><a data-cy="cookie-https" href="https://www.foobar.com:3502/fixtures/secondary-origin.html">Visit foobar.com over https</a></li>
|
||||
<li><a data-cy="document-cookie" href="http://www.foobar.com:3500/fixtures/auth/document-cookie.html">http://www.foobar.com:3500/fixtures/auth/document-cookie.html</a></li>
|
||||
<li><a data-cy="cookie-login-land-on-document-cookie">Login with Social (lands on document.cookie)</a></li>
|
||||
</ul>
|
||||
<script>
|
||||
function setHref (dataCy, redirect2, primaryQueryAddition = '') {
|
||||
@@ -45,7 +46,8 @@
|
||||
setHref('cookie-login-subdomain', 'http://localhost:3500/login&subdomain=true')
|
||||
setHref('cookie-login-alias', 'http://localhost:3500/login&alias=true')
|
||||
setHref('cookie-login-override', 'https://localhost:3502/login', '&override=true')
|
||||
setHref('cookie-login-land-on-idp', 'http://www.idp.com:3500/welcome')
|
||||
setHref('cookie-login-land-on-idp', 'http://www.idp.com:3501/welcome')
|
||||
setHref('cookie-login-land-on-document-cookie', 'http://www.idp.com:3501/fixtures/auth/document-cookie.html')
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -60,7 +60,7 @@ const createCypress = () => {
|
||||
|
||||
const autWindow = findWindow()
|
||||
|
||||
if (autWindow) {
|
||||
if (autWindow && !autWindow.Cypress) {
|
||||
attachToWindow(autWindow)
|
||||
}
|
||||
})
|
||||
@@ -155,6 +155,11 @@ const attachToWindow = (autWindow: Window) => {
|
||||
|
||||
const cy = Cypress.cy
|
||||
|
||||
// this communicates to the injection code that Cypress is now available so
|
||||
// it can safely subscribe to Cypress events, etc
|
||||
// @ts-ignore
|
||||
autWindow.__attachToCypress(Cypress)
|
||||
|
||||
Cypress.state('window', autWindow)
|
||||
Cypress.state('document', autWindow.document)
|
||||
|
||||
@@ -162,14 +167,6 @@ const attachToWindow = (autWindow: Window) => {
|
||||
patchFormElementSubmit(autWindow)
|
||||
}
|
||||
|
||||
Cypress.removeAllListeners('app:timers:reset')
|
||||
Cypress.removeAllListeners('app:timers:pause')
|
||||
|
||||
// @ts-expect-error - the injected code adds the cypressTimersReset function to window
|
||||
Cypress.on('app:timers:reset', autWindow.cypressTimersReset)
|
||||
// @ts-ignore - the injected code adds the cypressTimersPause function to window
|
||||
Cypress.on('app:timers:pause', autWindow.cypressTimersPause)
|
||||
|
||||
cy.urlNavigationEvent('before:load')
|
||||
|
||||
cy.overrides.wrapNativeMethods(autWindow)
|
||||
|
||||
42
packages/driver/src/cross-origin/events/cookies.ts
Normal file
42
packages/driver/src/cross-origin/events/cookies.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import type { AutomationCookie } from '@packages/server/lib/automation/cookies'
|
||||
import type { ICypress } from '../../cypress'
|
||||
|
||||
// cross-origin cookies collected by the the proxy are sent down to the driver
|
||||
// via this event, so that they can be set via automation once the page has
|
||||
// loaded. it's necessary to wait until page load because Firefox with the
|
||||
// extension will hang the page load if we attempt to set the cookies via
|
||||
// automation before the page loads
|
||||
export const handleCrossOriginCookies = (Cypress: ICypress) => {
|
||||
// multiple requests could set cookies while the page is loading, so we
|
||||
// collect all cookies and only send set them via automation once after
|
||||
// the page has loaded
|
||||
let cookiesToSend: AutomationCookie[] = []
|
||||
let waitingToSend = false
|
||||
|
||||
Cypress.on('cross:origin:cookies', (cookies: AutomationCookie[]) => {
|
||||
cookiesToSend = cookiesToSend.concat(cookies)
|
||||
|
||||
Cypress.backend('cross:origin:cookies:received')
|
||||
|
||||
if (waitingToSend) return
|
||||
|
||||
waitingToSend = true
|
||||
|
||||
// this event allows running a handler before stability is released.
|
||||
// this prevents subsequent commands from running until the cookies
|
||||
// are set via automation
|
||||
// @ts-ignore
|
||||
Cypress.once('before:stability:release', () => {
|
||||
const cookies = cookiesToSend
|
||||
|
||||
cookiesToSend = []
|
||||
waitingToSend = false
|
||||
|
||||
// this will be awaited before any stability-reliant actions
|
||||
return Cypress.automation('add:cookies', cookies)
|
||||
.catch(() => {
|
||||
// errors here can be ignored as they're not user-actionable
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -355,6 +355,8 @@ export default function (Commands, Cypress, cy, state, config) {
|
||||
$errUtils.throwErrByPath('setCookie.host_prefix', { onFail })
|
||||
}
|
||||
|
||||
Cypress.emit('set:cookie', cookie)
|
||||
|
||||
return cy.retryIfCommandAUTOriginMismatch(() => {
|
||||
return automateCookies('set:cookie', cookie, options._log, responseTimeout)
|
||||
.then(pickCookieProps)
|
||||
@@ -404,6 +406,8 @@ export default function (Commands, Cypress, cy, state, config) {
|
||||
$errUtils.throwErrByPath('clearCookie.invalid_argument', { onFail })
|
||||
}
|
||||
|
||||
Cypress.emit('clear:cookie', name)
|
||||
|
||||
// TODO: prevent clearing a cypress namespace
|
||||
return cy.retryIfCommandAUTOriginMismatch(() => {
|
||||
return automateCookies('clear:cookie', { name }, options._log, responseTimeout)
|
||||
@@ -449,6 +453,8 @@ export default function (Commands, Cypress, cy, state, config) {
|
||||
})
|
||||
}
|
||||
|
||||
Cypress.emit('clear:cookies')
|
||||
|
||||
return cy.retryIfCommandAUTOriginMismatch(() => {
|
||||
return getAndClear(options._log, responseTimeout, { domain: options.domain })
|
||||
.then((resp) => {
|
||||
|
||||
@@ -200,6 +200,7 @@ export default (Commands, Cypress: Cypress.Cypress, cy: Cypress.cy, state: State
|
||||
originCommandBaseUrl: location.href,
|
||||
isStable: Cypress.state('isStable'),
|
||||
autLocation: Cypress.state('autLocation')?.href,
|
||||
crossOriginCookies: Cypress.state('crossOriginCookies'),
|
||||
},
|
||||
config: preprocessConfig(Cypress.config()),
|
||||
env: preprocessEnv(Cypress.env()),
|
||||
|
||||
@@ -15,11 +15,15 @@ export const create = (Cypress: ICypress, state: StateFunc) => ({
|
||||
// show the 'loading spinner' during an app page loading transition event
|
||||
Cypress.action('cy:stability:changed', stable, event)
|
||||
|
||||
Cypress.action('cy:before:stability:release', stable)
|
||||
if (!stable) {
|
||||
return
|
||||
}
|
||||
|
||||
Cypress.action('cy:before:stability:release')
|
||||
.then(() => {
|
||||
const whenStable = state('whenStable')
|
||||
|
||||
if (stable && whenStable) {
|
||||
if (whenStable) {
|
||||
whenStable()
|
||||
}
|
||||
})
|
||||
|
||||
@@ -645,7 +645,7 @@ class $Cypress {
|
||||
return this.emit('snapshot', ...args)
|
||||
|
||||
case 'cy:before:stability:release':
|
||||
return this.emitThen('before:stability:release', ...args)
|
||||
return this.emitThen('before:stability:release')
|
||||
|
||||
case 'app:uncaught:exception':
|
||||
return this.emitMap('uncaught:exception', ...args)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import _ from 'lodash'
|
||||
import Cookies from 'js-cookie'
|
||||
import { CookieJar, toughCookieToAutomationCookie } from '@packages/server/lib/util/cookies'
|
||||
import { CookieJar } from '@packages/server/lib/util/cookies'
|
||||
|
||||
import $errUtils from './error_utils'
|
||||
|
||||
@@ -153,8 +153,6 @@ export const $Cookies = (namespace, domain) => {
|
||||
parse (cookieString: string) {
|
||||
return CookieJar.parse(cookieString)
|
||||
},
|
||||
|
||||
toughCookieToAutomationCookie,
|
||||
}
|
||||
|
||||
return API
|
||||
|
||||
@@ -33,11 +33,11 @@ import { TestConfigOverride } from '../cy/testConfigOverrides'
|
||||
import { create as createOverrides, IOverrides } from '../cy/overrides'
|
||||
import { historyNavigationTriggeredHashChange } from '../cy/navigation'
|
||||
import { EventEmitter2 } from 'eventemitter2'
|
||||
import { handleCrossOriginCookies } from '../cross-origin/events/cookies'
|
||||
|
||||
import type { ICypress } from '../cypress'
|
||||
import type { ICookies } from './cookies'
|
||||
import type { StateFunc } from './state'
|
||||
import type { AutomationCookie } from '@packages/server/lib/automation/cookies'
|
||||
|
||||
const debugErrors = debugFn('cypress:driver:errors')
|
||||
|
||||
@@ -375,29 +375,7 @@ export class $Cy extends EventEmitter2 implements ITimeouts, IStability, IAssert
|
||||
this.enqueue(attrs)
|
||||
})
|
||||
|
||||
Cypress.on('cross:origin:automation:cookies', (cookies: AutomationCookie[]) => {
|
||||
const existingCookies: AutomationCookie[] = state('cross:origin:automation:cookies') || []
|
||||
|
||||
this.state('cross:origin:automation:cookies', existingCookies.concat(cookies))
|
||||
|
||||
Cypress.backend('cross:origin:automation:cookies:received')
|
||||
})
|
||||
|
||||
Cypress.on('before:stability:release', (stable: boolean) => {
|
||||
const cookies: AutomationCookie[] = state('cross:origin:automation:cookies') || []
|
||||
|
||||
if (!stable || !cookies.length) return
|
||||
|
||||
// reset the state cookies before setting them via automation in case
|
||||
// any more get set in the interim
|
||||
state('cross:origin:automation:cookies', [])
|
||||
|
||||
// this will be awaited before any stability-reliant actions
|
||||
return Cypress.automation('add:cookies', cookies)
|
||||
.catch(() => {
|
||||
// errors here can be ignored as they're not user-actionable
|
||||
})
|
||||
})
|
||||
handleCrossOriginCookies(Cypress)
|
||||
}
|
||||
|
||||
isCy (val) {
|
||||
|
||||
16
packages/driver/types/internal-types.d.ts
vendored
16
packages/driver/types/internal-types.d.ts
vendored
@@ -1,6 +1,7 @@
|
||||
// NOTE: this is for internal Cypress types that we don't want exposed in the public API but want for development
|
||||
// TODO: find a better place for this
|
||||
/// <reference path="./internal-types-lite.d.ts" />
|
||||
|
||||
interface InternalWindowLoadDetails {
|
||||
type: 'same:origin' | 'cross:origin' | 'cross:origin:failure'
|
||||
error?: Error
|
||||
@@ -30,6 +31,9 @@ declare namespace Cypress {
|
||||
isCrossOriginSpecBridge: boolean
|
||||
originalConfig: Cypress.ObjectLike
|
||||
cy: $Cy
|
||||
Location: {
|
||||
create: (url: string) => ({ domain: string, superDomain: string })
|
||||
}
|
||||
}
|
||||
|
||||
interface CypressUtils {
|
||||
@@ -47,6 +51,18 @@ declare namespace Cypress {
|
||||
document: Document
|
||||
projectRoot?: string
|
||||
}
|
||||
|
||||
interface Actions {
|
||||
(action: 'set:cookie', fn: (cookie: AutomationCookie) => void)
|
||||
(action: 'clear:cookie', fn: (name: string) => void)
|
||||
(action: 'clear:cookies', fn: () => void)
|
||||
(action: 'cross:origin:cookies', fn: (cookies: AutomationCookie[]) => void)
|
||||
(action: 'before:stability:release', fn: () => void)
|
||||
}
|
||||
|
||||
interface Backend {
|
||||
(task: 'cross:origin:cookies:received'): Promise<void>
|
||||
}
|
||||
}
|
||||
|
||||
type AliasedRequest = {
|
||||
|
||||
@@ -22,7 +22,7 @@ import { DeferredSourceMapCache } from '@packages/rewriter'
|
||||
import type { RemoteStates } from '@packages/server/lib/remote_states'
|
||||
import type { CookieJar } from '@packages/server/lib/util/cookies'
|
||||
import type { ResourceTypeAndCredentialManager } from '@packages/server/lib/util/resourceTypeAndCredentialManager'
|
||||
import type { Automation } from '@packages/server/lib/automation/automation'
|
||||
import type { AutomationCookie } from '@packages/server/lib/automation/cookies'
|
||||
|
||||
function getRandomColorFn () {
|
||||
return chalk.hex(`#${Number(
|
||||
@@ -55,10 +55,10 @@ type HttpMiddlewareCtx<T> = {
|
||||
middleware: HttpMiddlewareStacks
|
||||
getCookieJar: () => CookieJar
|
||||
deferSourceMapRewrite: (opts: { js: string, url: string }) => string
|
||||
getAutomation: () => Automation
|
||||
getPreRequest: (cb: GetPreRequestCb) => void
|
||||
getAUTUrl: Http['getAUTUrl']
|
||||
setAUTUrl: Http['setAUTUrl']
|
||||
simulatedCookies: AutomationCookie[]
|
||||
} & T
|
||||
|
||||
export const defaultMiddleware = {
|
||||
@@ -70,7 +70,6 @@ export const defaultMiddleware = {
|
||||
export type ServerCtx = Readonly<{
|
||||
config: CyServer.Config & Cypress.Config
|
||||
shouldCorrelatePreRequests?: () => boolean
|
||||
getAutomation: () => Automation
|
||||
getFileServerToken: () => string
|
||||
getCookieJar: () => CookieJar
|
||||
remoteStates: RemoteStates
|
||||
@@ -215,7 +214,6 @@ export class Http {
|
||||
config: CyServer.Config
|
||||
shouldCorrelatePreRequests: () => boolean
|
||||
deferredSourceMapCache: DeferredSourceMapCache
|
||||
getAutomation: () => Automation
|
||||
getFileServerToken: () => string
|
||||
remoteStates: RemoteStates
|
||||
middleware: HttpMiddlewareStacks
|
||||
@@ -235,7 +233,6 @@ export class Http {
|
||||
|
||||
this.config = opts.config
|
||||
this.shouldCorrelatePreRequests = opts.shouldCorrelatePreRequests || (() => false)
|
||||
this.getAutomation = opts.getAutomation
|
||||
this.getFileServerToken = opts.getFileServerToken
|
||||
this.remoteStates = opts.remoteStates
|
||||
this.middleware = opts.middleware
|
||||
@@ -263,7 +260,6 @@ export class Http {
|
||||
buffers: this.buffers,
|
||||
config: this.config,
|
||||
shouldCorrelatePreRequests: this.shouldCorrelatePreRequests,
|
||||
getAutomation: this.getAutomation,
|
||||
getFileServerToken: this.getFileServerToken,
|
||||
remoteStates: this.remoteStates,
|
||||
request: this.request,
|
||||
@@ -273,6 +269,7 @@ export class Http {
|
||||
serverBus: this.serverBus,
|
||||
resourceTypeAndCredentialManager: this.resourceTypeAndCredentialManager,
|
||||
getCookieJar: this.getCookieJar,
|
||||
simulatedCookies: [],
|
||||
debug: (formatter, ...args) => {
|
||||
if (!debugVerbose.enabled) return
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import type Debug from 'debug'
|
||||
import type { CookieOptions } from 'express'
|
||||
import { cors, concatStream, httpUtils } from '@packages/network'
|
||||
import type { CypressIncomingRequest, CypressOutgoingResponse } from '@packages/proxy'
|
||||
import type { HttpMiddleware } from '.'
|
||||
import type { HttpMiddleware, HttpMiddlewareThis } from '.'
|
||||
import iconv from 'iconv-lite'
|
||||
import type { IncomingMessage, IncomingHttpHeaders } from 'http'
|
||||
import { InterceptResponse } from '@packages/net-stubbing'
|
||||
@@ -14,6 +14,7 @@ import zlib from 'zlib'
|
||||
import { URL } from 'url'
|
||||
import { CookiesHelper } from './util/cookies'
|
||||
import { doesTopNeedToBeSimulated } from './util/top-simulation'
|
||||
import { toughCookieToAutomationCookie } from '@packages/server/lib/util/cookies'
|
||||
|
||||
interface ResponseMiddlewareProps {
|
||||
/**
|
||||
@@ -372,10 +373,23 @@ const MaybePreventCaching: ResponseMiddleware = function () {
|
||||
this.next()
|
||||
}
|
||||
|
||||
const setSimulatedCookies = (ctx: HttpMiddlewareThis<ResponseMiddlewareProps>) => {
|
||||
if (ctx.res.wantsInjection !== 'fullCrossOrigin') return
|
||||
|
||||
const defaultDomain = (new URL(ctx.req.proxiedUrl)).hostname
|
||||
const allCookiesForRequest = ctx.getCookieJar()
|
||||
.getCookies(ctx.req.proxiedUrl)
|
||||
.map((cookie) => toughCookieToAutomationCookie(cookie, defaultDomain))
|
||||
|
||||
ctx.simulatedCookies = allCookiesForRequest
|
||||
}
|
||||
|
||||
const MaybeCopyCookiesFromIncomingRes: ResponseMiddleware = async function () {
|
||||
const cookies: string | string[] | undefined = this.incomingRes.headers['set-cookie']
|
||||
|
||||
if (!cookies || !cookies.length) {
|
||||
setSimulatedCookies(this)
|
||||
|
||||
return this.next()
|
||||
}
|
||||
|
||||
@@ -441,17 +455,25 @@ const MaybeCopyCookiesFromIncomingRes: ResponseMiddleware = async function () {
|
||||
appendCookie(cookie)
|
||||
})
|
||||
|
||||
setSimulatedCookies(this)
|
||||
|
||||
const addedCookies = await cookiesHelper.getAddedCookies()
|
||||
|
||||
if (!addedCookies.length) {
|
||||
return this.next()
|
||||
}
|
||||
|
||||
this.serverBus.once('cross:origin:automation:cookies:received', () => {
|
||||
// we want to set the cookies via automation so they exist in the browser
|
||||
// itself. however, firefox will hang if we try to use the extension
|
||||
// to set cookies on a url that's in-flight, so we send the cookies down to
|
||||
// the driver, let the response go, and set the cookies via automation
|
||||
// from the driver once the page has loaded but before we run any further
|
||||
// commands
|
||||
this.serverBus.once('cross:origin:cookies:received', () => {
|
||||
this.next()
|
||||
})
|
||||
|
||||
this.serverBus.emit('cross:origin:automation:cookies', addedCookies)
|
||||
this.serverBus.emit('cross:origin:cookies', addedCookies)
|
||||
}
|
||||
|
||||
const REDIRECT_STATUS_CODES: any[] = [301, 302, 303, 307, 308]
|
||||
@@ -524,6 +546,7 @@ const MaybeInjectHtml: ResponseMiddleware = function () {
|
||||
modifyObstructiveCode: this.config.modifyObstructiveCode,
|
||||
url: this.req.proxiedUrl,
|
||||
deferSourceMapRewrite: this.deferSourceMapRewrite,
|
||||
simulatedCookies: this.simulatedCookies,
|
||||
})
|
||||
const encodedBody = iconv.encode(injectedBody, nodeCharset)
|
||||
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
import { oneLine } from 'common-tags'
|
||||
import { getRunnerInjectionContents, getRunnerCrossOriginInjectionContents } from '@packages/resolve-dist'
|
||||
import type { AutomationCookie } from '@packages/server/lib/automation/cookies'
|
||||
|
||||
interface FullCrossOriginOpts {
|
||||
modifyObstructiveThirdPartyCode: boolean
|
||||
modifyObstructiveCode: boolean
|
||||
simulatedCookies: AutomationCookie[]
|
||||
}
|
||||
|
||||
export function partial (domain) {
|
||||
return oneLine`
|
||||
@@ -21,15 +28,16 @@ export function full (domain) {
|
||||
})
|
||||
}
|
||||
|
||||
export function fullCrossOrigin (domain, { modifyObstructiveThirdPartyCode, modifyObstructiveCode }) {
|
||||
return getRunnerCrossOriginInjectionContents().then((contents) => {
|
||||
return oneLine`
|
||||
<script type='text/javascript'>
|
||||
document.domain = '${domain}';
|
||||
const cypressConfig = { modifyObstructiveThirdPartyCode: ${modifyObstructiveThirdPartyCode}, modifyObstructiveCode: ${modifyObstructiveCode} };
|
||||
export async function fullCrossOrigin (domain, options: FullCrossOriginOpts) {
|
||||
const contents = await getRunnerCrossOriginInjectionContents()
|
||||
|
||||
return oneLine`
|
||||
<script type='text/javascript'>
|
||||
document.domain = '${domain}';
|
||||
|
||||
(function (cypressConfig) {
|
||||
${contents}
|
||||
</script>
|
||||
`
|
||||
})
|
||||
}(${JSON.stringify(options)}));
|
||||
</script>
|
||||
`
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import * as inject from './inject'
|
||||
import * as astRewriter from './ast-rewriter'
|
||||
import * as regexRewriter from './regex-rewriter'
|
||||
import type { CypressWantsInjection } from '../../types'
|
||||
import type { AutomationCookie } from '@packages/server/lib/automation/cookies'
|
||||
|
||||
export type SecurityOpts = {
|
||||
isHtml?: boolean
|
||||
@@ -16,23 +17,36 @@ export type InjectionOpts = {
|
||||
domainName: string
|
||||
wantsInjection: CypressWantsInjection
|
||||
wantsSecurityRemoved: any
|
||||
simulatedCookies: AutomationCookie[]
|
||||
}
|
||||
|
||||
const doctypeRe = /(<\!doctype.*?>)/i
|
||||
const headRe = /(<head(?!er).*?>)/i
|
||||
const bodyRe = /(<body.*?>)/i
|
||||
const htmlRe = /(<html.*?>)/i
|
||||
const doctypeRe = /<\!doctype.*?>/i
|
||||
const headRe = /<head(?!er).*?>/i
|
||||
const bodyRe = /<body.*?>/i
|
||||
const htmlRe = /<html.*?>/i
|
||||
|
||||
function getRewriter (useAstSourceRewriting: boolean) {
|
||||
return useAstSourceRewriting ? astRewriter : regexRewriter
|
||||
}
|
||||
|
||||
function getHtmlToInject ({ domainName, wantsInjection, modifyObstructiveThirdPartyCode, modifyObstructiveCode }: InjectionOpts & SecurityOpts) {
|
||||
function getHtmlToInject (opts: InjectionOpts & SecurityOpts) {
|
||||
const {
|
||||
domainName,
|
||||
wantsInjection,
|
||||
modifyObstructiveThirdPartyCode,
|
||||
modifyObstructiveCode,
|
||||
simulatedCookies,
|
||||
} = opts
|
||||
|
||||
switch (wantsInjection) {
|
||||
case 'full':
|
||||
return inject.full(domainName)
|
||||
case 'fullCrossOrigin':
|
||||
return inject.fullCrossOrigin(domainName, { modifyObstructiveThirdPartyCode, modifyObstructiveCode })
|
||||
return inject.fullCrossOrigin(domainName, {
|
||||
modifyObstructiveThirdPartyCode,
|
||||
modifyObstructiveCode,
|
||||
simulatedCookies,
|
||||
})
|
||||
case 'partial':
|
||||
return inject.partial(domainName)
|
||||
default:
|
||||
@@ -40,11 +54,19 @@ function getHtmlToInject ({ domainName, wantsInjection, modifyObstructiveThirdPa
|
||||
}
|
||||
}
|
||||
|
||||
export async function html (html: string, opts: SecurityOpts & InjectionOpts) {
|
||||
const replace = (re, str) => {
|
||||
return html.replace(re, str)
|
||||
}
|
||||
const insertBefore = (originalString, match, stringToInsert) => {
|
||||
const index = match.index || 0
|
||||
|
||||
return `${originalString.slice(0, index)}${stringToInsert} ${originalString.slice(index)}`
|
||||
}
|
||||
|
||||
const insertAfter = (originalString, match, stringToInsert) => {
|
||||
const index = (match.index || 0) + match[0].length
|
||||
|
||||
return `${originalString.slice(0, index)} ${stringToInsert}${originalString.slice(index)}`
|
||||
}
|
||||
|
||||
export async function html (html: string, opts: SecurityOpts & InjectionOpts) {
|
||||
const htmlToInject = await Promise.resolve(getHtmlToInject(opts))
|
||||
|
||||
// strip clickjacking and framebusting
|
||||
@@ -58,23 +80,31 @@ export async function html (html: string, opts: SecurityOpts & InjectionOpts) {
|
||||
}
|
||||
|
||||
// TODO: move this into regex-rewriting and have ast-rewriting handle this in its own way
|
||||
switch (false) {
|
||||
case !headRe.test(html):
|
||||
return replace(headRe, `$1 ${htmlToInject}`)
|
||||
|
||||
case !bodyRe.test(html):
|
||||
return replace(bodyRe, `<head> ${htmlToInject} </head> $1`)
|
||||
const headMatch = html.match(headRe)
|
||||
|
||||
case !htmlRe.test(html):
|
||||
return replace(htmlRe, `$1 <head> ${htmlToInject} </head>`)
|
||||
|
||||
case !doctypeRe.test(html):
|
||||
// if only <!DOCTYPE> content, inject <head> after doctype
|
||||
return `${html}<head> ${htmlToInject} </head>`
|
||||
|
||||
default:
|
||||
return `<head> ${htmlToInject} </head>${html}`
|
||||
if (headMatch) {
|
||||
return insertAfter(html, headMatch, htmlToInject)
|
||||
}
|
||||
|
||||
const bodyMatch = html.match(bodyRe)
|
||||
|
||||
if (bodyMatch) {
|
||||
return insertBefore(html, bodyMatch, `<head> ${htmlToInject} </head>`)
|
||||
}
|
||||
|
||||
const htmlMatch = html.match(htmlRe)
|
||||
|
||||
if (htmlMatch) {
|
||||
return insertAfter(html, htmlMatch, `<head> ${htmlToInject} </head>`)
|
||||
}
|
||||
|
||||
// if only <!DOCTYPE> content, inject <head> after doctype
|
||||
if (doctypeRe.test(html)) {
|
||||
return `${html}<head> ${htmlToInject} </head>`
|
||||
}
|
||||
|
||||
return `<head> ${htmlToInject} </head>${html}`
|
||||
}
|
||||
|
||||
export function security (opts: SecurityOpts) {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { NetworkProxy } from '../../'
|
||||
import { NetworkProxy, RequestResourceType } from '../../'
|
||||
import {
|
||||
netStubbingState as _netStubbingState,
|
||||
NetStubbingState,
|
||||
@@ -12,6 +12,7 @@ import supertest from 'supertest'
|
||||
import { allowDestroy } from '@packages/network'
|
||||
import { EventEmitter } from 'events'
|
||||
import { RemoteStates } from '@packages/server/lib/remote_states'
|
||||
import { CookieJar } from '@packages/server/lib/util/cookies'
|
||||
|
||||
const Request = require('@packages/server/lib/request')
|
||||
const getFixture = async () => {}
|
||||
@@ -39,12 +40,22 @@ context('network stubbing', () => {
|
||||
netStubbingState,
|
||||
config,
|
||||
middleware: defaultMiddleware,
|
||||
getCurrentBrowser: () => ({ family: 'chromium' }),
|
||||
getCookieJar: () => new CookieJar(),
|
||||
remoteStates,
|
||||
getFileServerToken: () => 'fake-token',
|
||||
request: new Request(),
|
||||
getRenderedHTMLOrigins: () => ({}),
|
||||
serverBus: new EventEmitter(),
|
||||
resourceTypeAndCredentialManager: {
|
||||
get (url: string, optionalResourceType?: RequestResourceType) {
|
||||
return {
|
||||
resourceType: 'xhr',
|
||||
credentialStatus: 'same-origin',
|
||||
}
|
||||
},
|
||||
set () {},
|
||||
clear () {},
|
||||
},
|
||||
})
|
||||
|
||||
app.use((req, res, next) => {
|
||||
|
||||
@@ -687,16 +687,19 @@ describe('http/response-middleware', function () {
|
||||
expect(appendStub).to.be.calledWith('Set-Cookie', 'cookie=value')
|
||||
})
|
||||
|
||||
const getCookieJarStub = () => {
|
||||
return {
|
||||
getAllCookies: sinon.stub().returns([{ key: 'cookie', value: 'value' }]),
|
||||
getCookies: sinon.stub().returns([]),
|
||||
setCookie: sinon.stub(),
|
||||
}
|
||||
}
|
||||
|
||||
describe('same-origin', () => {
|
||||
['same-origin', 'include'].forEach((credentialLevel) => {
|
||||
it(`sets first-party cookie context in the jar when simulating top if credentials included with fetch with credential ${credentialLevel}`, async function () {
|
||||
const appendStub = sinon.stub()
|
||||
|
||||
const cookieJar = {
|
||||
getAllCookies: () => [{ key: 'cookie', value: 'value' }],
|
||||
setCookie: sinon.stub(),
|
||||
}
|
||||
|
||||
const cookieJar = getCookieJarStub()
|
||||
const ctx = prepareContext({
|
||||
cookieJar,
|
||||
res: {
|
||||
@@ -751,12 +754,7 @@ describe('http/response-middleware', function () {
|
||||
;[true, false].forEach((credentialLevel) => {
|
||||
it(`sets first-party cookie context in the jar when simulating top if withCredentials ${credentialLevel} with xhr`, async function () {
|
||||
const appendStub = sinon.stub()
|
||||
|
||||
const cookieJar = {
|
||||
getAllCookies: () => [{ key: 'cookie', value: 'value' }],
|
||||
setCookie: sinon.stub(),
|
||||
}
|
||||
|
||||
const cookieJar = getCookieJarStub()
|
||||
const ctx = prepareContext({
|
||||
cookieJar,
|
||||
res: {
|
||||
@@ -810,12 +808,7 @@ describe('http/response-middleware', function () {
|
||||
|
||||
it(`sets no cookies if fetch level is omit`, async function () {
|
||||
const appendStub = sinon.stub()
|
||||
|
||||
const cookieJar = {
|
||||
getAllCookies: () => [{ key: 'cookie', value: 'value' }],
|
||||
setCookie: sinon.stub(),
|
||||
}
|
||||
|
||||
const cookieJar = getCookieJarStub()
|
||||
const ctx = prepareContext({
|
||||
cookieJar,
|
||||
res: {
|
||||
@@ -871,12 +864,7 @@ describe('http/response-middleware', function () {
|
||||
describe('same-site', () => {
|
||||
it('sets first-party cookie context in the jar when simulating top if credentials included with fetch via include', async function () {
|
||||
const appendStub = sinon.stub()
|
||||
|
||||
const cookieJar = {
|
||||
getAllCookies: () => [{ key: 'cookie', value: 'value' }],
|
||||
setCookie: sinon.stub(),
|
||||
}
|
||||
|
||||
const cookieJar = getCookieJarStub()
|
||||
const ctx = prepareContext({
|
||||
cookieJar,
|
||||
res: {
|
||||
@@ -929,12 +917,7 @@ describe('http/response-middleware', function () {
|
||||
|
||||
it('sets first-party cookie context in the jar when simulating top if credentials true with xhr', async function () {
|
||||
const appendStub = sinon.stub()
|
||||
|
||||
const cookieJar = {
|
||||
getAllCookies: () => [{ key: 'cookie', value: 'value' }],
|
||||
setCookie: sinon.stub(),
|
||||
}
|
||||
|
||||
const cookieJar = getCookieJarStub()
|
||||
const ctx = prepareContext({
|
||||
cookieJar,
|
||||
res: {
|
||||
@@ -988,12 +971,7 @@ describe('http/response-middleware', function () {
|
||||
;['same-origin', 'omit'].forEach((credentialLevel) => {
|
||||
it(`sets no cookies if fetch level is ${credentialLevel}`, async function () {
|
||||
const appendStub = sinon.stub()
|
||||
|
||||
const cookieJar = {
|
||||
getAllCookies: () => [{ key: 'cookie', value: 'value' }],
|
||||
setCookie: sinon.stub(),
|
||||
}
|
||||
|
||||
const cookieJar = getCookieJarStub()
|
||||
const ctx = prepareContext({
|
||||
cookieJar,
|
||||
res: {
|
||||
@@ -1032,12 +1010,7 @@ describe('http/response-middleware', function () {
|
||||
describe('cross-site', () => {
|
||||
it('sets third-party cookie context in the jar when simulating top if credentials included with fetch', async function () {
|
||||
const appendStub = sinon.stub()
|
||||
|
||||
const cookieJar = {
|
||||
getAllCookies: () => [{ key: 'cookie', value: 'value' }],
|
||||
setCookie: sinon.stub(),
|
||||
}
|
||||
|
||||
const cookieJar = getCookieJarStub()
|
||||
const ctx = prepareContext({
|
||||
cookieJar,
|
||||
res: {
|
||||
@@ -1088,12 +1061,7 @@ describe('http/response-middleware', function () {
|
||||
;['same-origin', 'omit'].forEach((credentialLevel) => {
|
||||
it(`does NOT set third-party cookie context in the jar when simulating top if credentials ${credentialLevel} with fetch`, async function () {
|
||||
const appendStub = sinon.stub()
|
||||
|
||||
const cookieJar = {
|
||||
getAllCookies: () => [{ key: 'cookie', value: 'value' }],
|
||||
setCookie: sinon.stub(),
|
||||
}
|
||||
|
||||
const cookieJar = getCookieJarStub()
|
||||
const ctx = prepareContext({
|
||||
cookieJar,
|
||||
res: {
|
||||
@@ -1127,12 +1095,7 @@ describe('http/response-middleware', function () {
|
||||
|
||||
it('sets third-party cookie context in the jar when simulating top if withCredentials true with xhr', async function () {
|
||||
const appendStub = sinon.stub()
|
||||
|
||||
const cookieJar = {
|
||||
getAllCookies: () => [{ key: 'cookie', value: 'value' }],
|
||||
setCookie: sinon.stub(),
|
||||
}
|
||||
|
||||
const cookieJar = getCookieJarStub()
|
||||
const ctx = prepareContext({
|
||||
cookieJar,
|
||||
res: {
|
||||
@@ -1182,12 +1145,7 @@ describe('http/response-middleware', function () {
|
||||
|
||||
it('does not set third-party cookie context in the jar when simulating top if withCredentials false with xhr', async function () {
|
||||
const appendStub = sinon.stub()
|
||||
|
||||
const cookieJar = {
|
||||
getAllCookies: () => [{ key: 'cookie', value: 'value' }],
|
||||
setCookie: sinon.stub(),
|
||||
}
|
||||
|
||||
const cookieJar = getCookieJarStub()
|
||||
const ctx = prepareContext({
|
||||
cookieJar,
|
||||
res: {
|
||||
@@ -1221,12 +1179,7 @@ describe('http/response-middleware', function () {
|
||||
|
||||
it(`does NOT set third-party cookie context in the jar if secure cookie is not enabled`, async function () {
|
||||
const appendStub = sinon.stub()
|
||||
|
||||
const cookieJar = {
|
||||
getAllCookies: () => [{ key: 'cookie', value: 'value' }],
|
||||
setCookie: sinon.stub(),
|
||||
}
|
||||
|
||||
const cookieJar = getCookieJarStub()
|
||||
const ctx = prepareContext({
|
||||
cookieJar,
|
||||
res: {
|
||||
@@ -1259,12 +1212,7 @@ describe('http/response-middleware', function () {
|
||||
|
||||
it(`allows setting cookies if request type cannot be determined, but comes from the AUT frame (likely in the case of documents or redirects)`, async function () {
|
||||
const appendStub = sinon.stub()
|
||||
|
||||
const cookieJar = {
|
||||
getAllCookies: () => [{ key: 'cookie', value: 'value' }],
|
||||
setCookie: sinon.stub(),
|
||||
}
|
||||
|
||||
const cookieJar = getCookieJarStub()
|
||||
const ctx = prepareContext({
|
||||
cookieJar,
|
||||
res: {
|
||||
@@ -1299,12 +1247,7 @@ describe('http/response-middleware', function () {
|
||||
|
||||
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 appendStub = sinon.stub()
|
||||
|
||||
const cookieJar = {
|
||||
getAllCookies: () => [{ key: 'cookie', value: 'value' }],
|
||||
setCookie: sinon.stub(),
|
||||
}
|
||||
|
||||
const cookieJar = getCookieJarStub()
|
||||
const ctx = prepareContext({
|
||||
cookieJar,
|
||||
res: {
|
||||
@@ -1332,7 +1275,7 @@ describe('http/response-middleware', function () {
|
||||
expect(appendStub).to.be.calledWith('Set-Cookie', 'cookie=value')
|
||||
})
|
||||
|
||||
it('does not send cross:origin:automation:cookies if request does not need top simulation', async () => {
|
||||
it('does not send cross:origin:cookies if request does not need top simulation', async () => {
|
||||
const { ctx } = prepareSameOriginContext()
|
||||
|
||||
await testMiddleware([MaybeCopyCookiesFromIncomingRes], ctx)
|
||||
@@ -1340,11 +1283,8 @@ describe('http/response-middleware', function () {
|
||||
expect(ctx.serverBus.emit).not.to.be.called
|
||||
})
|
||||
|
||||
it('does not send cross:origin:automation:cookies if there are no added cookies', async () => {
|
||||
const cookieJar = {
|
||||
getAllCookies: () => [{ key: 'cookie', value: 'value' }],
|
||||
}
|
||||
|
||||
it('does not send cross:origin:cookies if there are no added cookies', async () => {
|
||||
const cookieJar = getCookieJarStub()
|
||||
const ctx = prepareContext({
|
||||
cookieJar,
|
||||
incomingRes: {
|
||||
@@ -1359,16 +1299,17 @@ describe('http/response-middleware', function () {
|
||||
expect(ctx.serverBus.emit).not.to.be.called
|
||||
})
|
||||
|
||||
it('sends cross:origin:automation:cookies if there are added cookies and resolves on cross:origin:automation:cookies:received', async () => {
|
||||
const cookieJar = {
|
||||
getAllCookies: sinon.stub(),
|
||||
}
|
||||
it('sends cross:origin:cookies with origin and cookies if there are added cookies and resolves on cross:origin:cookies:received', async () => {
|
||||
const cookieJar = getCookieJarStub()
|
||||
|
||||
cookieJar.getAllCookies.onCall(0).returns([])
|
||||
cookieJar.getAllCookies.onCall(1).returns([cookieStub({ key: 'cookie', value: 'value' })])
|
||||
|
||||
const ctx = prepareContext({
|
||||
cookieJar,
|
||||
req: {
|
||||
isAUTFrame: true,
|
||||
},
|
||||
incomingRes: {
|
||||
headers: {
|
||||
'set-cookie': 'cookie=value',
|
||||
@@ -1378,13 +1319,13 @@ describe('http/response-middleware', function () {
|
||||
|
||||
// test will hang if this.next() is not called, so this also tests
|
||||
// that we move on once receiving this event
|
||||
ctx.serverBus.once.withArgs('cross:origin:automation:cookies:received').yields()
|
||||
ctx.serverBus.once.withArgs('cross:origin:cookies:received').yields()
|
||||
|
||||
await testMiddleware([MaybeCopyCookiesFromIncomingRes], ctx)
|
||||
|
||||
expect(ctx.serverBus.emit).to.be.calledWith('cross:origin:automation:cookies')
|
||||
expect(ctx.serverBus.emit).to.be.calledWith('cross:origin:cookies')
|
||||
|
||||
const cookies = ctx.serverBus.emit.withArgs('cross:origin:automation:cookies').args[0][1]
|
||||
const cookies = ctx.serverBus.emit.withArgs('cross:origin:cookies').args[0][1]
|
||||
|
||||
expect(cookies[0].name).to.equal('cookie')
|
||||
expect(cookies[0].value).to.equal('value')
|
||||
@@ -1405,6 +1346,7 @@ describe('http/response-middleware', function () {
|
||||
|
||||
const cookieJar = props.cookieJar || {
|
||||
getAllCookies: () => [],
|
||||
getCookies: () => [],
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -1496,6 +1438,7 @@ describe('http/response-middleware', function () {
|
||||
req: {
|
||||
proxiedUrl: 'http://www.foobar.com:3501/primary-origin.html',
|
||||
},
|
||||
simulatedCookies: [],
|
||||
})
|
||||
|
||||
return testMiddleware([MaybeInjectHtml], ctx)
|
||||
@@ -1511,12 +1454,15 @@ describe('http/response-middleware', function () {
|
||||
'useAstSourceRewriting': undefined,
|
||||
'wantsInjection': 'full',
|
||||
'wantsSecurityRemoved': true,
|
||||
'simulatedCookies': [],
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it('modifyObstructiveThirdPartyCode is false for primary requests', function () {
|
||||
prepareContext({})
|
||||
prepareContext({
|
||||
simulatedCookies: [],
|
||||
})
|
||||
|
||||
return testMiddleware([MaybeInjectHtml], ctx)
|
||||
.then(() => {
|
||||
@@ -1531,6 +1477,7 @@ describe('http/response-middleware', function () {
|
||||
'useAstSourceRewriting': undefined,
|
||||
'wantsInjection': 'full',
|
||||
'wantsSecurityRemoved': true,
|
||||
'simulatedCookies': [],
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1544,6 +1491,7 @@ describe('http/response-middleware', function () {
|
||||
modifyObstructiveCode: false,
|
||||
experimentalModifyObstructiveThirdPartyCode: false,
|
||||
},
|
||||
simulatedCookies: [],
|
||||
})
|
||||
|
||||
return testMiddleware([MaybeInjectHtml], ctx)
|
||||
@@ -1559,6 +1507,7 @@ describe('http/response-middleware', function () {
|
||||
'useAstSourceRewriting': undefined,
|
||||
'wantsInjection': 'full',
|
||||
'wantsSecurityRemoved': true,
|
||||
'simulatedCookies': [],
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -71,7 +71,9 @@ const handleErrorEvent = (event) => {
|
||||
window.addEventListener('error', handleErrorEvent)
|
||||
|
||||
// Apply Patches
|
||||
patchDocumentCookie(window)
|
||||
const documentCookiePatch = patchDocumentCookie(cypressConfig.simulatedCookies)
|
||||
|
||||
const Cypress = findCypress()
|
||||
|
||||
// return null to trick contentWindow into thinking
|
||||
// its not been iFramed if modifyObstructiveCode is true
|
||||
@@ -92,13 +94,28 @@ const timers = createTimers()
|
||||
|
||||
timers.wrap()
|
||||
|
||||
const Cypress = findCypress()
|
||||
const attachToCypress = (Cypress) => {
|
||||
documentCookiePatch.onCypress(Cypress)
|
||||
|
||||
// Attach these to window so cypress can call them when it attaches.
|
||||
window.cypressTimersReset = timers.reset
|
||||
window.cypressTimersPause = timers.pause
|
||||
Cypress.removeAllListeners('app:timers:reset')
|
||||
Cypress.removeAllListeners('app:timers:pause')
|
||||
|
||||
Cypress.on('app:timers:reset', timers.reset)
|
||||
Cypress.on('app:timers:pause', timers.pause)
|
||||
}
|
||||
|
||||
// if the page loaded before creating a spec bridge for it, this method will
|
||||
// be called, letting us know we can utilize window.Cypress. we can skip this
|
||||
// if we already have access to window.Cypress
|
||||
window.__attachToCypress = (asyncAttachedCypress) => {
|
||||
if (!Cypress) {
|
||||
attachToCypress(asyncAttachedCypress)
|
||||
}
|
||||
}
|
||||
|
||||
// Check for cy too to prevent a race condition for attaching.
|
||||
if (Cypress && Cypress.cy) {
|
||||
attachToCypress(Cypress)
|
||||
|
||||
Cypress.action('app:window:before:load', window)
|
||||
}
|
||||
|
||||
@@ -1,125 +0,0 @@
|
||||
import { Cookie } from 'tough-cookie'
|
||||
|
||||
// document.cookie monkey-patching
|
||||
// -------------------------------
|
||||
// We monkey-patch document.cookie when in a cross-origin injection, because
|
||||
// document.cookie runs into cross-origin restrictions when the AUT is on
|
||||
// a different origin than top. The goal is to make it act like it would
|
||||
// if the user's app was run in top.
|
||||
//
|
||||
// The general strategy is:
|
||||
// - Keep the document.cookie value (`documentCookieValue`) available so
|
||||
// the document.cookie getter can synchronously return it.
|
||||
// - Optimistically update that value when document.cookie is set, so that
|
||||
// subsequent synchronous calls to get the value will work.
|
||||
// - On an interval, get the browser's cookies for the given domain, so that
|
||||
// updates to the cookie jar (via http requests, cy.setCookie, etc) are
|
||||
// reflected in the document.cookie value.
|
||||
export const patchDocumentCookie = (win) => {
|
||||
const getCookiesFromCypress = () => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const handler = (event) => {
|
||||
if (event.data.event === 'cross:origin:aut:get:cookie') {
|
||||
window.removeEventListener('message', handler)
|
||||
resolve(event.data.cookies)
|
||||
}
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
window.removeEventListener('message', handler)
|
||||
reject()
|
||||
}, 1000)
|
||||
|
||||
window.addEventListener('message', handler)
|
||||
|
||||
window.top.postMessage({ event: 'cross:origin:aut:get:cookie', data: { href: window.location.href } }, '*')
|
||||
})
|
||||
}
|
||||
|
||||
// The interval value is arbitrary; it shouldn't be too often, but needs to
|
||||
// be fairly frequent so that the local value is kept as up-to-date as
|
||||
// possible. It's possible there could be a race condition where
|
||||
// document.cookie returns an out-of-date value, but there's not really a
|
||||
// way around that since it's a synchronous API and we can only get the
|
||||
// browser's true cookie values asynchronously.
|
||||
const syncCookieValues = () => {
|
||||
return setInterval(async () => {
|
||||
try {
|
||||
// If Cypress is defined on the window, that means we have a spec bridge and we should use that to set cookies. If not we have to delegate to the primary cypress instance.
|
||||
const cookies = window.Cypress ? await window.Cypress.automation('get:cookies', { domain: window.Cypress.Location.create(win.location.href).domain }) : await getCookiesFromCypress()
|
||||
|
||||
const cookiesString = (cookies || []).map((c) => `${c.name}=${c.value}`).join('; ')
|
||||
|
||||
documentCookieValue = cookiesString
|
||||
} catch (err) {
|
||||
// unlikely there will be errors, but ignore them in any case, since
|
||||
// they're not user-actionable
|
||||
}
|
||||
}, 250)
|
||||
}
|
||||
|
||||
let cookieSyncIntervalId = syncCookieValues()
|
||||
const setAutomationCookie = (cookie) => {
|
||||
// If Cypress is defined on the window, that means we have a spec bridge and we should use that to set cookies. If not we have to delegate to the primary cypress instance.
|
||||
if (window.Cypress) {
|
||||
const { superDomain } = window.Cypress.Location.create(win.location.href)
|
||||
const automationCookie = window.Cypress.Cookies.toughCookieToAutomationCookie(window.Cypress.Cookies.parse(cookie), superDomain)
|
||||
|
||||
window.Cypress.automation('set:cookie', automationCookie)
|
||||
.then(() => {
|
||||
// Resume syncing once we've gotten confirmation that cookies have been set.
|
||||
cookieSyncIntervalId = syncCookieValues()
|
||||
})
|
||||
.catch(() => {
|
||||
// unlikely there will be errors, but ignore them in any case, since
|
||||
// they're not user-actionable
|
||||
})
|
||||
} else {
|
||||
const handler = (event) => {
|
||||
if (event.data.event === 'cross:origin:aut:set:cookie') {
|
||||
window.removeEventListener('message', handler)
|
||||
// Resume syncing once we've gotten confirmation that cookies have been set.
|
||||
cookieSyncIntervalId = syncCookieValues()
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener('message', handler)
|
||||
|
||||
window.top.postMessage({ event: 'cross:origin:aut:set:cookie', data: { cookie, href: window.location.href } }, '*')
|
||||
}
|
||||
}
|
||||
let documentCookieValue = ''
|
||||
|
||||
Object.defineProperty(win.document, 'cookie', {
|
||||
get () {
|
||||
return documentCookieValue
|
||||
},
|
||||
|
||||
set (newValue) {
|
||||
const cookie = Cookie.parse(newValue)
|
||||
|
||||
// If cookie is undefined, it was invalid and couldn't be parsed
|
||||
if (!cookie) return documentCookieValue
|
||||
|
||||
const cookieString = `${cookie.key}=${cookie.value}`
|
||||
|
||||
clearInterval(cookieSyncIntervalId)
|
||||
|
||||
// New cookies get prepended to existing cookies
|
||||
documentCookieValue = documentCookieValue.length
|
||||
? `${cookieString}; ${documentCookieValue}`
|
||||
: cookieString
|
||||
|
||||
setAutomationCookie(newValue)
|
||||
|
||||
return documentCookieValue
|
||||
},
|
||||
})
|
||||
|
||||
const onUnload = () => {
|
||||
win.removeEventListener('unload', onUnload)
|
||||
clearInterval(cookieSyncIntervalId)
|
||||
}
|
||||
|
||||
win.addEventListener('unload', onUnload)
|
||||
}
|
||||
127
packages/runner/injection/patches/cookies.ts
Normal file
127
packages/runner/injection/patches/cookies.ts
Normal file
@@ -0,0 +1,127 @@
|
||||
import {
|
||||
CookieJar,
|
||||
toughCookieToAutomationCookie,
|
||||
automationCookieToToughCookie,
|
||||
} from '@packages/server/lib/util/cookies'
|
||||
import { Cookie as ToughCookie } from 'tough-cookie'
|
||||
import type { AutomationCookie } from '@packages/server/lib/automation/cookies'
|
||||
|
||||
const parseDocumentCookieString = (documentCookieString: string): AutomationCookie[] => {
|
||||
if (!documentCookieString || !documentCookieString.trim().length) return []
|
||||
|
||||
return documentCookieString.split(';').map((cookieString) => {
|
||||
const [name, value] = cookieString.split('=')
|
||||
|
||||
return {
|
||||
name: name.trim(),
|
||||
value: value.trim(),
|
||||
domain: location.hostname,
|
||||
expiry: null,
|
||||
httpOnly: false,
|
||||
maxAge: null,
|
||||
path: null,
|
||||
sameSite: 'lax',
|
||||
secure: false,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const sendCookieToServer = (cookie: AutomationCookie) => {
|
||||
window.top!.postMessage({
|
||||
event: 'cross:origin:aut:set:cookie',
|
||||
data: {
|
||||
cookie,
|
||||
url: location.href,
|
||||
// url will always match the cookie domain, so strict context tells
|
||||
// tough-cookie to allow it to be set
|
||||
sameSiteContext: 'strict',
|
||||
},
|
||||
}, '*')
|
||||
}
|
||||
|
||||
// document.cookie monkey-patching
|
||||
// -------------------------------
|
||||
// We monkey-patch document.cookie when in a cross-origin injection, because
|
||||
// document.cookie runs into cross-origin restrictions when the AUT is on
|
||||
// a different origin than top. The goal is to make it act like it would
|
||||
// if the user's app was run in top.
|
||||
export const patchDocumentCookie = (requestCookies: AutomationCookie[]) => {
|
||||
const url = location.href
|
||||
const domain = location.hostname
|
||||
const cookieJar = new CookieJar()
|
||||
const existingCookies = parseDocumentCookieString(document.cookie)
|
||||
|
||||
const getDocumentCookieValue = () => {
|
||||
return cookieJar.getCookies(url, undefined).map((cookie: ToughCookie) => {
|
||||
return `${cookie.key}=${cookie.value}`
|
||||
}).join('; ')
|
||||
}
|
||||
|
||||
const addCookies = (cookies: AutomationCookie[]) => {
|
||||
cookies.forEach((cookie) => {
|
||||
cookieJar.setCookie(automationCookieToToughCookie(cookie, domain), url, undefined)
|
||||
})
|
||||
}
|
||||
|
||||
// requestCookies are ones included with the page request that's now being
|
||||
// injected into. they're captured by the proxy and included statically in
|
||||
// the injection so they can be added here and available before page load
|
||||
addCookies(existingCookies.concat(requestCookies))
|
||||
|
||||
Object.defineProperty(window.document, 'cookie', {
|
||||
get () {
|
||||
return getDocumentCookieValue()
|
||||
},
|
||||
|
||||
set (newValue: any) {
|
||||
const stringValue = `${newValue}`
|
||||
const parsedCookie = CookieJar.parse(stringValue)
|
||||
|
||||
// if result is undefined, it was invalid and couldn't be parsed
|
||||
if (!parsedCookie) return getDocumentCookieValue()
|
||||
|
||||
// we should be able to pass in parsedCookie here instead of the string
|
||||
// value, but tough-cookie doesn't recognize it using an instanceof
|
||||
// check and throws an error. because we can't, we have to massage
|
||||
// some of the properties below to be correct
|
||||
const cookie = cookieJar.setCookie(stringValue, url, undefined)!
|
||||
|
||||
cookie.sameSite = parsedCookie.sameSite
|
||||
|
||||
if (!parsedCookie.path) {
|
||||
cookie.path = '/'
|
||||
}
|
||||
|
||||
// send the cookie to the server so it can be set in the browser via
|
||||
// automation and in our server-side cookie jar so it's available
|
||||
// to subsequent injections
|
||||
sendCookieToServer(toughCookieToAutomationCookie(cookie, domain))
|
||||
|
||||
return getDocumentCookieValue()
|
||||
},
|
||||
})
|
||||
|
||||
const reset = () => {
|
||||
cookieJar.removeAllCookies()
|
||||
}
|
||||
|
||||
const bindCypressListeners = (Cypress: Cypress.Cypress) => {
|
||||
Cypress.on('test:before:run', reset)
|
||||
|
||||
// the following listeners are called from Cypress cookie commands, so that
|
||||
// the document.cookie value is updated optimistically
|
||||
Cypress.on('set:cookie', (cookie: AutomationCookie) => {
|
||||
cookieJar.setCookie(automationCookieToToughCookie(cookie, domain), url, undefined)
|
||||
})
|
||||
|
||||
Cypress.on('clear:cookie', (name: string) => {
|
||||
cookieJar.removeCookie({ name, domain })
|
||||
})
|
||||
|
||||
Cypress.on('clear:cookies', reset)
|
||||
}
|
||||
|
||||
return {
|
||||
onCypress: bindCypressListeners,
|
||||
}
|
||||
}
|
||||
@@ -162,7 +162,6 @@ export class ProjectBase<TServer extends Server> extends EE {
|
||||
|
||||
const [port, warning] = await this._server.open(cfg, {
|
||||
getCurrentBrowser: () => this.browser,
|
||||
getAutomation: () => this.automation,
|
||||
getSpec: () => this.spec,
|
||||
exit: this.options.args?.exit,
|
||||
onError: this.options.onError,
|
||||
|
||||
@@ -33,7 +33,6 @@ import type { FoundSpec } from '@packages/types'
|
||||
import type { Server as WebSocketServer } from 'ws'
|
||||
import { RemoteStates } from './remote_states'
|
||||
import { cookieJar } from './util/cookies'
|
||||
import type { Automation } from './automation/automation'
|
||||
import type { AutomationCookie } from './automation/cookies'
|
||||
import { resourceTypeAndCredentialManager, ResourceTypeAndCredentialManager } from './util/resourceTypeAndCredentialManager'
|
||||
|
||||
@@ -103,7 +102,6 @@ export interface OpenServerOptions {
|
||||
onWarning: any
|
||||
exit?: boolean
|
||||
getCurrentBrowser: () => Browser
|
||||
getAutomation: () => Automation
|
||||
getSpec: () => FoundSpec | null
|
||||
shouldCorrelatePreRequests: () => boolean
|
||||
}
|
||||
@@ -178,12 +176,12 @@ export abstract class ServerBase<TSocket extends SocketE2E | SocketCt> {
|
||||
}
|
||||
|
||||
setupCrossOriginRequestHandling () {
|
||||
this._eventBus.on('cross:origin:automation:cookies', (cookies: AutomationCookie[]) => {
|
||||
this.socket.localBus.once('cross:origin:automation:cookies:received', () => {
|
||||
this._eventBus.emit('cross:origin:automation:cookies:received')
|
||||
this._eventBus.on('cross:origin:cookies', (cookies: AutomationCookie[]) => {
|
||||
this.socket.localBus.once('cross:origin:cookies:received', () => {
|
||||
this._eventBus.emit('cross:origin:cookies:received')
|
||||
})
|
||||
|
||||
this.socket.toDriver('cross:origin:automation:cookies', cookies)
|
||||
this.socket.toDriver('cross:origin:cookies', cookies)
|
||||
})
|
||||
|
||||
this.socket.localBus.on('request:sent:with:credentials', this.resourceTypeAndCredentialManager.set)
|
||||
@@ -197,7 +195,6 @@ export abstract class ServerBase<TSocket extends SocketE2E | SocketCt> {
|
||||
|
||||
open (config: Cfg, {
|
||||
getSpec,
|
||||
getAutomation,
|
||||
getCurrentBrowser,
|
||||
onError,
|
||||
onWarning,
|
||||
@@ -225,7 +222,7 @@ export abstract class ServerBase<TSocket extends SocketE2E | SocketCt> {
|
||||
clientCertificates.loadClientCertificateConfig(config)
|
||||
|
||||
this.createNetworkProxy({
|
||||
config, getAutomation,
|
||||
config,
|
||||
remoteStates: this._remoteStates,
|
||||
resourceTypeAndCredentialManager: this.resourceTypeAndCredentialManager,
|
||||
shouldCorrelatePreRequests,
|
||||
@@ -321,7 +318,7 @@ export abstract class ServerBase<TSocket extends SocketE2E | SocketCt> {
|
||||
return e
|
||||
}
|
||||
|
||||
createNetworkProxy ({ config, getAutomation, remoteStates, resourceTypeAndCredentialManager, shouldCorrelatePreRequests }) {
|
||||
createNetworkProxy ({ config, remoteStates, resourceTypeAndCredentialManager, shouldCorrelatePreRequests }) {
|
||||
const getFileServerToken = () => {
|
||||
return this._fileServer.token
|
||||
}
|
||||
@@ -331,7 +328,6 @@ export abstract class ServerBase<TSocket extends SocketE2E | SocketCt> {
|
||||
this._networkProxy = new NetworkProxy({
|
||||
config,
|
||||
shouldCorrelatePreRequests,
|
||||
getAutomation,
|
||||
remoteStates,
|
||||
getFileServerToken,
|
||||
getCookieJar: () => cookieJar,
|
||||
|
||||
@@ -20,13 +20,14 @@ import { openFile, OpenFileDetails } from './util/file-opener'
|
||||
import open from './util/open'
|
||||
import type { DestroyableHttpServer } from './util/server_destroy'
|
||||
import * as session from './session'
|
||||
import { cookieJar } from './util/cookies'
|
||||
import { AutomationCookie, cookieJar, SameSiteContext, automationCookieToToughCookie } from './util/cookies'
|
||||
import runEvents from './plugins/run_events'
|
||||
|
||||
// eslint-disable-next-line no-duplicate-imports
|
||||
import type { Socket } from '@packages/socket'
|
||||
|
||||
import type { RunState, CachedTestState } from '@packages/types'
|
||||
import { cors } from '@packages/network'
|
||||
|
||||
type StartListeningCallbacks = {
|
||||
onSocketConnection: (socket: any) => void
|
||||
@@ -392,6 +393,12 @@ export class SocketBase {
|
||||
})
|
||||
})
|
||||
|
||||
const setCrossOriginCookie = ({ cookie, url, sameSiteContext }: { cookie: AutomationCookie, url: string, sameSiteContext: SameSiteContext }) => {
|
||||
const domain = cors.getOrigin(url)
|
||||
|
||||
cookieJar.setCookie(automationCookieToToughCookie(cookie, domain), url, sameSiteContext)
|
||||
}
|
||||
|
||||
socket.on('backend:request', (eventName: string, ...args) => {
|
||||
// cb is always the last argument
|
||||
const cb = args.pop()
|
||||
@@ -459,8 +466,10 @@ export class SocketBase {
|
||||
return options.getRenderedHTMLOrigins()
|
||||
case 'reset:rendered:html:origins':
|
||||
return resetRenderedHTMLOrigins()
|
||||
case 'cross:origin:automation:cookies:received':
|
||||
return this.localBus.emit('cross:origin:automation:cookies:received')
|
||||
case 'cross:origin:cookies:received':
|
||||
return this.localBus.emit('cross:origin:cookies:received')
|
||||
case 'cross:origin:set:cookie':
|
||||
return setCrossOriginCookie(args[0])
|
||||
case 'request:sent:with:credentials':
|
||||
return this.localBus.emit('request:sent:with:credentials', args[0])
|
||||
default:
|
||||
|
||||
@@ -9,6 +9,8 @@ interface CookieData {
|
||||
path?: string
|
||||
}
|
||||
|
||||
export type SameSiteContext = 'strict' | 'lax' | 'none' | undefined
|
||||
|
||||
export const toughCookieToAutomationCookie = (toughCookie: Cookie, defaultDomain: string): AutomationCookie => {
|
||||
const expiry = toughCookie.expiryTime()
|
||||
|
||||
@@ -25,6 +27,20 @@ export const toughCookieToAutomationCookie = (toughCookie: Cookie, defaultDomain
|
||||
}
|
||||
}
|
||||
|
||||
export const automationCookieToToughCookie = (automationCookie: AutomationCookie, defaultDomain: string): Cookie => {
|
||||
return new Cookie({
|
||||
domain: automationCookie.domain || defaultDomain,
|
||||
expires: automationCookie.expiry != null && isFinite(automationCookie.expiry) ? new Date(automationCookie.expiry * 1000) : undefined,
|
||||
httpOnly: automationCookie.httpOnly,
|
||||
maxAge: automationCookie.maxAge || 'Infinity',
|
||||
key: automationCookie.name,
|
||||
path: automationCookie.path || undefined,
|
||||
sameSite: automationCookie.sameSite === 'no_restriction' ? 'none' : automationCookie.sameSite,
|
||||
secure: automationCookie.secure,
|
||||
value: automationCookie.value,
|
||||
})
|
||||
}
|
||||
|
||||
const sameSiteNoneRe = /; +samesite=(?:'none'|"none"|none)/i
|
||||
|
||||
/**
|
||||
@@ -57,7 +73,7 @@ export class CookieJar {
|
||||
this._cookieJar = new ToughCookieJar(undefined, { allowSpecialUseDomain: true })
|
||||
}
|
||||
|
||||
getCookies (url, sameSiteContext) {
|
||||
getCookies (url: string, sameSiteContext: SameSiteContext = undefined) {
|
||||
// @ts-ignore
|
||||
return this._cookieJar.getCookiesSync(url, { sameSiteContext })
|
||||
}
|
||||
@@ -75,9 +91,9 @@ export class CookieJar {
|
||||
return cookies
|
||||
}
|
||||
|
||||
setCookie (cookie: string | Cookie, url: string, sameSiteContext: 'strict' | 'lax' | 'none') {
|
||||
setCookie (cookie: string | Cookie, url: string, sameSiteContext: SameSiteContext) {
|
||||
// @ts-ignore
|
||||
this._cookieJar.setCookieSync(cookie, url, { sameSiteContext })
|
||||
return this._cookieJar.setCookieSync(cookie, url, { sameSiteContext })
|
||||
}
|
||||
|
||||
removeCookie (cookieData: CookieData) {
|
||||
|
||||
Reference in New Issue
Block a user