Skip to content
Open
10 changes: 5 additions & 5 deletions packages/server/lib/adapters/internal-routes.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
type InternalRouteConfig = {
clientRoute: string
namespace: string
port: number | null
export type InternalRouteConfig = {
clientRoute?: string
namespace?: string
port?: number | null
proxyUrl?: string
socketIoRoute: string
socketIoRoute?: string
// CT Vite/webpack assets live under this prefix (default /__cypress/src).
// They must not be treated as Express-owned internal routes.
devServerPublicPathRoute?: string
Expand Down
8 changes: 2 additions & 6 deletions packages/server/lib/adapters/serve-internal-routes.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,8 @@
import type { HttpHeaders, HttpRequest, InterceptMiddleware } from '@packages/network-interception'
import type CyServer from '../../index.d.ts'
import type { Request as ServerRequest } from '../request'
import { CYPRESS_INTERNAL_LOOPBACK_HEADER, isInternalCypressRoute, resolveProxyUrlBase } from './internal-routes'
import { CYPRESS_INTERNAL_LOOPBACK_HEADER, isInternalCypressRoute, resolveProxyUrlBase, type InternalRouteConfig } from './internal-routes'

type ServeInternalRoutesConfig = Pick<
CyServer.Config & Cypress.Config,
'clientRoute' | 'devServerPublicPathRoute' | 'namespace' | 'port' | 'proxyUrl' | 'socketIoRoute'
>
export type ServeInternalRoutesConfig = InternalRouteConfig

type CreateServeInternalRoutesMiddlewareOptions = {
config: ServeInternalRoutesConfig
Expand Down
62 changes: 59 additions & 3 deletions packages/server/lib/browsers/cdp-protocol/cdp-fetch-transport.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,11 @@ type CdpFetchClient = Pick<ICriClient, 'send' | 'on' | 'off'>

type CdpFetchRequest = Protocol.Fetch.RequestPausedEvent['request']
const RESPONSE_PAUSE_TIMEOUT_MS = 30000
const AUT_FRAME_HEADER = 'X-Cypress-Is-AUT-Frame'

type CdpFetchTransportOptions = {
isAUTFrame?: (frameId: string) => Promise<boolean>
}

export interface CdpFetchTransportRequest extends CdpFetchRequest {
id: string
Expand All @@ -37,6 +42,7 @@ export class CdpFetchTransport {
constructor (
private readonly client: CdpFetchClient,
private readonly httpIntercept: ForHttpIntercept<CdpFetchTransportRequest, CdpFetchTransportResponse> = new HttpIntercept(createCdpFetchCodec()),
private readonly options: CdpFetchTransportOptions = {},
) {}

/**
Expand Down Expand Up @@ -77,6 +83,14 @@ export class CdpFetchTransport {
}
}

/**
* Clears in-flight request correlation without disabling CDP Fetch.
* Used between tests so the next test still receives paused traffic.
*/
async reset (): Promise<void> {
this.rejectAll(new Error('CDP Fetch transport reset'))
}

async stop (): Promise<void> {
if (!this.isStarted) {
return
Expand Down Expand Up @@ -109,6 +123,7 @@ export class CdpFetchTransport {
debug('continuing request pause without network id: %s', event.request.url)
await this.safeSend('Fetch.continueRequest', {
requestId: event.requestId,
...(await this.autFrameHeader(event)),
}, sessionId)

return
Expand All @@ -131,14 +146,14 @@ export class CdpFetchTransport {
this.inFlightRequests.set(networkId, deferred)

response = await this.httpIntercept.handle(request, async (outbound) => {
const headers = await this.continueRequestHeaders(event, outbound)

await this.client.send('Fetch.continueRequest', {
requestId: event.requestId,
...(outbound.url !== event.request.url ? { url: outbound.url } : {}),
...(outbound.method !== event.request.method ? { method: outbound.method } : {}),
...(outbound.postData !== event.request.postData ? { postData: outbound.postData } : {}),
...(this.headersChanged(outbound.headers ?? {}, event.request.headers)
? { headers: this.toContinueRequestHeaders(outbound.headers ?? {}) }
: {}),
...(headers ? { headers } : {}),
}, outbound.sessionId)

requestContinued = true
Expand Down Expand Up @@ -330,6 +345,47 @@ export class CdpFetchTransport {
}
}

private autFrameHeader = async (
event: Protocol.Fetch.RequestPausedEvent,
outbound: CdpFetchTransportRequest = {
...event.request,
id: event.networkId ?? event.requestId,
requestId: event.requestId,
},
): Promise<Pick<Protocol.Fetch.ContinueRequestRequest, 'headers'>> => {
const headers = await this.continueRequestHeaders(event, outbound)

return headers ? { headers } : {}
}

/**
* Builds continueRequest headers when the outbound headers changed or the
* request is from the AUT frame. Always preserves X-Cypress-Is-AUT-Frame
* alongside any mutated headers (a later headers overwrite must not drop it).
*/
private continueRequestHeaders = async (
event: Protocol.Fetch.RequestPausedEvent,
outbound: CdpFetchTransportRequest,
): Promise<Protocol.Fetch.HeaderEntry[] | undefined> => {
const isAUTFrame = event.frameId && this.options.isAUTFrame ? await this.options.isAUTFrame(event.frameId) : false
const headersChanged = this.headersChanged(outbound.headers ?? {}, event.request.headers)

if (!isAUTFrame && !headersChanged) {
return
}

const headers = this.toContinueRequestHeaders(outbound.headers ?? {})

if (isAUTFrame) {
headers.push({
name: AUT_FRAME_HEADER,
value: 'true',
})
}

return headers
}

private cleanup (networkId: string, deferred?: pDefer.DeferredPromise<CdpFetchTransportResponse>): void {
if (deferred && this.inFlightRequests.get(networkId) !== deferred) {
return
Expand Down
4 changes: 4 additions & 0 deletions packages/server/lib/browsers/cdp-protocol/cdp_automation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -372,6 +372,10 @@ export class CdpAutomation implements CDPClient, AutomationMiddleware {
return false
}

isAUTFrame = (frameId: string) => {
return this._isAUTFrame(frameId)
}

private _getAutFrame = async () => {
try {
if (this.gettingFrameTree) {
Expand Down
13 changes: 10 additions & 3 deletions packages/server/lib/browsers/chrome.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import memory from './memory'
import type { BrowserLaunchOpts, BrowserNewTabOpts, ProtocolManagerShape, CyPromptManagerShape, StudioManagerShape, RunModeVideoApi } from '@packages/types'
import type { CDPSocketServer } from '@packages/socket'
import { DEFAULT_CHROME_FLAGS } from '../util/chromium_flags'
import { isProxyDisabled } from '../util/is-proxy-disabled'

const debug = debugModule('cypress:server:browsers:chrome')

Expand Down Expand Up @@ -575,10 +576,16 @@ export = {
utils.initializeCDP(pageCriClient, automation),
])

await this._navigateUsingCRI(pageCriClient, url)
if (isProxyDisabled()) {
cdpAutomation._listenForFrameTreeChanges(pageCriClient)
await options.onPageCriClientReady?.(pageCriClient, cdpAutomation.isAUTFrame)

await cdpAutomation._handlePausedRequests(pageCriClient)
cdpAutomation._listenForFrameTreeChanges(pageCriClient)
await this._navigateUsingCRI(pageCriClient, url)
} else {
await this._navigateUsingCRI(pageCriClient, url)
await cdpAutomation._handlePausedRequests(pageCriClient)
cdpAutomation._listenForFrameTreeChanges(pageCriClient)
}

return cdpAutomation
},
Expand Down
17 changes: 14 additions & 3 deletions packages/server/lib/browsers/electron.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import memory from './memory'
import { BrowserCriClient } from './browser-cri-client'
import { getRemoteDebuggingPort } from '../util/electron-app'
import type { CriClient } from './cdp-protocol/cri-client'
import { isProxyDisabled } from '../util/is-proxy-disabled'

// TODO: unmix these two types
type ElectronOpts = Windows.WindowOptions & BrowserLaunchOpts
Expand Down Expand Up @@ -378,9 +379,19 @@ export = {

// Note that these calls have to happen before we load the page so that we don't miss out on any events that happen quickly
if (cdpAutomation) {
// These calls need to happen prior to loading the URL so we can be sure to get the frames as they come in
await cdpAutomation._handlePausedRequests(browserCriClient?.currentlyAttachedTarget)
cdpAutomation._listenForFrameTreeChanges(browserCriClient?.currentlyAttachedTarget)
const pageCriClient = browserCriClient?.currentlyAttachedTarget

if (!pageCriClient) throw new Error('Missing pageCriClient in _launch')

if (isProxyDisabled()) {
// These calls need to happen prior to loading the URL so we can be sure to get the frames as they come in
cdpAutomation._listenForFrameTreeChanges(pageCriClient)
await options.onPageCriClientReady?.(pageCriClient, cdpAutomation.isAUTFrame)
} else {
// These calls need to happen prior to loading the URL so we can be sure to get the frames as they come in
await cdpAutomation._handlePausedRequests(pageCriClient)
cdpAutomation._listenForFrameTreeChanges(pageCriClient)
}
}

await win.loadURL(url)
Expand Down
61 changes: 18 additions & 43 deletions packages/server/lib/network-runtime.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type EventEmitter from 'events'
import { NetworkProxy, BrowserPreRequest, createProxyNetworkInterception, defaultMiddleware, createSyntheticProxyCodec } from '@packages/proxy'
import { NetworkProxy, BrowserPreRequest, createProxyNetworkInterception, defaultMiddleware } from '@packages/proxy'
import { netStubbingState, NetStubbingState } from '@packages/net-stubbing'
import { HttpIntercept, registerDefaultNetworkPolicies } from '@packages/network-interception'
import { HttpIntercept, registerDefaultNetworkPolicies, NetworkInterceptionCore as NetworkInterceptionCoreImpl } from '@packages/network-interception'
import type { NetworkInterceptionRuntime, ForNetworkPolicyRegistration, NetworkInterceptionCore } from '@packages/network-interception'
import { blocked } from '@packages/network'
import type { SocketBroadcaster } from '@packages/socket'
Expand All @@ -15,7 +15,7 @@ import type { ICriClient } from './browsers/cdp-protocol/cri-client'
import { createCdpFetchCodec } from './browsers/cdp-protocol/cdp-fetch-codec'
import { CdpFetchTransport } from './browsers/cdp-protocol/cdp-fetch-transport'
import type { CdpFetchTransportRequest, CdpFetchTransportResponse } from './browsers/cdp-protocol/cdp-fetch-transport'
import { createServeInternalRoutesMiddleware } from './adapters/serve-internal-routes'
import { createServeInternalRoutesMiddleware, type ServeInternalRoutesConfig } from './adapters/serve-internal-routes'

export type CreateProxyRuntimeDeps = {
config: CyServer.Config & Cypress.Config
Expand All @@ -36,19 +36,21 @@ export type ProxyNetworkRuntime = NetworkInterceptionRuntime & {
networkInterceptionCore: NetworkInterceptionCore
}

export type CreateCdpFetchRuntimeDeps = CreateProxyRuntimeDeps & {
export type CreateCdpFetchRuntimeDeps = {
client: Pick<ICriClient, 'send' | 'on' | 'off'>
isAUTFrame?: (frameId: string) => Promise<boolean>
config: ServeInternalRoutesConfig
request: ServerRequest
}

export type CdpFetchNetworkRuntime = {
networkProxy: NetworkProxy
netStubbingState: NetStubbingState
networkPolicyRegistration: ForNetworkPolicyRegistration
networkInterceptionCore: NetworkInterceptionCore
networkInterception: HttpIntercept<CdpFetchTransportRequest, CdpFetchTransportResponse>
fetchTransport: CdpFetchTransport
start (): Promise<void>
reset (): Promise<void>
stop (): Promise<void>
}

/**
Expand Down Expand Up @@ -114,58 +116,28 @@ export function createProxyRuntime (deps: CreateProxyRuntimeDeps): ProxyNetworkR
}

/**
* Composition-root factory for the CDP Fetch network runtime.
*
* This is intentionally not wired into the default proxy runtime yet. The
* HTTP/2 migration can opt into it with an existing cri-client while preserving
* the minimal HttpRequest/HttpResponse transport-neutral contract.
* Composition-root factory for the CDP Fetch network runtime used when the
* MITM proxy is disabled. Keeps the transport-neutral HttpIntercept surface
* without wiring the legacy proxy pipeline.
*/
export function createCdpFetchRuntime (deps: CreateCdpFetchRuntimeDeps): CdpFetchNetworkRuntime {
const stubbingState = netStubbingState()
const networkPolicyRegistration = new ConfiguratorNetworkPolicyAdapter()

registerDefaultNetworkPolicies(networkPolicyRegistration, deps.config, {
matchesBlockedHost: blocked.matches,
})

const networkInterceptionCore = createProxyNetworkInterception({
const networkInterceptionCore = new NetworkInterceptionCoreImpl({
policyRegistration: networkPolicyRegistration,
})
const networkProxy = new NetworkProxy({
config: deps.config,
shouldCorrelatePreRequests: deps.shouldCorrelatePreRequests,
remoteStates: deps.remoteStates,
getFileServerToken: deps.getFileServerToken,
getCookieJar: deps.getCookieJar,
socket: deps.socket,
netStubbingState: stubbingState,
networkInterceptionCore,
request: deps.request,
serverBus: deps.serverBus,
getCurrentBrowser: deps.getCurrentBrowser,
middleware: defaultMiddleware,
getRenderedHTMLOrigins: () => ({}),
})
const networkInterception = new HttpIntercept(createCdpFetchCodec())
const syntheticCodec = createSyntheticProxyCodec({
createMiddlewareContext: (req, res) => networkProxy.http.createMiddlewareContext(req, res),
})

networkInterception.use(createServeInternalRoutesMiddleware({
config: deps.config,
request: deps.request,
}))

networkInterception.use(networkProxy.http.createLegacyProxyPipeline(syntheticCodec))
// Keep the proxy-codec intercept for NetworkProxy.handleHttpRequest; the CDP
// codec intercept is owned by CdpFetchTransport and must not be shared.
networkProxy.withIntercept(new HttpIntercept(networkProxy.codec))

const fetchTransport = new CdpFetchTransport(deps.client, networkInterception)
const fetchTransport = new CdpFetchTransport(deps.client, networkInterception, {
isAUTFrame: deps.isAUTFrame,
})

return {
networkProxy,
netStubbingState: stubbingState,
networkPolicyRegistration,
networkInterceptionCore,
networkInterception,
Expand All @@ -174,6 +146,9 @@ export function createCdpFetchRuntime (deps: CreateCdpFetchRuntimeDeps): CdpFetc
return fetchTransport.start()
},
reset () {
return fetchTransport.reset()
},
stop () {
return fetchTransport.stop()
},
}
Expand Down
5 changes: 5 additions & 0 deletions packages/server/lib/open_project.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,11 @@ export class OpenProject extends EventEmitter {
userAgent: cfg.userAgent,
proxyUrl: cfg.proxyUrl,
...(isProxyEnabled() ? { proxyServer: ensureProxyServer(cfg) } : {}),
...(!isProxyEnabled() ? {
onPageCriClientReady: (client, isAUTFrame) => {
return this.projectBase!.server.createCdpFetchNetworkRuntime(client, isAUTFrame)
},
} : {}),
socketIoRoute: cfg.socketIoRoute,
chromeWebSecurity: cfg.chromeWebSecurity,
isTextTerminal: !!cfg.isTextTerminal,
Expand Down
6 changes: 5 additions & 1 deletion packages/server/lib/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import client from './controllers/client'
import files from './controllers/files'
import * as plugins from './plugins'
import { privilegedCommandsManager } from './privileged-commands/privileged-commands-manager'
import { isProxyDisabled } from './util/is-proxy-disabled'

const debug = Debug('cypress:server:routes')

Expand Down Expand Up @@ -276,7 +277,10 @@ export const createCommonRoutes = ({
}

router.get(clientRoute, (req: Request & { proxiedUrl?: string }, res) => {
const nonProxied = req.proxiedUrl?.startsWith('/') ?? false
// Path-only clientRoute hits are expected when CDP Fetch replaces the
// MITM proxy; only treat them as "not launched through Cypress" when the
// HTTP proxy is supposed to be in use.
const nonProxied = !isProxyDisabled() && (req.proxiedUrl?.startsWith('/') ?? false)

getCtx().actions.app.setBrowserUserAgent(req.headers['user-agent'])

Expand Down
Loading
Loading