mirror of
https://github.com/cypress-io/cypress.git
synced 2026-05-25 01:49:06 -05:00
refactor(proxy): network capture and cookie state adapters
Stage 6 (6a+6b): capture/cookie driven ports and middleware delegation. - Add ProxyNetworkCaptureAdapter and ProxyCookieStateAdapter in proxy/lib/adapters - Expand ForNetworkCapture / ForCookieState and wire through NetworkPolicyCore - Delegate RecordRequest, RecordResponse, and cookie middleware through the core Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -26,6 +26,7 @@
|
||||
"@packages/config": "0.0.0-development",
|
||||
"@packages/errors": "0.0.0-development",
|
||||
"@packages/net-stubbing": "0.0.0-development",
|
||||
"@packages/network-policy": "0.0.0-development",
|
||||
"@packages/network-tools": "0.0.0-development",
|
||||
"@packages/proxy": "0.0.0-development",
|
||||
"@packages/rewriter": "0.0.0-development",
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
import type {
|
||||
CommandLogInterceptionInput,
|
||||
CommandLogInterceptionResult,
|
||||
ForCommandLog,
|
||||
} from '@packages/network-policy'
|
||||
|
||||
export type DriverCommandLogAdapterOptions = {
|
||||
logInterception: (interception: unknown, route: unknown) => CommandLogInterceptionResult
|
||||
}
|
||||
|
||||
/** Driver-side {@link ForCommandLog} adapter — delegates to {@link Cypress.ProxyLogging}. */
|
||||
export class DriverCommandLogAdapter implements ForCommandLog {
|
||||
constructor (private readonly options: DriverCommandLogAdapterOptions) {}
|
||||
|
||||
notifyIncomingRequest (_ctx: unknown): void {
|
||||
// Server-side only — driver receives incoming requests via socket events.
|
||||
}
|
||||
|
||||
logInterception (input: CommandLogInterceptionInput): CommandLogInterceptionResult {
|
||||
return this.options.logInterception(input.interception, input.route)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export { DriverCommandLogAdapter } from './driver-command-log'
|
||||
|
||||
export type { DriverCommandLogAdapterOptions } from './driver-command-log'
|
||||
@@ -17,6 +17,7 @@ import type { HandlerFn, HandlerResult } from '.'
|
||||
import Bluebird from 'bluebird'
|
||||
import type { NetEvent } from '@packages/net-stubbing/lib/types'
|
||||
import Debug from 'debug'
|
||||
import { DriverCommandLogAdapter } from '../adapters'
|
||||
|
||||
const debug = Debug('cypress:driver:net-stubbing:events:before-request')
|
||||
|
||||
@@ -306,7 +307,11 @@ export const onBeforeRequest: HandlerFn<CyHttpMessages.IncomingRequest> = (Cypre
|
||||
resolve = _resolve
|
||||
})
|
||||
|
||||
request.setLogFlag = Cypress.ProxyLogging.logInterception(request, route)?.setFlag || (() => {})
|
||||
const commandLog = new DriverCommandLogAdapter({
|
||||
logInterception: (interception, route) => Cypress.ProxyLogging.logInterception(interception as Interception, route),
|
||||
})
|
||||
|
||||
request.setLogFlag = commandLog.logInterception({ interception: request, route })?.setFlag || (() => {})
|
||||
|
||||
// TODO: this misnomer is a holdover from XHR, should be numRequests
|
||||
route.log.set('numResponses', (route.log.get('numResponses') || 0) + 1)
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
import { describe, it, expect, vi } from 'vitest'
|
||||
import { DriverCommandLogAdapter } from '../../../src/cy/net-stubbing/adapters/driver-command-log'
|
||||
|
||||
describe('DriverCommandLogAdapter', () => {
|
||||
it('delegates logInterception to supplied proxy logging hook', () => {
|
||||
const setFlag = vi.fn()
|
||||
const logInterception = vi.fn().mockReturnValue({ setFlag })
|
||||
const adapter = new DriverCommandLogAdapter({ logInterception })
|
||||
const interception = { requestId: 'req-1' }
|
||||
const route = { routeId: 'route-1' }
|
||||
|
||||
const result = adapter.logInterception({ interception, route })
|
||||
|
||||
expect(logInterception).toHaveBeenCalledWith(interception, route)
|
||||
expect(result?.setFlag).toBe(setFlag)
|
||||
})
|
||||
|
||||
it('notifyIncomingRequest is a no-op on the driver', () => {
|
||||
const adapter = new DriverCommandLogAdapter({ logInterception: vi.fn() })
|
||||
|
||||
expect(() => adapter.notifyIncomingRequest({})).not.toThrow()
|
||||
})
|
||||
})
|
||||
@@ -8,4 +8,6 @@ export * from './merge-handler-result'
|
||||
|
||||
export * from './document-preparation'
|
||||
|
||||
export * from './request-logging'
|
||||
|
||||
export * from './network-policy-core'
|
||||
|
||||
@@ -122,6 +122,56 @@ export class NetworkPolicyCore {
|
||||
return port.removeSecurity(ctx)
|
||||
}
|
||||
|
||||
notifyIncomingRequest (ctx: unknown): void {
|
||||
const port = this.options.commandLog
|
||||
|
||||
if (!port) {
|
||||
throw new Error('NetworkPolicyCore.commandLog is not configured')
|
||||
}
|
||||
|
||||
return port.notifyIncomingRequest(ctx)
|
||||
}
|
||||
|
||||
async attachCrossOriginCookies (ctx: unknown): Promise<void> {
|
||||
const port = this.options.cookieState
|
||||
|
||||
if (!port) {
|
||||
throw new Error('NetworkPolicyCore.cookieState is not configured')
|
||||
}
|
||||
|
||||
return port.attachCrossOriginCookies(ctx)
|
||||
}
|
||||
|
||||
async copyCookiesFromResponse (ctx: unknown): Promise<void> {
|
||||
const port = this.options.cookieState
|
||||
|
||||
if (!port) {
|
||||
throw new Error('NetworkPolicyCore.cookieState is not configured')
|
||||
}
|
||||
|
||||
return port.copyCookiesFromResponse(ctx)
|
||||
}
|
||||
|
||||
async notifyResponseStreamReceived (ctx: unknown): Promise<void> {
|
||||
const port = this.options.networkCapture
|
||||
|
||||
if (!port) {
|
||||
throw new Error('NetworkPolicyCore.networkCapture is not configured')
|
||||
}
|
||||
|
||||
return port.notifyResponseStreamReceived(ctx)
|
||||
}
|
||||
|
||||
notifyResponseEndedWithEmptyBody (ctx: unknown, options: { isCached: boolean }): void {
|
||||
const port = this.options.networkCapture
|
||||
|
||||
if (!port) {
|
||||
throw new Error('NetworkPolicyCore.networkCapture is not configured')
|
||||
}
|
||||
|
||||
return port.notifyResponseEndedWithEmptyBody(ctx, options)
|
||||
}
|
||||
|
||||
get requestInterception (): ForRequestInterception | undefined {
|
||||
return this.options.requestInterception
|
||||
}
|
||||
@@ -133,4 +183,16 @@ export class NetworkPolicyCore {
|
||||
get documentPreparation (): ForDocumentPreparation | undefined {
|
||||
return this.options.documentPreparation
|
||||
}
|
||||
|
||||
get networkCapture (): ForNetworkCapture | undefined {
|
||||
return this.options.networkCapture
|
||||
}
|
||||
|
||||
get cookieState (): ForCookieState | undefined {
|
||||
return this.options.cookieState
|
||||
}
|
||||
|
||||
get commandLog (): ForCommandLog | undefined {
|
||||
return this.options.commandLog
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
export type ShouldLogRequestFacts = {
|
||||
matchingRoutes?: Array<{ staticResponse?: { log?: boolean } }>
|
||||
resourceType?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Pure intercept/request logging decision — extracted from proxy `SendToDriver` middleware.
|
||||
*/
|
||||
export function shouldLogRequest (facts: ShouldLogRequestFacts): boolean {
|
||||
if (facts.matchingRoutes?.length) {
|
||||
const lastMatchingRoute = facts.matchingRoutes[0]
|
||||
|
||||
if (!lastMatchingRoute.staticResponse) {
|
||||
return true
|
||||
}
|
||||
|
||||
if (lastMatchingRoute.staticResponse.log !== undefined) {
|
||||
return Boolean(lastMatchingRoute.staticResponse.log)
|
||||
}
|
||||
}
|
||||
|
||||
return facts.resourceType === 'fetch' || facts.resourceType === 'xhr'
|
||||
}
|
||||
@@ -33,21 +33,36 @@ export interface ForDocumentPreparation {
|
||||
* Driven port: Test Replay / protocol capture at the proxy boundary.
|
||||
*/
|
||||
export interface ForNetworkCapture {
|
||||
// Expanded in Stage 6a.
|
||||
notifyResponseStreamReceived (ctx: unknown): Promise<void>
|
||||
|
||||
notifyResponseEndedWithEmptyBody (ctx: unknown, options: { isCached: boolean }): void
|
||||
}
|
||||
|
||||
/**
|
||||
* Driven port: cookie jar read/write for proxied requests.
|
||||
*/
|
||||
export interface ForCookieState {
|
||||
// Expanded in Stage 6a.
|
||||
attachCrossOriginCookies (ctx: unknown): Promise<void>
|
||||
|
||||
copyCookiesFromResponse (ctx: unknown): Promise<void>
|
||||
}
|
||||
|
||||
export type CommandLogInterceptionInput = {
|
||||
interception: unknown
|
||||
route: unknown
|
||||
}
|
||||
|
||||
export type CommandLogInterceptionResult = {
|
||||
setFlag?: (flag: string) => void
|
||||
} | undefined
|
||||
|
||||
/**
|
||||
* Driven port: command log entries for intercept provenance.
|
||||
*/
|
||||
export interface ForCommandLog {
|
||||
// Expanded in Stage 6a.
|
||||
notifyIncomingRequest (ctx: unknown): void
|
||||
|
||||
logInterception (input: CommandLogInterceptionInput): CommandLogInterceptionResult
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -210,4 +210,32 @@ describe('NetworkPolicyCore', () => {
|
||||
await expect(core.injectHtml({})).rejects.toThrow(/documentPreparation/)
|
||||
await expect(core.removeSecurity({})).rejects.toThrow(/documentPreparation/)
|
||||
})
|
||||
|
||||
it('delegates capture, cookie, and command log ports', async () => {
|
||||
const notifyIncomingRequest = vi.fn()
|
||||
const attachCrossOriginCookies = vi.fn().mockResolvedValue(undefined)
|
||||
const copyCookiesFromResponse = vi.fn().mockResolvedValue(undefined)
|
||||
const notifyResponseStreamReceived = vi.fn().mockResolvedValue(undefined)
|
||||
const notifyResponseEndedWithEmptyBody = vi.fn()
|
||||
|
||||
const core = new NetworkPolicyCore({
|
||||
commandLog: { notifyIncomingRequest, logInterception: vi.fn() },
|
||||
cookieState: { attachCrossOriginCookies, copyCookiesFromResponse },
|
||||
networkCapture: { notifyResponseStreamReceived, notifyResponseEndedWithEmptyBody },
|
||||
})
|
||||
|
||||
const ctx = { req: {} }
|
||||
|
||||
core.notifyIncomingRequest(ctx)
|
||||
await core.attachCrossOriginCookies(ctx)
|
||||
await core.copyCookiesFromResponse(ctx)
|
||||
await core.notifyResponseStreamReceived(ctx)
|
||||
core.notifyResponseEndedWithEmptyBody(ctx, { isCached: false })
|
||||
|
||||
expect(notifyIncomingRequest).toHaveBeenCalledWith(ctx)
|
||||
expect(attachCrossOriginCookies).toHaveBeenCalledWith(ctx)
|
||||
expect(copyCookiesFromResponse).toHaveBeenCalledWith(ctx)
|
||||
expect(notifyResponseStreamReceived).toHaveBeenCalledWith(ctx)
|
||||
expect(notifyResponseEndedWithEmptyBody).toHaveBeenCalledWith(ctx, { isCached: false })
|
||||
})
|
||||
})
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import { shouldLogRequest } from '../../lib/core/request-logging'
|
||||
|
||||
describe('core/request-logging', () => {
|
||||
it('logs intercept routes without static responses by default', () => {
|
||||
expect(shouldLogRequest({
|
||||
matchingRoutes: [{ staticResponse: undefined }],
|
||||
resourceType: 'image',
|
||||
})).toBe(true)
|
||||
})
|
||||
|
||||
it('respects staticResponse.log when set', () => {
|
||||
expect(shouldLogRequest({
|
||||
matchingRoutes: [{ staticResponse: { log: false } }],
|
||||
resourceType: 'xhr',
|
||||
})).toBe(false)
|
||||
})
|
||||
|
||||
it('logs xhr and fetch when no intercept routes match', () => {
|
||||
expect(shouldLogRequest({ resourceType: 'xhr' })).toBe(true)
|
||||
expect(shouldLogRequest({ resourceType: 'fetch' })).toBe(true)
|
||||
expect(shouldLogRequest({ resourceType: 'image' })).toBe(false)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,79 @@
|
||||
import { telemetry } from '@packages/telemetry'
|
||||
import { getSameSiteContext, shouldAttachAndSetCookies, addCookieJarCookiesToRequest } from '../http/util/cookies'
|
||||
import { doesTopNeedToBeSimulated } from '../http/util/top-simulation'
|
||||
import { isVerboseTelemetry as isVerbose } from '../http'
|
||||
import * as errors from '@packages/errors'
|
||||
import type { RequestInterceptionMiddlewareCtx } from './types'
|
||||
|
||||
/**
|
||||
* Attach cross-origin cookies from the server-side cookie jar to proxied requests.
|
||||
*/
|
||||
export async function attachCrossOriginCookies (mw: RequestInterceptionMiddlewareCtx): Promise<void> {
|
||||
const span = telemetry.startSpan({ name: 'maybe:attach:cross:origin:cookies', parentSpan: mw.reqMiddlewareSpan, isVerbose })
|
||||
|
||||
const doesTopNeedSimulation = doesTopNeedToBeSimulated(mw)
|
||||
|
||||
span?.setAttributes({
|
||||
doesTopNeedToBeSimulated: doesTopNeedSimulation,
|
||||
resourceType: mw.req.resourceType,
|
||||
})
|
||||
|
||||
if (!doesTopNeedSimulation) {
|
||||
span?.end()
|
||||
|
||||
return mw.next()
|
||||
}
|
||||
|
||||
if (mw.req.isSyncRequest) {
|
||||
errors.warning('SYNCHRONOUS_XHR_REQUEST_COOKIES_NOT_APPLIED', mw.req.proxiedUrl)
|
||||
}
|
||||
|
||||
const currentAUTUrl = mw.getAUTUrl()
|
||||
const shouldCookiesBeAttachedToRequest = shouldAttachAndSetCookies(mw.req.proxiedUrl, currentAUTUrl, mw.req.resourceType, mw.req.credentialsLevel, mw.req.isAUTFrame)
|
||||
|
||||
span?.setAttributes({
|
||||
currentAUTUrl,
|
||||
shouldCookiesBeAttachedToRequest,
|
||||
})
|
||||
|
||||
mw.debug(`should cookies be attached to request?: ${shouldCookiesBeAttachedToRequest}`)
|
||||
if (!shouldCookiesBeAttachedToRequest) {
|
||||
span?.end()
|
||||
|
||||
return mw.next()
|
||||
}
|
||||
|
||||
const sameSiteContext = getSameSiteContext(
|
||||
currentAUTUrl,
|
||||
mw.req.proxiedUrl,
|
||||
mw.req.isAUTFrame,
|
||||
)
|
||||
|
||||
span?.setAttributes({
|
||||
sameSiteContext,
|
||||
currentAUTUrl,
|
||||
isAUTFrame: mw.req.isAUTFrame,
|
||||
})
|
||||
|
||||
const applicableCookiesInCookieJar = mw.getCookieJar().getCookies(mw.req.proxiedUrl, sameSiteContext)
|
||||
const cookiesOnRequest = (mw.req.headers['cookie'] || '').split('; ')
|
||||
|
||||
const existingCookiesInJar = applicableCookiesInCookieJar.join('; ')
|
||||
const addedCookiesFromHeader = cookiesOnRequest.join('; ')
|
||||
|
||||
mw.debug('existing cookies on request from cookie jar: %s', existingCookiesInJar)
|
||||
mw.debug('add cookies to request from header: %s', addedCookiesFromHeader)
|
||||
|
||||
mw.req.headers['cookie'] = addCookieJarCookiesToRequest(applicableCookiesInCookieJar, cookiesOnRequest) || undefined
|
||||
|
||||
span?.setAttributes({
|
||||
existingCookiesInJar,
|
||||
addedCookiesFromHeader,
|
||||
cookieHeader: mw.req.headers['cookie'],
|
||||
})
|
||||
|
||||
mw.debug('cookies being sent with request: %s', mw.req.headers['cookie'])
|
||||
|
||||
span?.end()
|
||||
mw.next()
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
import { URL } from 'url'
|
||||
import { telemetry } from '@packages/telemetry'
|
||||
import { toughCookieToAutomationCookie } from '@packages/server/lib/util/cookies'
|
||||
import { CookiesHelper } from '../http/util/cookies'
|
||||
import { doesTopNeedToBeSimulated } from '../http/util/top-simulation'
|
||||
import { isVerboseTelemetry as isVerbose } from '../http'
|
||||
import * as errors from '@packages/errors'
|
||||
import type { ResponseInterceptionMiddlewareCtx } from './types'
|
||||
|
||||
function setSimulatedCookies (mw: ResponseInterceptionMiddlewareCtx) {
|
||||
if (mw.res.wantsInjection !== 'fullCrossOrigin') return
|
||||
|
||||
const defaultDomain = (new URL(mw.req.proxiedUrl)).hostname
|
||||
const allCookiesForRequest = mw.getCookieJar()
|
||||
.getCookies(mw.req.proxiedUrl)
|
||||
.map((cookie) => toughCookieToAutomationCookie(cookie, defaultDomain))
|
||||
|
||||
mw.simulatedCookies = allCookiesForRequest
|
||||
}
|
||||
|
||||
/**
|
||||
* Capture Set-Cookie headers into the server-side cookie jar and browser automation.
|
||||
*/
|
||||
export async function copyCookiesFromResponse (mw: ResponseInterceptionMiddlewareCtx): Promise<void> {
|
||||
const span = telemetry.startSpan({ name: 'maybe:copy:cookies:from:incoming:res', parentSpan: mw.resMiddlewareSpan, isVerbose })
|
||||
|
||||
const cookies: string | string[] | undefined = mw.incomingRes.headers['set-cookie']
|
||||
|
||||
const areCookiesPresent = !cookies || !cookies.length
|
||||
|
||||
span?.setAttributes({
|
||||
areCookiesPresent,
|
||||
})
|
||||
|
||||
if (areCookiesPresent) {
|
||||
setSimulatedCookies(mw)
|
||||
|
||||
span?.end()
|
||||
|
||||
return mw.next()
|
||||
}
|
||||
|
||||
const doesTopNeedSimulating = doesTopNeedToBeSimulated(mw)
|
||||
|
||||
span?.setAttributes({
|
||||
doesTopNeedSimulating,
|
||||
})
|
||||
|
||||
const appendCookie = (cookie: string) => {
|
||||
const headerName = 'Set-Cookie'
|
||||
|
||||
try {
|
||||
mw.res.append(headerName, cookie)
|
||||
} catch (err) {
|
||||
mw.debug(`failed to append header ${headerName}, continuing %o`, { err, cookie })
|
||||
}
|
||||
}
|
||||
|
||||
if (!doesTopNeedSimulating) {
|
||||
([] as string[]).concat(cookies).forEach((cookie) => {
|
||||
appendCookie(cookie)
|
||||
})
|
||||
|
||||
span?.end()
|
||||
|
||||
return mw.next()
|
||||
}
|
||||
|
||||
const cookiesHelper = new CookiesHelper({
|
||||
cookieJar: mw.getCookieJar(),
|
||||
currentAUTUrl: mw.getAUTUrl(),
|
||||
debug: mw.debug,
|
||||
request: {
|
||||
url: mw.req.proxiedUrl,
|
||||
isAUTFrame: mw.req.isAUTFrame,
|
||||
doesTopNeedSimulating,
|
||||
resourceType: mw.req.resourceType,
|
||||
credentialLevel: mw.req.credentialsLevel,
|
||||
},
|
||||
})
|
||||
|
||||
await cookiesHelper.capturePreviousCookies()
|
||||
|
||||
;([] as string[]).concat(cookies).forEach((cookie) => {
|
||||
cookiesHelper.setCookie(cookie)
|
||||
|
||||
appendCookie(cookie)
|
||||
})
|
||||
|
||||
setSimulatedCookies(mw)
|
||||
|
||||
const addedCookies = await cookiesHelper.getAddedCookies()
|
||||
const wereSimCookiesAdded = addedCookies.length
|
||||
|
||||
span?.setAttributes({
|
||||
wereSimCookiesAdded,
|
||||
})
|
||||
|
||||
if (!wereSimCookiesAdded) {
|
||||
span?.end()
|
||||
|
||||
return mw.next()
|
||||
}
|
||||
|
||||
if (mw.req.isSyncRequest) {
|
||||
errors.warning('SYNCHRONOUS_XHR_REQUEST_COOKIES_NOT_SET', mw.req.proxiedUrl)
|
||||
|
||||
span?.end()
|
||||
|
||||
return mw.next()
|
||||
}
|
||||
|
||||
mw.serverBus.once('cross:origin:cookies:received', () => {
|
||||
span?.end()
|
||||
mw.next()
|
||||
})
|
||||
|
||||
mw.serverBus.emit('cross:origin:cookies', addedCookies)
|
||||
}
|
||||
@@ -4,6 +4,12 @@ export { ProxyResponseInterceptionAdapter } from './proxy-response-interception'
|
||||
|
||||
export { ProxyDocumentPreparationAdapter } from './proxy-document-preparation'
|
||||
|
||||
export { ProxyNetworkCaptureAdapter } from './proxy-network-capture'
|
||||
|
||||
export { ProxyCookieStateAdapter } from './proxy-cookie-state'
|
||||
|
||||
export { ProxyCommandLogAdapter } from './proxy-command-log'
|
||||
|
||||
export { correlateBrowserPreRequest } from './correlate-browser-pre-request'
|
||||
|
||||
export { sendRequestOutgoing } from './send-request-outgoing'
|
||||
@@ -14,4 +20,12 @@ export { injectHtml } from './inject-html'
|
||||
|
||||
export { removeSecurity } from './remove-security'
|
||||
|
||||
export { sendToDriver } from './send-to-driver'
|
||||
|
||||
export { attachCrossOriginCookies } from './attach-cross-origin-cookies'
|
||||
|
||||
export { copyCookiesFromResponse } from './copy-cookies-from-response'
|
||||
|
||||
export { notifyResponseEndedWithEmptyBody, notifyResponseStreamReceived } from './network-capture'
|
||||
|
||||
export type { RequestInterceptionMiddlewareCtx, ResponseInterceptionMiddlewareCtx } from './types'
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
import type { ResponseStreamOptions } from '@packages/types'
|
||||
import { telemetry } from '@packages/telemetry'
|
||||
import { isVerboseTelemetry as isVerbose } from '../http'
|
||||
import { getOriginalRequestId } from '../http/util/protocol-capture'
|
||||
import type { ResponseInterceptionMiddlewareCtx } from './types'
|
||||
|
||||
/**
|
||||
* Notify the protocol manager that a response stream is available for capture.
|
||||
*/
|
||||
export async function notifyResponseStreamReceived (mw: ResponseInterceptionMiddlewareCtx): Promise<void> {
|
||||
if (!mw.protocolManager || !mw.req.browserPreRequest?.requestId) {
|
||||
return
|
||||
}
|
||||
|
||||
const preRequest = mw.req.browserPreRequest
|
||||
const requestId = getOriginalRequestId(preRequest.requestId)
|
||||
|
||||
const span = telemetry.startSpan({ name: 'gzip:body:protocol-notification', parentSpan: mw.resMiddlewareSpan, isVerbose })
|
||||
|
||||
const streamOptions: ResponseStreamOptions = {
|
||||
requestId,
|
||||
responseHeaders: mw.incomingRes.headers,
|
||||
isAlreadyGunzipped: mw.isGunzipped,
|
||||
isAlreadyBrotliDecompressed: mw.isBrotliDecompressed,
|
||||
responseStream: mw.incomingResStream,
|
||||
res: mw.res,
|
||||
timings: {
|
||||
cdpRequestWillBeSentTimestamp: preRequest.cdpRequestWillBeSentTimestamp,
|
||||
cdpRequestWillBeSentReceivedTimestamp: preRequest.cdpRequestWillBeSentReceivedTimestamp,
|
||||
proxyRequestReceivedTimestamp: preRequest.proxyRequestReceivedTimestamp,
|
||||
cdpLagDuration: preRequest.cdpLagDuration,
|
||||
proxyRequestCorrelationDuration: preRequest.proxyRequestCorrelationDuration,
|
||||
},
|
||||
}
|
||||
|
||||
const resultingStream = mw.protocolManager.responseStreamReceived(streamOptions)
|
||||
|
||||
if (resultingStream) {
|
||||
mw.incomingResStream = resultingStream.on('error', mw.onError).once('close', () => {
|
||||
span?.end()
|
||||
})
|
||||
} else {
|
||||
span?.end()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Notify the protocol manager that a response ended with an empty body.
|
||||
*/
|
||||
export function notifyResponseEndedWithEmptyBody (
|
||||
mw: ResponseInterceptionMiddlewareCtx,
|
||||
options: { isCached: boolean },
|
||||
): void {
|
||||
if (!mw.protocolManager || !mw.req.browserPreRequest?.requestId) {
|
||||
return
|
||||
}
|
||||
|
||||
const requestId = getOriginalRequestId(mw.req.browserPreRequest.requestId)
|
||||
|
||||
mw.protocolManager.responseEndedWithEmptyBody({
|
||||
requestId,
|
||||
isCached: options.isCached,
|
||||
timings: {
|
||||
cdpRequestWillBeSentTimestamp: mw.req.browserPreRequest.cdpRequestWillBeSentTimestamp,
|
||||
cdpRequestWillBeSentReceivedTimestamp: mw.req.browserPreRequest.cdpRequestWillBeSentReceivedTimestamp,
|
||||
proxyRequestReceivedTimestamp: mw.req.browserPreRequest.proxyRequestReceivedTimestamp,
|
||||
cdpLagDuration: mw.req.browserPreRequest.cdpLagDuration,
|
||||
proxyRequestCorrelationDuration: mw.req.browserPreRequest.proxyRequestCorrelationDuration,
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import type { CommandLogInterceptionInput, CommandLogInterceptionResult, ForCommandLog } from '@packages/network-policy'
|
||||
import { sendToDriver } from './send-to-driver'
|
||||
import type { RequestInterceptionMiddlewareCtx } from './types'
|
||||
|
||||
/** Server-side {@link ForCommandLog} adapter — emits incoming request events to the driver. */
|
||||
export class ProxyCommandLogAdapter implements ForCommandLog {
|
||||
notifyIncomingRequest (ctx: unknown): void {
|
||||
sendToDriver(ctx as RequestInterceptionMiddlewareCtx)
|
||||
}
|
||||
|
||||
logInterception (_input: CommandLogInterceptionInput): CommandLogInterceptionResult {
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import type { ForCookieState } from '@packages/network-policy'
|
||||
import { attachCrossOriginCookies } from './attach-cross-origin-cookies'
|
||||
import { copyCookiesFromResponse } from './copy-cookies-from-response'
|
||||
import type { RequestInterceptionMiddlewareCtx, ResponseInterceptionMiddlewareCtx } from './types'
|
||||
|
||||
/** {@link ForCookieState} adapter — delegates to legacy proxy cookie jar middleware. */
|
||||
export class ProxyCookieStateAdapter implements ForCookieState {
|
||||
attachCrossOriginCookies (ctx: unknown): Promise<void> {
|
||||
return attachCrossOriginCookies(ctx as RequestInterceptionMiddlewareCtx)
|
||||
}
|
||||
|
||||
copyCookiesFromResponse (ctx: unknown): Promise<void> {
|
||||
return copyCookiesFromResponse(ctx as ResponseInterceptionMiddlewareCtx)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import type { ForNetworkCapture } from '@packages/network-policy'
|
||||
import { notifyResponseEndedWithEmptyBody, notifyResponseStreamReceived } from './network-capture'
|
||||
import type { ResponseInterceptionMiddlewareCtx } from './types'
|
||||
|
||||
/** {@link ForNetworkCapture} adapter — delegates to legacy proxy protocol capture hooks. */
|
||||
export class ProxyNetworkCaptureAdapter implements ForNetworkCapture {
|
||||
notifyResponseStreamReceived (ctx: unknown): Promise<void> {
|
||||
return notifyResponseStreamReceived(ctx as ResponseInterceptionMiddlewareCtx)
|
||||
}
|
||||
|
||||
notifyResponseEndedWithEmptyBody (ctx: unknown, options: { isCached: boolean }): void {
|
||||
return notifyResponseEndedWithEmptyBody(ctx as ResponseInterceptionMiddlewareCtx, options)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import { shouldLogRequest } from '@packages/network-policy'
|
||||
import { telemetry } from '@packages/telemetry'
|
||||
import { isVerboseTelemetry as isVerbose } from '../http'
|
||||
import type { RequestInterceptionMiddlewareCtx } from './types'
|
||||
|
||||
/**
|
||||
* Emit incoming request events to the driver for proxy command logging.
|
||||
*/
|
||||
export function sendToDriver (mw: RequestInterceptionMiddlewareCtx): void {
|
||||
const span = telemetry.startSpan({ name: 'send:to:driver', parentSpan: mw.reqMiddlewareSpan, isVerbose })
|
||||
|
||||
const shouldLogReq = shouldLogRequest({
|
||||
matchingRoutes: mw.req.matchingRoutes,
|
||||
resourceType: mw.req.resourceType,
|
||||
})
|
||||
|
||||
if (shouldLogReq && mw.req.browserPreRequest) {
|
||||
mw.socket.toDriver('request:event', 'incoming:request', mw.req.browserPreRequest)
|
||||
}
|
||||
|
||||
span?.setAttributes({
|
||||
shouldLogReq,
|
||||
hasBrowserPreRequest: !!mw.req.browserPreRequest,
|
||||
})
|
||||
|
||||
span?.end()
|
||||
mw.next()
|
||||
}
|
||||
@@ -3,15 +3,10 @@ import { blocked } from '@packages/network'
|
||||
import { InterceptRequest, SetMatchingRoutes } from '@packages/net-stubbing'
|
||||
import { telemetry } from '@packages/telemetry'
|
||||
import { isVerboseTelemetry as isVerbose } from '.'
|
||||
import {
|
||||
addCookieJarCookiesToRequest, getSameSiteContext, shouldAttachAndSetCookies,
|
||||
} from './util/cookies'
|
||||
import { doesTopNeedToBeSimulated } from './util/top-simulation'
|
||||
import { resourceTypeAndCredentialManager } from '../resourceTypeAndCredentialManager'
|
||||
import type { HttpMiddleware } from './'
|
||||
import type { CypressIncomingRequest } from '../types'
|
||||
import { getSupportedAcceptEncoding, urlMatchesOriginProtectionSpace } from '@packages/network-tools'
|
||||
import * as errors from '@packages/errors'
|
||||
|
||||
// do not use a debug namespace in this file - use the per-request `this.debug` instead
|
||||
// available as cypress-verbose:proxy:http
|
||||
@@ -139,113 +134,12 @@ const FormatCookiesIfApplicable: RequestMiddleware = function () {
|
||||
return this.next()
|
||||
}
|
||||
|
||||
const MaybeAttachCrossOriginCookies: RequestMiddleware = function () {
|
||||
const span = telemetry.startSpan({ name: 'maybe:attach:cross:origin:cookies', parentSpan: this.reqMiddlewareSpan, isVerbose })
|
||||
|
||||
const doesTopNeedSimulation = doesTopNeedToBeSimulated(this)
|
||||
|
||||
span?.setAttributes({
|
||||
doesTopNeedToBeSimulated: doesTopNeedSimulation,
|
||||
resourceType: this.req.resourceType,
|
||||
})
|
||||
|
||||
if (!doesTopNeedSimulation) {
|
||||
span?.end()
|
||||
|
||||
return this.next()
|
||||
}
|
||||
|
||||
if (this.req.isSyncRequest) {
|
||||
errors.warning('SYNCHRONOUS_XHR_REQUEST_COOKIES_NOT_APPLIED', this.req.proxiedUrl)
|
||||
}
|
||||
|
||||
// Top needs to be simulated since the AUT is in a cross origin state. Get the "requested with" and credentials and see what cookies need to be attached
|
||||
const currentAUTUrl = this.getAUTUrl()
|
||||
const shouldCookiesBeAttachedToRequest = shouldAttachAndSetCookies(this.req.proxiedUrl, currentAUTUrl, this.req.resourceType, this.req.credentialsLevel, this.req.isAUTFrame)
|
||||
|
||||
span?.setAttributes({
|
||||
currentAUTUrl,
|
||||
shouldCookiesBeAttachedToRequest,
|
||||
})
|
||||
|
||||
this.debug(`should cookies be attached to request?: ${shouldCookiesBeAttachedToRequest}`)
|
||||
if (!shouldCookiesBeAttachedToRequest) {
|
||||
span?.end()
|
||||
|
||||
return this.next()
|
||||
}
|
||||
|
||||
const sameSiteContext = getSameSiteContext(
|
||||
currentAUTUrl,
|
||||
this.req.proxiedUrl,
|
||||
this.req.isAUTFrame,
|
||||
)
|
||||
|
||||
span?.setAttributes({
|
||||
sameSiteContext,
|
||||
currentAUTUrl,
|
||||
isAUTFrame: this.req.isAUTFrame,
|
||||
})
|
||||
|
||||
const applicableCookiesInCookieJar = this.getCookieJar().getCookies(this.req.proxiedUrl, sameSiteContext)
|
||||
const cookiesOnRequest = (this.req.headers['cookie'] || '').split('; ')
|
||||
|
||||
const existingCookiesInJar = applicableCookiesInCookieJar.join('; ')
|
||||
const addedCookiesFromHeader = cookiesOnRequest.join('; ')
|
||||
|
||||
this.debug('existing cookies on request from cookie jar: %s', existingCookiesInJar)
|
||||
this.debug('add cookies to request from header: %s', addedCookiesFromHeader)
|
||||
|
||||
// if the cookie header is empty (i.e. ''), set it to undefined for expected behavior
|
||||
this.req.headers['cookie'] = addCookieJarCookiesToRequest(applicableCookiesInCookieJar, cookiesOnRequest) || undefined
|
||||
|
||||
span?.setAttributes({
|
||||
existingCookiesInJar,
|
||||
addedCookiesFromHeader,
|
||||
cookieHeader: this.req.headers['cookie'],
|
||||
})
|
||||
|
||||
this.debug('cookies being sent with request: %s', this.req.headers['cookie'])
|
||||
|
||||
span?.end()
|
||||
this.next()
|
||||
}
|
||||
|
||||
function shouldLog (req: CypressIncomingRequest) {
|
||||
// 1. Any matching `cy.intercept()` should cause `req` to be logged by default, unless `log: false` is passed explicitly.
|
||||
if (req.matchingRoutes?.length) {
|
||||
const lastMatchingRoute = req.matchingRoutes[0]
|
||||
|
||||
if (!lastMatchingRoute.staticResponse) {
|
||||
// No StaticResponse is set, therefore the request must be logged.
|
||||
return true
|
||||
}
|
||||
|
||||
if (lastMatchingRoute.staticResponse.log !== undefined) {
|
||||
return Boolean(lastMatchingRoute.staticResponse.log)
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Otherwise, only log if it is an XHR or fetch.
|
||||
return req.resourceType === 'fetch' || req.resourceType === 'xhr'
|
||||
const MaybeAttachCrossOriginCookies: RequestMiddleware = async function () {
|
||||
return this.networkPolicyCore.attachCrossOriginCookies(this)
|
||||
}
|
||||
|
||||
const SendToDriver: RequestMiddleware = function () {
|
||||
const span = telemetry.startSpan({ name: 'send:to:driver', parentSpan: this.reqMiddlewareSpan, isVerbose })
|
||||
|
||||
const shouldLogReq = shouldLog(this.req)
|
||||
|
||||
if (shouldLogReq && this.req.browserPreRequest) {
|
||||
this.socket.toDriver('request:event', 'incoming:request', this.req.browserPreRequest)
|
||||
}
|
||||
|
||||
span?.setAttributes({
|
||||
shouldLogReq,
|
||||
hasBrowserPreRequest: !!this.req.browserPreRequest,
|
||||
})
|
||||
|
||||
span?.end()
|
||||
this.next()
|
||||
this.networkPolicyCore.notifyIncomingRequest(this)
|
||||
}
|
||||
|
||||
const MaybeEndRequestWithBufferedResponse: RequestMiddleware = function () {
|
||||
|
||||
@@ -4,17 +4,12 @@ import { URL } from 'url'
|
||||
import zlib from 'zlib'
|
||||
import { InterceptResponse } from '@packages/net-stubbing'
|
||||
import { concatStream, httpUtils } from '@packages/network'
|
||||
import { toughCookieToAutomationCookie } from '@packages/server/lib/util/cookies'
|
||||
import { telemetry } from '@packages/telemetry'
|
||||
import { hasServiceWorkerHeader, isVerboseTelemetry as isVerbose } from '.'
|
||||
import { CookiesHelper } from './util/cookies'
|
||||
import { doesTopNeedToBeSimulated } from './util/top-simulation'
|
||||
import * as errors from '@packages/errors'
|
||||
|
||||
import type { CookieOptions } from 'express'
|
||||
import type { ResponseStreamOptions } from '@packages/types'
|
||||
import type { CypressOutgoingResponse } from '../types'
|
||||
import type { HttpMiddleware, HttpMiddlewareThis } from '.'
|
||||
import type { HttpMiddleware } from '.'
|
||||
import type { IncomingMessage } from 'http'
|
||||
|
||||
import { cspHeaderNames, generateCspDirectives, parseCspHeaders, problematicCspDirectives, unsupportedCSPDirectives } from './util/csp-header'
|
||||
@@ -150,18 +145,6 @@ const stringifyFeaturePolicy = (policy: any): string => {
|
||||
return pairs.map((directive) => directive.join(' ')).join('; ')
|
||||
}
|
||||
|
||||
const requestIdRegEx = /^(.*)-retry-([\d]+)$/
|
||||
const getOriginalRequestId = (requestId: string) => {
|
||||
let originalRequestId = requestId
|
||||
const match = requestIdRegEx.exec(requestId)
|
||||
|
||||
if (match) {
|
||||
[, originalRequestId] = match
|
||||
}
|
||||
|
||||
return originalRequestId
|
||||
}
|
||||
|
||||
const LogResponse: ResponseMiddleware = function () {
|
||||
this.debug('received response %o', {
|
||||
browserPreRequest: _.pick(this.req.browserPreRequest, 'requestId'),
|
||||
@@ -469,142 +452,8 @@ 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 span = telemetry.startSpan({ name: 'maybe:copy:cookies:from:incoming:res', parentSpan: this.resMiddlewareSpan, isVerbose })
|
||||
|
||||
const cookies: string | string[] | undefined = this.incomingRes.headers['set-cookie']
|
||||
|
||||
const areCookiesPresent = !cookies || !cookies.length
|
||||
|
||||
span?.setAttributes({
|
||||
areCookiesPresent,
|
||||
})
|
||||
|
||||
if (areCookiesPresent) {
|
||||
setSimulatedCookies(this)
|
||||
|
||||
span?.end()
|
||||
|
||||
return this.next()
|
||||
}
|
||||
|
||||
// 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 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. 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 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 doesTopNeedSimulating = doesTopNeedToBeSimulated(this)
|
||||
|
||||
span?.setAttributes({
|
||||
doesTopNeedSimulating,
|
||||
})
|
||||
|
||||
const appendCookie = (cookie: string) => {
|
||||
// always call 'Set-Cookie' in the browser as cross origin or same site requests
|
||||
// can effectively set cookies in the browser if given correct credential permissions
|
||||
const headerName = 'Set-Cookie'
|
||||
|
||||
try {
|
||||
this.res.append(headerName, cookie)
|
||||
} catch (err) {
|
||||
this.debug(`failed to append header ${headerName}, continuing %o`, { err, cookie })
|
||||
}
|
||||
}
|
||||
|
||||
if (!doesTopNeedSimulating) {
|
||||
([] as string[]).concat(cookies).forEach((cookie) => {
|
||||
appendCookie(cookie)
|
||||
})
|
||||
|
||||
span?.end()
|
||||
|
||||
return this.next()
|
||||
}
|
||||
|
||||
const cookiesHelper = new CookiesHelper({
|
||||
cookieJar: this.getCookieJar(),
|
||||
currentAUTUrl: this.getAUTUrl(),
|
||||
debug: this.debug,
|
||||
request: {
|
||||
url: this.req.proxiedUrl,
|
||||
isAUTFrame: this.req.isAUTFrame,
|
||||
doesTopNeedSimulating,
|
||||
resourceType: this.req.resourceType,
|
||||
credentialLevel: this.req.credentialsLevel,
|
||||
},
|
||||
})
|
||||
|
||||
await cookiesHelper.capturePreviousCookies()
|
||||
|
||||
;([] as string[]).concat(cookies).forEach((cookie) => {
|
||||
cookiesHelper.setCookie(cookie)
|
||||
|
||||
appendCookie(cookie)
|
||||
})
|
||||
|
||||
setSimulatedCookies(this)
|
||||
|
||||
const addedCookies = await cookiesHelper.getAddedCookies()
|
||||
const wereSimCookiesAdded = addedCookies.length
|
||||
|
||||
span?.setAttributes({
|
||||
wereSimCookiesAdded,
|
||||
})
|
||||
|
||||
if (!wereSimCookiesAdded) {
|
||||
span?.end()
|
||||
|
||||
return this.next()
|
||||
}
|
||||
|
||||
// if the request is sync, we cannot wait on the cross:origin:cookies:received
|
||||
// event since the sync request is blocking. This means that the cross-origin cookies
|
||||
// may not have been applied.
|
||||
if (this.req.isSyncRequest) {
|
||||
errors.warning('SYNCHRONOUS_XHR_REQUEST_COOKIES_NOT_SET', this.req.proxiedUrl)
|
||||
|
||||
span?.end()
|
||||
|
||||
return this.next()
|
||||
}
|
||||
|
||||
// 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', () => {
|
||||
span?.end()
|
||||
this.next()
|
||||
})
|
||||
|
||||
this.serverBus.emit('cross:origin:cookies', addedCookies)
|
||||
return this.networkPolicyCore.copyCookiesFromResponse(this)
|
||||
}
|
||||
|
||||
const REDIRECT_STATUS_CODES: any[] = [301, 302, 303, 307, 308]
|
||||
@@ -661,40 +510,16 @@ const ClearCyInitialCookie: ResponseMiddleware = function () {
|
||||
}
|
||||
|
||||
const MaybeEndWithEmptyBody: ResponseMiddleware = function () {
|
||||
const notifyProtocolManagerOfEmptyBody = (isCached: boolean) => {
|
||||
if (this.protocolManager && this.req.browserPreRequest?.requestId) {
|
||||
const requestId = getOriginalRequestId(this.req.browserPreRequest.requestId)
|
||||
|
||||
this.protocolManager.responseEndedWithEmptyBody({
|
||||
requestId,
|
||||
isCached,
|
||||
timings: {
|
||||
cdpRequestWillBeSentTimestamp: this.req.browserPreRequest.cdpRequestWillBeSentTimestamp,
|
||||
cdpRequestWillBeSentReceivedTimestamp: this.req.browserPreRequest.cdpRequestWillBeSentReceivedTimestamp,
|
||||
proxyRequestReceivedTimestamp: this.req.browserPreRequest.proxyRequestReceivedTimestamp,
|
||||
cdpLagDuration: this.req.browserPreRequest.cdpLagDuration,
|
||||
proxyRequestCorrelationDuration: this.req.browserPreRequest.proxyRequestCorrelationDuration,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if (httpUtils.responseMustHaveEmptyBody(this.req, this.incomingRes)) {
|
||||
notifyProtocolManagerOfEmptyBody(this.incomingRes.statusCode === 304)
|
||||
this.networkPolicyCore.notifyResponseEndedWithEmptyBody(this, {
|
||||
isCached: this.incomingRes.statusCode === 304,
|
||||
})
|
||||
|
||||
this.res.end()
|
||||
|
||||
return this.end()
|
||||
}
|
||||
|
||||
// When the origin response declared `Content-Length: 0`, short-circuit with an
|
||||
// explicit Content-Length: 0 instead of streaming an empty body — otherwise
|
||||
// OmitProblematicHeaders has stripped Content-Length and Node's HTTP layer
|
||||
// adds `Transfer-Encoding: chunked`, which breaks clients that assume a
|
||||
// response for chunked encoding. See cypress-io/cypress#16469.
|
||||
// Skip when downstream middleware will rewrite the body or when a cy.intercept
|
||||
// route matched (the interceptor may have replaced the body without updating
|
||||
// the upstream Content-Length header).
|
||||
const wasIntercepted = !!this.netStubbingState?.requests?.[this.req.requestId]
|
||||
|
||||
if (
|
||||
@@ -703,7 +528,7 @@ const MaybeEndWithEmptyBody: ResponseMiddleware = function () {
|
||||
&& !this.res.wantsInjection
|
||||
&& !this.res.wantsSecurityRemoved
|
||||
) {
|
||||
notifyProtocolManagerOfEmptyBody(false)
|
||||
this.networkPolicyCore.notifyResponseEndedWithEmptyBody(this, { isCached: false })
|
||||
this.res.setHeader('Content-Length', '0')
|
||||
this.res.end()
|
||||
|
||||
@@ -755,38 +580,7 @@ const MaybeInjectServiceWorker: ResponseMiddleware = function () {
|
||||
}
|
||||
|
||||
const CompressBody: ResponseMiddleware = async function () {
|
||||
if (this.protocolManager && this.req.browserPreRequest?.requestId) {
|
||||
const preRequest = this.req.browserPreRequest
|
||||
const requestId = getOriginalRequestId(preRequest.requestId)
|
||||
|
||||
const span = telemetry.startSpan({ name: 'gzip:body:protocol-notification', parentSpan: this.resMiddlewareSpan, isVerbose })
|
||||
|
||||
const streamOptions: ResponseStreamOptions = {
|
||||
requestId,
|
||||
responseHeaders: this.incomingRes.headers,
|
||||
isAlreadyGunzipped: this.isGunzipped,
|
||||
isAlreadyBrotliDecompressed: this.isBrotliDecompressed,
|
||||
responseStream: this.incomingResStream,
|
||||
res: this.res,
|
||||
timings: {
|
||||
cdpRequestWillBeSentTimestamp: preRequest.cdpRequestWillBeSentTimestamp,
|
||||
cdpRequestWillBeSentReceivedTimestamp: preRequest.cdpRequestWillBeSentReceivedTimestamp,
|
||||
proxyRequestReceivedTimestamp: preRequest.proxyRequestReceivedTimestamp,
|
||||
cdpLagDuration: preRequest.cdpLagDuration,
|
||||
proxyRequestCorrelationDuration: preRequest.proxyRequestCorrelationDuration,
|
||||
},
|
||||
}
|
||||
|
||||
const resultingStream = this.protocolManager.responseStreamReceived(streamOptions)
|
||||
|
||||
if (resultingStream) {
|
||||
this.incomingResStream = resultingStream.on('error', this.onError).once('close', () => {
|
||||
span?.end()
|
||||
})
|
||||
} else {
|
||||
span?.end()
|
||||
}
|
||||
}
|
||||
await this.networkPolicyCore.notifyResponseStreamReceived(this)
|
||||
|
||||
// Re-compress in the same order as the original content-encoding (innermost first).
|
||||
const order = this.contentEncodingOrder ?? []
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
const requestIdRegEx = /^(.*)-retry-([\d]+)$/
|
||||
|
||||
/**
|
||||
* Strip synthetic retry suffixes from CDP request IDs before protocol capture.
|
||||
*/
|
||||
export function getOriginalRequestId (requestId: string) {
|
||||
let originalRequestId = requestId
|
||||
const match = requestIdRegEx.exec(requestId)
|
||||
|
||||
if (match) {
|
||||
[, originalRequestId] = match
|
||||
}
|
||||
|
||||
return originalRequestId
|
||||
}
|
||||
@@ -13,7 +13,10 @@ import { DocumentDomainInjection, RemoteStates } from '@packages/network-tools'
|
||||
import { EventEmitter } from 'events'
|
||||
import { NetworkPolicyCore } from '@packages/network-policy'
|
||||
import {
|
||||
ProxyCommandLogAdapter,
|
||||
ProxyCookieStateAdapter,
|
||||
ProxyDocumentPreparationAdapter,
|
||||
ProxyNetworkCaptureAdapter,
|
||||
ProxyRequestInterceptionAdapter,
|
||||
ProxyResponseInterceptionAdapter,
|
||||
} from '../../lib/adapters'
|
||||
@@ -62,6 +65,9 @@ describe('network stubbing', () => {
|
||||
requestInterception: new ProxyRequestInterceptionAdapter(),
|
||||
responseInterception: new ProxyResponseInterceptionAdapter(),
|
||||
documentPreparation: new ProxyDocumentPreparationAdapter(),
|
||||
networkCapture: new ProxyNetworkCaptureAdapter(),
|
||||
cookieState: new ProxyCookieStateAdapter(),
|
||||
commandLog: new ProxyCommandLogAdapter(),
|
||||
}),
|
||||
config,
|
||||
middleware: defaultMiddleware,
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
||||
import { ProxyCommandLogAdapter } from '../../../lib/adapters/proxy-command-log'
|
||||
import { sendToDriver } from '../../../lib/adapters/send-to-driver'
|
||||
|
||||
vi.mock('../../../lib/adapters/send-to-driver', () => {
|
||||
return {
|
||||
sendToDriver: vi.fn(),
|
||||
}
|
||||
})
|
||||
|
||||
describe('ProxyCommandLogAdapter', () => {
|
||||
beforeEach(() => {
|
||||
vi.mocked(sendToDriver).mockReset()
|
||||
})
|
||||
|
||||
it('delegates notifyIncomingRequest to sendToDriver helper', () => {
|
||||
const adapter = new ProxyCommandLogAdapter()
|
||||
const ctx = { req: { browserPreRequest: { requestId: '1' } } }
|
||||
|
||||
adapter.notifyIncomingRequest(ctx)
|
||||
|
||||
expect(sendToDriver).toHaveBeenCalledWith(ctx)
|
||||
})
|
||||
|
||||
it('returns undefined from logInterception on the server', () => {
|
||||
const adapter = new ProxyCommandLogAdapter()
|
||||
|
||||
expect(adapter.logInterception({ interception: {}, route: {} })).toBeUndefined()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,45 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
||||
import { ProxyCookieStateAdapter } from '../../../lib/adapters/proxy-cookie-state'
|
||||
import { attachCrossOriginCookies } from '../../../lib/adapters/attach-cross-origin-cookies'
|
||||
import { copyCookiesFromResponse } from '../../../lib/adapters/copy-cookies-from-response'
|
||||
|
||||
vi.mock('../../../lib/adapters/attach-cross-origin-cookies', () => {
|
||||
return {
|
||||
attachCrossOriginCookies: vi.fn(),
|
||||
}
|
||||
})
|
||||
|
||||
vi.mock('../../../lib/adapters/copy-cookies-from-response', () => {
|
||||
return {
|
||||
copyCookiesFromResponse: vi.fn(),
|
||||
}
|
||||
})
|
||||
|
||||
describe('ProxyCookieStateAdapter', () => {
|
||||
beforeEach(() => {
|
||||
vi.mocked(attachCrossOriginCookies).mockReset()
|
||||
vi.mocked(copyCookiesFromResponse).mockReset()
|
||||
})
|
||||
|
||||
it('delegates attachCrossOriginCookies to helper', async () => {
|
||||
const adapter = new ProxyCookieStateAdapter()
|
||||
const ctx = { req: {} }
|
||||
|
||||
vi.mocked(attachCrossOriginCookies).mockResolvedValue(undefined)
|
||||
|
||||
await adapter.attachCrossOriginCookies(ctx)
|
||||
|
||||
expect(attachCrossOriginCookies).toHaveBeenCalledWith(ctx)
|
||||
})
|
||||
|
||||
it('delegates copyCookiesFromResponse to helper', async () => {
|
||||
const adapter = new ProxyCookieStateAdapter()
|
||||
const ctx = { req: {} }
|
||||
|
||||
vi.mocked(copyCookiesFromResponse).mockResolvedValue(undefined)
|
||||
|
||||
await adapter.copyCookiesFromResponse(ctx)
|
||||
|
||||
expect(copyCookiesFromResponse).toHaveBeenCalledWith(ctx)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,37 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
||||
import { ProxyNetworkCaptureAdapter } from '../../../lib/adapters/proxy-network-capture'
|
||||
import { notifyResponseEndedWithEmptyBody, notifyResponseStreamReceived } from '../../../lib/adapters/network-capture'
|
||||
|
||||
vi.mock('../../../lib/adapters/network-capture', () => {
|
||||
return {
|
||||
notifyResponseStreamReceived: vi.fn(),
|
||||
notifyResponseEndedWithEmptyBody: vi.fn(),
|
||||
}
|
||||
})
|
||||
|
||||
describe('ProxyNetworkCaptureAdapter', () => {
|
||||
beforeEach(() => {
|
||||
vi.mocked(notifyResponseStreamReceived).mockReset()
|
||||
vi.mocked(notifyResponseEndedWithEmptyBody).mockReset()
|
||||
})
|
||||
|
||||
it('delegates notifyResponseStreamReceived to helper', async () => {
|
||||
const adapter = new ProxyNetworkCaptureAdapter()
|
||||
const ctx = { req: { requestId: '1' } }
|
||||
|
||||
vi.mocked(notifyResponseStreamReceived).mockResolvedValue(undefined)
|
||||
|
||||
await adapter.notifyResponseStreamReceived(ctx)
|
||||
|
||||
expect(notifyResponseStreamReceived).toHaveBeenCalledWith(ctx)
|
||||
})
|
||||
|
||||
it('delegates notifyResponseEndedWithEmptyBody to helper', () => {
|
||||
const adapter = new ProxyNetworkCaptureAdapter()
|
||||
const ctx = { req: { requestId: '1' } }
|
||||
|
||||
adapter.notifyResponseEndedWithEmptyBody(ctx, { isCached: true })
|
||||
|
||||
expect(notifyResponseEndedWithEmptyBody).toHaveBeenCalledWith(ctx, { isCached: true })
|
||||
})
|
||||
})
|
||||
@@ -1,7 +1,10 @@
|
||||
import { HttpMiddleware, HttpStages, _runStage } from '../../../lib/http'
|
||||
import { NetworkPolicyCore } from '@packages/network-policy'
|
||||
import {
|
||||
ProxyCommandLogAdapter,
|
||||
ProxyCookieStateAdapter,
|
||||
ProxyDocumentPreparationAdapter,
|
||||
ProxyNetworkCaptureAdapter,
|
||||
ProxyRequestInterceptionAdapter,
|
||||
ProxyResponseInterceptionAdapter,
|
||||
} from '../../../lib/adapters'
|
||||
@@ -11,6 +14,9 @@ export function createTestNetworkPolicyCore () {
|
||||
requestInterception: new ProxyRequestInterceptionAdapter(),
|
||||
responseInterception: new ProxyResponseInterceptionAdapter(),
|
||||
documentPreparation: new ProxyDocumentPreparationAdapter(),
|
||||
networkCapture: new ProxyNetworkCaptureAdapter(),
|
||||
cookieState: new ProxyCookieStateAdapter(),
|
||||
commandLog: new ProxyCommandLogAdapter(),
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import type EventEmitter from 'events'
|
||||
import { NetworkProxy, BrowserPreRequest } from '@packages/proxy'
|
||||
import {
|
||||
ProxyCommandLogAdapter,
|
||||
ProxyCookieStateAdapter,
|
||||
ProxyDocumentPreparationAdapter,
|
||||
ProxyNetworkCaptureAdapter,
|
||||
ProxyRequestInterceptionAdapter,
|
||||
ProxyResponseInterceptionAdapter,
|
||||
} from '@packages/proxy/lib/adapters'
|
||||
@@ -47,6 +50,9 @@ export function createProxyRuntime (deps: CreateProxyRuntimeDeps): ProxyNetworkR
|
||||
requestInterception: new ProxyRequestInterceptionAdapter(),
|
||||
responseInterception: new ProxyResponseInterceptionAdapter(),
|
||||
documentPreparation: new ProxyDocumentPreparationAdapter(),
|
||||
networkCapture: new ProxyNetworkCaptureAdapter(),
|
||||
cookieState: new ProxyCookieStateAdapter(),
|
||||
commandLog: new ProxyCommandLogAdapter(),
|
||||
})
|
||||
|
||||
registerDefaultNetworkPolicies(networkPolicyRegistration, deps.config)
|
||||
|
||||
@@ -51,6 +51,9 @@ describe('lib/network-runtime', () => {
|
||||
expect(runtime.networkPolicyCore.requestInterception).to.exist
|
||||
expect(runtime.networkPolicyCore.responseInterception).to.exist
|
||||
expect(runtime.networkPolicyCore.documentPreparation).to.exist
|
||||
expect(runtime.networkPolicyCore.networkCapture).to.exist
|
||||
expect(runtime.networkPolicyCore.cookieState).to.exist
|
||||
expect(runtime.networkPolicyCore.commandLog).to.exist
|
||||
})
|
||||
|
||||
it('registers configurator CSP and document rewrite policies at startup', () => {
|
||||
|
||||
Reference in New Issue
Block a user