chore: cookies in intercepted bidi requests (#32623)
This commit is contained in:
parent
f2a974b045
commit
34876e9291
|
|
@ -22,6 +22,7 @@ import type * as frames from '../frames';
|
||||||
import type * as types from '../types';
|
import type * as types from '../types';
|
||||||
import * as bidi from './third_party/bidiProtocol';
|
import * as bidi from './third_party/bidiProtocol';
|
||||||
import type { BidiSession } from './bidiConnection';
|
import type { BidiSession } from './bidiConnection';
|
||||||
|
import { parseRawCookie } from '../cookieStore';
|
||||||
|
|
||||||
|
|
||||||
export class BidiNetworkManager {
|
export class BidiNetworkManager {
|
||||||
|
|
@ -68,7 +69,7 @@ export class BidiNetworkManager {
|
||||||
if (redirectedFrom) {
|
if (redirectedFrom) {
|
||||||
this._session.sendMayFail('network.continueRequest', {
|
this._session.sendMayFail('network.continueRequest', {
|
||||||
request: param.request.request,
|
request: param.request.request,
|
||||||
headers: redirectedFrom._originalRequestRoute?._alreadyContinuedHeaders,
|
...(redirectedFrom._originalRequestRoute?._alreadyContinuedHeaders || {}),
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
route = new BidiRouteImpl(this._session, param.request.request);
|
route = new BidiRouteImpl(this._session, param.request.request);
|
||||||
|
|
@ -245,7 +246,7 @@ class BidiRouteImpl implements network.RouteDelegate {
|
||||||
private _requestId: bidi.Network.Request;
|
private _requestId: bidi.Network.Request;
|
||||||
private _session: BidiSession;
|
private _session: BidiSession;
|
||||||
private _request!: network.Request;
|
private _request!: network.Request;
|
||||||
_alreadyContinuedHeaders: bidi.Network.Header[] | undefined;
|
_alreadyContinuedHeaders: types.HeadersArray | undefined;
|
||||||
|
|
||||||
constructor(session: BidiSession, requestId: bidi.Network.Request) {
|
constructor(session: BidiSession, requestId: bidi.Network.Request) {
|
||||||
this._session = session;
|
this._session = session;
|
||||||
|
|
@ -266,13 +267,12 @@ class BidiRouteImpl implements network.RouteDelegate {
|
||||||
return header;
|
return header;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
this._alreadyContinuedHeaders = toBidiHeaders(headers);
|
this._alreadyContinuedHeaders = headers;
|
||||||
await this._session.sendMayFail('network.continueRequest', {
|
await this._session.sendMayFail('network.continueRequest', {
|
||||||
request: this._requestId,
|
request: this._requestId,
|
||||||
url: overrides.url,
|
url: overrides.url,
|
||||||
method: overrides.method,
|
method: overrides.method,
|
||||||
// TODO: cookies!
|
...toBidiRequestHeaders(this._alreadyContinuedHeaders),
|
||||||
headers: this._alreadyContinuedHeaders,
|
|
||||||
body: overrides.postData ? { type: 'base64', value: Buffer.from(overrides.postData).toString('base64') } : undefined,
|
body: overrides.postData ? { type: 'base64', value: Buffer.from(overrides.postData).toString('base64') } : undefined,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -283,7 +283,7 @@ class BidiRouteImpl implements network.RouteDelegate {
|
||||||
request: this._requestId,
|
request: this._requestId,
|
||||||
statusCode: response.status,
|
statusCode: response.status,
|
||||||
reasonPhrase: network.statusText(response.status),
|
reasonPhrase: network.statusText(response.status),
|
||||||
headers: toBidiHeaders(response.headers),
|
...toBidiResponseHeaders(response.headers),
|
||||||
body: { type: 'base64', value: base64body },
|
body: { type: 'base64', value: base64body },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -302,6 +302,27 @@ function fromBidiHeaders(bidiHeaders: bidi.Network.Header[]): types.HeadersArray
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function toBidiRequestHeaders(allHeaders: types.HeadersArray): { cookies: bidi.Network.CookieHeader[], headers: bidi.Network.Header[] } {
|
||||||
|
const bidiHeaders = toBidiHeaders(allHeaders);
|
||||||
|
const cookies = bidiHeaders.filter(h => h.name.toLowerCase() === 'cookie');
|
||||||
|
const headers = bidiHeaders.filter(h => h.name.toLowerCase() !== 'cookie');
|
||||||
|
return { cookies, headers };
|
||||||
|
}
|
||||||
|
|
||||||
|
function toBidiResponseHeaders(headers: types.HeadersArray): { cookies: bidi.Network.SetCookieHeader[], headers: bidi.Network.Header[] } {
|
||||||
|
const setCookieHeaders = headers.filter(h => h.name.toLowerCase() === 'set-cookie');
|
||||||
|
const otherHeaders = headers.filter(h => h.name.toLowerCase() !== 'set-cookie');
|
||||||
|
const rawCookies = setCookieHeaders.map(h => parseRawCookie(h.value));
|
||||||
|
const cookies: bidi.Network.SetCookieHeader[] = rawCookies.filter(Boolean).map(c => {
|
||||||
|
return {
|
||||||
|
...c!,
|
||||||
|
value: { type: 'string', value: c!.value },
|
||||||
|
sameSite: toBidiSameSite(c!.sameSite),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
return { cookies, headers: toBidiHeaders(otherHeaders) };
|
||||||
|
}
|
||||||
|
|
||||||
function toBidiHeaders(headers: types.HeadersArray): bidi.Network.Header[] {
|
function toBidiHeaders(headers: types.HeadersArray): bidi.Network.Header[] {
|
||||||
return headers.map(({ name, value }) => ({ name, value: { type: 'string', value } }));
|
return headers.map(({ name, value }) => ({ name, value: { type: 'string', value } }));
|
||||||
}
|
}
|
||||||
|
|
@ -314,3 +335,13 @@ export function bidiBytesValueToString(value: bidi.Network.BytesValue): string {
|
||||||
return 'unknown value type: ' + (value as any).type;
|
return 'unknown value type: ' + (value as any).type;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function toBidiSameSite(sameSite?: 'Strict' | 'Lax' | 'None'): bidi.Network.SameSite | undefined {
|
||||||
|
if (!sameSite)
|
||||||
|
return undefined;
|
||||||
|
if (sameSite === 'Strict')
|
||||||
|
return bidi.Network.SameSite.Strict;
|
||||||
|
if (sameSite === 'Lax')
|
||||||
|
return bidi.Network.SameSite.Lax;
|
||||||
|
return bidi.Network.SameSite.None;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type * as channels from '@protocol/channels';
|
import type * as channels from '@protocol/channels';
|
||||||
|
import { kMaxCookieExpiresDateInSeconds } from './network';
|
||||||
|
|
||||||
class Cookie {
|
class Cookie {
|
||||||
private _raw: channels.NetworkCookie;
|
private _raw: channels.NetworkCookie;
|
||||||
|
|
@ -115,6 +116,97 @@ export class CookieStore {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type RawCookie = {
|
||||||
|
name: string,
|
||||||
|
value: string,
|
||||||
|
domain?: string,
|
||||||
|
path?: string,
|
||||||
|
expires?: number,
|
||||||
|
httpOnly?: boolean,
|
||||||
|
secure?: boolean,
|
||||||
|
sameSite?: 'Strict' | 'Lax' | 'None',
|
||||||
|
};
|
||||||
|
|
||||||
|
export function parseRawCookie(header: string): RawCookie | null {
|
||||||
|
const pairs = header.split(';').filter(s => s.trim().length > 0).map(p => {
|
||||||
|
let key = '';
|
||||||
|
let value = '';
|
||||||
|
const separatorPos = p.indexOf('=');
|
||||||
|
if (separatorPos === -1) {
|
||||||
|
// If only a key is specified, the value is left undefined.
|
||||||
|
key = p.trim();
|
||||||
|
} else {
|
||||||
|
// Otherwise we assume that the key is the element before the first `=`
|
||||||
|
key = p.slice(0, separatorPos).trim();
|
||||||
|
// And the value is the rest of the string.
|
||||||
|
value = p.slice(separatorPos + 1).trim();
|
||||||
|
}
|
||||||
|
return [key, value];
|
||||||
|
});
|
||||||
|
if (!pairs.length)
|
||||||
|
return null;
|
||||||
|
const [name, value] = pairs[0];
|
||||||
|
const cookie: RawCookie = {
|
||||||
|
name,
|
||||||
|
value,
|
||||||
|
};
|
||||||
|
for (let i = 1; i < pairs.length; i++) {
|
||||||
|
const [name, value] = pairs[i];
|
||||||
|
switch (name.toLowerCase()) {
|
||||||
|
case 'expires':
|
||||||
|
const expiresMs = (+new Date(value));
|
||||||
|
// https://datatracker.ietf.org/doc/html/rfc6265#section-5.2.1
|
||||||
|
if (isFinite(expiresMs)) {
|
||||||
|
if (expiresMs <= 0)
|
||||||
|
cookie.expires = 0;
|
||||||
|
else
|
||||||
|
cookie.expires = Math.min(expiresMs / 1000, kMaxCookieExpiresDateInSeconds);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'max-age':
|
||||||
|
const maxAgeSec = parseInt(value, 10);
|
||||||
|
if (isFinite(maxAgeSec)) {
|
||||||
|
// From https://datatracker.ietf.org/doc/html/rfc6265#section-5.2.2
|
||||||
|
// If delta-seconds is less than or equal to zero (0), let expiry-time
|
||||||
|
// be the earliest representable date and time.
|
||||||
|
if (maxAgeSec <= 0)
|
||||||
|
cookie.expires = 0;
|
||||||
|
else
|
||||||
|
cookie.expires = Math.min(Date.now() / 1000 + maxAgeSec, kMaxCookieExpiresDateInSeconds);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'domain':
|
||||||
|
cookie.domain = value.toLocaleLowerCase() || '';
|
||||||
|
if (cookie.domain && !cookie.domain.startsWith('.') && cookie.domain.includes('.'))
|
||||||
|
cookie.domain = '.' + cookie.domain;
|
||||||
|
break;
|
||||||
|
case 'path':
|
||||||
|
cookie.path = value || '';
|
||||||
|
break;
|
||||||
|
case 'secure':
|
||||||
|
cookie.secure = true;
|
||||||
|
break;
|
||||||
|
case 'httponly':
|
||||||
|
cookie.httpOnly = true;
|
||||||
|
break;
|
||||||
|
case 'samesite':
|
||||||
|
switch (value.toLowerCase()) {
|
||||||
|
case 'none':
|
||||||
|
cookie.sameSite = 'None';
|
||||||
|
break;
|
||||||
|
case 'lax':
|
||||||
|
cookie.sameSite = 'Lax';
|
||||||
|
break;
|
||||||
|
case 'strict':
|
||||||
|
cookie.sameSite = 'Strict';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return cookie;
|
||||||
|
}
|
||||||
|
|
||||||
export function domainMatches(value: string, domain: string): boolean {
|
export function domainMatches(value: string, domain: string): boolean {
|
||||||
if (value === domain)
|
if (value === domain)
|
||||||
return true;
|
return true;
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,7 @@ import { getUserAgent } from '../utils/userAgent';
|
||||||
import { assert, createGuid, monotonicTime } from '../utils';
|
import { assert, createGuid, monotonicTime } from '../utils';
|
||||||
import { HttpsProxyAgent, SocksProxyAgent } from '../utilsBundle';
|
import { HttpsProxyAgent, SocksProxyAgent } from '../utilsBundle';
|
||||||
import { BrowserContext, verifyClientCertificates } from './browserContext';
|
import { BrowserContext, verifyClientCertificates } from './browserContext';
|
||||||
import { CookieStore, domainMatches } from './cookieStore';
|
import { CookieStore, domainMatches, parseRawCookie } from './cookieStore';
|
||||||
import { MultipartFormData } from './formData';
|
import { MultipartFormData } from './formData';
|
||||||
import { httpHappyEyeballsAgent, httpsHappyEyeballsAgent } from '../utils/happy-eyeballs';
|
import { httpHappyEyeballsAgent, httpsHappyEyeballsAgent } from '../utils/happy-eyeballs';
|
||||||
import type { CallMetadata } from './instrumentation';
|
import type { CallMetadata } from './instrumentation';
|
||||||
|
|
@ -39,7 +39,6 @@ import { ProgressController } from './progress';
|
||||||
import { Tracing } from './trace/recorder/tracing';
|
import { Tracing } from './trace/recorder/tracing';
|
||||||
import type * as types from './types';
|
import type * as types from './types';
|
||||||
import type { HeadersArray, ProxySettings } from './types';
|
import type { HeadersArray, ProxySettings } from './types';
|
||||||
import { kMaxCookieExpiresDateInSeconds } from './network';
|
|
||||||
import { getMatchingTLSOptionsForOrigin, rewriteOpenSSLErrorIfNeeded } from './socksClientCertificatesInterceptor';
|
import { getMatchingTLSOptionsForOrigin, rewriteOpenSSLErrorIfNeeded } from './socksClientCertificatesInterceptor';
|
||||||
|
|
||||||
type FetchRequestOptions = {
|
type FetchRequestOptions = {
|
||||||
|
|
@ -640,27 +639,10 @@ function toHeadersArray(rawHeaders: string[]): types.HeadersArray {
|
||||||
const redirectStatus = [301, 302, 303, 307, 308];
|
const redirectStatus = [301, 302, 303, 307, 308];
|
||||||
|
|
||||||
function parseCookie(header: string): channels.NetworkCookie | null {
|
function parseCookie(header: string): channels.NetworkCookie | null {
|
||||||
const pairs = header.split(';').filter(s => s.trim().length > 0).map(p => {
|
const raw = parseRawCookie(header);
|
||||||
let key = '';
|
if (!raw)
|
||||||
let value = '';
|
|
||||||
const separatorPos = p.indexOf('=');
|
|
||||||
if (separatorPos === -1) {
|
|
||||||
// If only a key is specified, the value is left undefined.
|
|
||||||
key = p.trim();
|
|
||||||
} else {
|
|
||||||
// Otherwise we assume that the key is the element before the first `=`
|
|
||||||
key = p.slice(0, separatorPos).trim();
|
|
||||||
// And the value is the rest of the string.
|
|
||||||
value = p.slice(separatorPos + 1).trim();
|
|
||||||
}
|
|
||||||
return [key, value];
|
|
||||||
});
|
|
||||||
if (!pairs.length)
|
|
||||||
return null;
|
return null;
|
||||||
const [name, value] = pairs[0];
|
|
||||||
const cookie: channels.NetworkCookie = {
|
const cookie: channels.NetworkCookie = {
|
||||||
name,
|
|
||||||
value,
|
|
||||||
domain: '',
|
domain: '',
|
||||||
path: '',
|
path: '',
|
||||||
expires: -1,
|
expires: -1,
|
||||||
|
|
@ -668,62 +650,9 @@ function parseCookie(header: string): channels.NetworkCookie | null {
|
||||||
secure: false,
|
secure: false,
|
||||||
// From https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie/SameSite
|
// From https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie/SameSite
|
||||||
// The cookie-sending behavior if SameSite is not specified is SameSite=Lax.
|
// The cookie-sending behavior if SameSite is not specified is SameSite=Lax.
|
||||||
sameSite: 'Lax'
|
sameSite: 'Lax',
|
||||||
|
...raw
|
||||||
};
|
};
|
||||||
for (let i = 1; i < pairs.length; i++) {
|
|
||||||
const [name, value] = pairs[i];
|
|
||||||
switch (name.toLowerCase()) {
|
|
||||||
case 'expires':
|
|
||||||
const expiresMs = (+new Date(value));
|
|
||||||
// https://datatracker.ietf.org/doc/html/rfc6265#section-5.2.1
|
|
||||||
if (isFinite(expiresMs)) {
|
|
||||||
if (expiresMs <= 0)
|
|
||||||
cookie.expires = 0;
|
|
||||||
else
|
|
||||||
cookie.expires = Math.min(expiresMs / 1000, kMaxCookieExpiresDateInSeconds);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case 'max-age':
|
|
||||||
const maxAgeSec = parseInt(value, 10);
|
|
||||||
if (isFinite(maxAgeSec)) {
|
|
||||||
// From https://datatracker.ietf.org/doc/html/rfc6265#section-5.2.2
|
|
||||||
// If delta-seconds is less than or equal to zero (0), let expiry-time
|
|
||||||
// be the earliest representable date and time.
|
|
||||||
if (maxAgeSec <= 0)
|
|
||||||
cookie.expires = 0;
|
|
||||||
else
|
|
||||||
cookie.expires = Math.min(Date.now() / 1000 + maxAgeSec, kMaxCookieExpiresDateInSeconds);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case 'domain':
|
|
||||||
cookie.domain = value.toLocaleLowerCase() || '';
|
|
||||||
if (cookie.domain && !cookie.domain.startsWith('.') && cookie.domain.includes('.'))
|
|
||||||
cookie.domain = '.' + cookie.domain;
|
|
||||||
break;
|
|
||||||
case 'path':
|
|
||||||
cookie.path = value || '';
|
|
||||||
break;
|
|
||||||
case 'secure':
|
|
||||||
cookie.secure = true;
|
|
||||||
break;
|
|
||||||
case 'httponly':
|
|
||||||
cookie.httpOnly = true;
|
|
||||||
break;
|
|
||||||
case 'samesite':
|
|
||||||
switch (value.toLowerCase()) {
|
|
||||||
case 'none':
|
|
||||||
cookie.sameSite = 'None';
|
|
||||||
break;
|
|
||||||
case 'lax':
|
|
||||||
cookie.sameSite = 'Lax';
|
|
||||||
break;
|
|
||||||
case 'strict':
|
|
||||||
cookie.sameSite = 'Strict';
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return cookie;
|
return cookie;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -62,21 +62,21 @@ const test = baseTest.extend<BrowserTestTestFixtures, BrowserTestWorkerFixtures>
|
||||||
await run(playwright[browserName]);
|
await run(playwright[browserName]);
|
||||||
}, { scope: 'worker' }],
|
}, { scope: 'worker' }],
|
||||||
|
|
||||||
allowsThirdParty: [async ({ browserName, browserMajorVersion, channel }, run) => {
|
allowsThirdParty: [async ({ browserName }, run) => {
|
||||||
if (browserName === 'firefox')
|
if (browserName === 'firefox')
|
||||||
await run(true);
|
await run(true);
|
||||||
else
|
else
|
||||||
await run(false);
|
await run(false);
|
||||||
}, { scope: 'worker' }],
|
}, { scope: 'worker' }],
|
||||||
|
|
||||||
defaultSameSiteCookieValue: [async ({ browserName, browserMajorVersion, channel, isLinux }, run) => {
|
defaultSameSiteCookieValue: [async ({ browserName, isLinux }, run) => {
|
||||||
if (browserName === 'chromium')
|
if (browserName === 'chromium' || browserName as any === '_bidiChromium')
|
||||||
await run('Lax');
|
await run('Lax');
|
||||||
else if (browserName === 'webkit' && isLinux)
|
else if (browserName === 'webkit' && isLinux)
|
||||||
await run('Lax');
|
await run('Lax');
|
||||||
else if (browserName === 'webkit' && !isLinux)
|
else if (browserName === 'webkit' && !isLinux)
|
||||||
await run('None');
|
await run('None');
|
||||||
else if (browserName === 'firefox')
|
else if (browserName === 'firefox' || browserName as any === '_bidiFirefox')
|
||||||
await run('None');
|
await run('None');
|
||||||
else
|
else
|
||||||
throw new Error('unknown browser - ' + browserName);
|
throw new Error('unknown browser - ' + browserName);
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,7 @@ export type PageWorkerFixtures = {
|
||||||
screenshot: ScreenshotMode | { mode: ScreenshotMode } & Pick<PageScreenshotOptions, 'fullPage' | 'omitBackground'>;
|
screenshot: ScreenshotMode | { mode: ScreenshotMode } & Pick<PageScreenshotOptions, 'fullPage' | 'omitBackground'>;
|
||||||
trace: 'off' | 'on' | 'retain-on-failure' | 'on-first-retry' | 'retain-on-first-failure' | 'on-all-retries' | /** deprecated */ 'retry-with-trace';
|
trace: 'off' | 'on' | 'retain-on-failure' | 'on-first-retry' | 'retain-on-first-failure' | 'on-all-retries' | /** deprecated */ 'retry-with-trace';
|
||||||
video: VideoMode | { mode: VideoMode, size: ViewportSize };
|
video: VideoMode | { mode: VideoMode, size: ViewportSize };
|
||||||
browserName: 'chromium' | 'firefox' | 'webkit';
|
browserName: 'chromium' | 'firefox' | 'webkit' | '_bidiFirefox' | '_bidiChromium';
|
||||||
browserVersion: string;
|
browserVersion: string;
|
||||||
browserMajorVersion: number;
|
browserMajorVersion: number;
|
||||||
electronMajorVersion: number;
|
electronMajorVersion: number;
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue