Skip to content

Commit

Permalink
feat: add attaching cookies to response logic w/ tests
Browse files Browse the repository at this point in the history
  • Loading branch information
AtofStryker committed Sep 21, 2022
1 parent b5f12ff commit e4579be
Show file tree
Hide file tree
Showing 3 changed files with 759 additions and 42 deletions.
46 changes: 17 additions & 29 deletions packages/proxy/lib/http/response-middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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, HttpMiddlewareThis } from '.'
import type { HttpMiddleware } from '.'
import iconv from 'iconv-lite'
import type { IncomingMessage, IncomingHttpHeaders } from 'http'
import { InterceptResponse } from '@packages/net-stubbing'
Expand All @@ -13,6 +13,7 @@ import * as rewriter from './util/rewriter'
import zlib from 'zlib'
import { URL } from 'url'
import { CookiesHelper } from './util/cookies'
import { doesTopNeedToBeSimulated } from './util/top-simulation'

interface ResponseMiddlewareProps {
/**
Expand Down Expand Up @@ -370,48 +371,33 @@ const MaybePreventCaching: ResponseMiddleware = function () {
this.next()
}

const checkIfNeedsCrossOriginHandling = (ctx: HttpMiddlewareThis<ResponseMiddlewareProps>) => {
const currentAUTUrl = ctx.getAUTUrl()

// A cookie needs cross origin handling if the request itself is
// cross-origin or the origins between requests don't match,
// since the browser won't set them in that case and if it's
// secondary-origin -> primary-origin, we don't recognize the request as cross-origin
return (
ctx.config.experimentalSessionAndOrigin
&& (
(currentAUTUrl && !cors.urlOriginsMatch(currentAUTUrl, ctx.req.proxiedUrl))
|| !ctx.remoteStates.isPrimaryOrigin(ctx.req.proxiedUrl)
)
)
}

const CopyCookiesFromIncomingRes: ResponseMiddleware = async function () {
const MaybeCopyCookiesFromIncomingRes: ResponseMiddleware = async function () {
const cookies: string | string[] | undefined = this.incomingRes.headers['set-cookie']

if (!cookies || !cookies.length) {
return this.next()
}

// Cross-origin Cookie Handling
// Simulated Top Cookie Handling
// ---------------------------
// - We capture cookies sent by responses and add them to our own server-side
// tough-cookie cookie jar. All request cookies are captured, since any
// future request could be cross-origin even if the response that sets them
// future request could be cross-origin in the context of top, even if the response that sets them
// is not.
// - If we sent the cookie header, it may fail to be set by the browser
// (in most cases). However, we cannot determine all the cases in which Set-Cookie
// will currently fail, and currently is best to set optimistically until #23551 is addressed.
// will currently fail. We try to address this in our tough cookie jar
// by only setting cookies that would otherwise work in the browser if the AUT url was top
// - We also set the cookies through automation so they are available in the
// browser via document.cookie and via Cypress cookie APIs
// (e.g. cy.getCookie). This is only done for cross-origin responses, since
// non-cross-origin responses will be successfully set in the browser
// automatically.
// (e.g. cy.getCookie). This is only done when the AUT url and top do not match responses,
// since AUT and Top being same origin will be successfully set in the browser
// automatically as expected.
// - In the request middleware, we retrieve the cookies for a given URL
// and attach them to the request, like the browser normally would.
// tough-cookie handles retrieving the correct cookies based on domain,
// path, etc. It also removes cookies from the cookie jar if they've expired.
const needsCrossOriginHandling = checkIfNeedsCrossOriginHandling(this)
const doesTopNeedSimulating = doesTopNeedToBeSimulated(this)

const appendCookie = (cookie: string) => {
// always call 'Set-Cookie' in the browser as cross origin or same site requests
Expand All @@ -425,7 +411,7 @@ const CopyCookiesFromIncomingRes: ResponseMiddleware = async function () {
}
}

if (!this.config.experimentalSessionAndOrigin) {
if (!this.config.experimentalSessionAndOrigin || !doesTopNeedSimulating) {
([] as string[]).concat(cookies).forEach((cookie) => {
appendCookie(cookie)
})
Expand All @@ -440,7 +426,9 @@ const CopyCookiesFromIncomingRes: ResponseMiddleware = async function () {
request: {
url: this.req.proxiedUrl,
isAUTFrame: this.req.isAUTFrame,
needsCrossOriginHandling,
doesTopNeedSimulating,
resourceType: this.req.requestedWith,
credentialLevel: this.req.credentialsLevel,
},
})

Expand All @@ -454,7 +442,7 @@ const CopyCookiesFromIncomingRes: ResponseMiddleware = async function () {

const addedCookies = await cookiesHelper.getAddedCookies()

if (!needsCrossOriginHandling || !addedCookies.length) {
if (!addedCookies.length) {
return this.next()
}

Expand Down Expand Up @@ -599,7 +587,7 @@ export default {
OmitProblematicHeaders,
MaybePreventCaching,
MaybeStripDocumentDomainFeaturePolicy,
CopyCookiesFromIncomingRes,
MaybeCopyCookiesFromIncomingRes,
MaybeSendRedirectToClient,
CopyResponseStatusCode,
ClearCyInitialCookie,
Expand Down
28 changes: 25 additions & 3 deletions packages/proxy/lib/http/util/cookies.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@ type SiteContext = 'same-origin' | 'same-site' | 'cross-site'
interface RequestDetails {
url: string
isAUTFrame: boolean
needsCrossOriginHandling: boolean
doesTopNeedSimulating: boolean
resourceType?: RequestResourceType
credentialLevel?: RequestCredentialLevel
}

/**
Expand Down Expand Up @@ -162,6 +164,7 @@ export class CookiesHelper {
debug: Debug.Debugger
defaultDomain: string
sameSiteContext: 'strict' | 'lax' | 'none'
siteContext: SiteContext
previousCookies: Cookie[] = []

constructor ({ cookieJar, currentAUTUrl, request, debug }) {
Expand All @@ -170,6 +173,7 @@ export class CookiesHelper {
this.request = request
this.debug = debug
this.sameSiteContext = getSameSiteContext(currentAUTUrl, request.url, request.isAUTFrame)
this.siteContext = calculateSiteContext(this.request.url, this.currentAUTUrl || '')

const parsedRequestUrl = new URL(request.url)

Expand All @@ -180,7 +184,7 @@ export class CookiesHelper {
// this plays a part in adding cross-origin cookies to the browser via
// automation. if the request doesn't need cross-origin handling, this
// is a noop
if (!this.request.needsCrossOriginHandling) return
if (!this.request.doesTopNeedSimulating) return

this.previousCookies = this.cookieJar.getAllCookies()
}
Expand All @@ -189,7 +193,7 @@ export class CookiesHelper {
// this plays a part in adding cross-origin cookies to the browser via
// automation. if the request doesn't need cross-origin handling, this
// is a noop
if (!this.request.needsCrossOriginHandling) return []
if (!this.request.doesTopNeedSimulating) return []

const afterCookies = this.cookieJar.getAllCookies()

Expand All @@ -208,10 +212,28 @@ export class CookiesHelper {
// because Secure is required for SameSite=None. not all browsers currently
// currently enforce this, but they're heading in that direction since
// it's now the standard, so this is more future-proof
// TODO: in the future we may want to check for https, which might be tricky since localhost is considered a secure context
if (!toughCookie || (toughCookie.sameSite === 'none' && !toughCookie.secure)) {
return
}

// cross site cookies cannot set lax/strict cookies in the browser for xhr/fetch requests (but ok with navigation/document requests)
if (this.request.resourceType && this.siteContext === 'cross-site' && toughCookie.sameSite !== 'none') {
this.debug(`cannot set cookie with SameSite=${toughCookie.sameSite} when site context is ${this.siteContext}`)

return
}

// don't set the cookie in our own cookie jar if the cookie would otherwise fail being set in the browser if the AUT Url
// was actually top. This prevents cookies from being applied to our cookie jar when they shouldn't, preventing possible security implications.
const shouldSetCookieGivenSiteContext = shouldAttachAndSetCookies(this.request.url, this.currentAUTUrl, this.request.resourceType, this.request.credentialLevel, this.request.isAUTFrame)

if (!shouldSetCookieGivenSiteContext) {
this.debug(`not setting cookie for ${this.request.url} with simulated top ${ this.currentAUTUrl} for ${ this.request.resourceType}:${this.request.credentialLevel}, cookie: ${toughCookie}`)

return
}

try {
this.cookieJar.setCookie(toughCookie, this.request.url, this.sameSiteContext)
} catch (err) {
Expand Down
Loading

0 comments on commit e4579be

Please sign in to comment.