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 * as bidi from './third_party/bidiProtocol';
|
||||
import type { BidiSession } from './bidiConnection';
|
||||
import { parseRawCookie } from '../cookieStore';
|
||||
|
||||
|
||||
export class BidiNetworkManager {
|
||||
|
|
@ -68,7 +69,7 @@ export class BidiNetworkManager {
|
|||
if (redirectedFrom) {
|
||||
this._session.sendMayFail('network.continueRequest', {
|
||||
request: param.request.request,
|
||||
headers: redirectedFrom._originalRequestRoute?._alreadyContinuedHeaders,
|
||||
...(redirectedFrom._originalRequestRoute?._alreadyContinuedHeaders || {}),
|
||||
});
|
||||
} else {
|
||||
route = new BidiRouteImpl(this._session, param.request.request);
|
||||
|
|
@ -245,7 +246,7 @@ class BidiRouteImpl implements network.RouteDelegate {
|
|||
private _requestId: bidi.Network.Request;
|
||||
private _session: BidiSession;
|
||||
private _request!: network.Request;
|
||||
_alreadyContinuedHeaders: bidi.Network.Header[] | undefined;
|
||||
_alreadyContinuedHeaders: types.HeadersArray | undefined;
|
||||
|
||||
constructor(session: BidiSession, requestId: bidi.Network.Request) {
|
||||
this._session = session;
|
||||
|
|
@ -266,13 +267,12 @@ class BidiRouteImpl implements network.RouteDelegate {
|
|||
return header;
|
||||
});
|
||||
}
|
||||
this._alreadyContinuedHeaders = toBidiHeaders(headers);
|
||||
this._alreadyContinuedHeaders = headers;
|
||||
await this._session.sendMayFail('network.continueRequest', {
|
||||
request: this._requestId,
|
||||
url: overrides.url,
|
||||
method: overrides.method,
|
||||
// TODO: cookies!
|
||||
headers: this._alreadyContinuedHeaders,
|
||||
...toBidiRequestHeaders(this._alreadyContinuedHeaders),
|
||||
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,
|
||||
statusCode: response.status,
|
||||
reasonPhrase: network.statusText(response.status),
|
||||
headers: toBidiHeaders(response.headers),
|
||||
...toBidiResponseHeaders(response.headers),
|
||||
body: { type: 'base64', value: base64body },
|
||||
});
|
||||
}
|
||||
|
|
@ -302,6 +302,27 @@ function fromBidiHeaders(bidiHeaders: bidi.Network.Header[]): types.HeadersArray
|
|||
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[] {
|
||||
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;
|
||||
|
||||
}
|
||||
|
||||
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 { kMaxCookieExpiresDateInSeconds } from './network';
|
||||
|
||||
class Cookie {
|
||||
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 {
|
||||
if (value === domain)
|
||||
return true;
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ import { getUserAgent } from '../utils/userAgent';
|
|||
import { assert, createGuid, monotonicTime } from '../utils';
|
||||
import { HttpsProxyAgent, SocksProxyAgent } from '../utilsBundle';
|
||||
import { BrowserContext, verifyClientCertificates } from './browserContext';
|
||||
import { CookieStore, domainMatches } from './cookieStore';
|
||||
import { CookieStore, domainMatches, parseRawCookie } from './cookieStore';
|
||||
import { MultipartFormData } from './formData';
|
||||
import { httpHappyEyeballsAgent, httpsHappyEyeballsAgent } from '../utils/happy-eyeballs';
|
||||
import type { CallMetadata } from './instrumentation';
|
||||
|
|
@ -39,7 +39,6 @@ import { ProgressController } from './progress';
|
|||
import { Tracing } from './trace/recorder/tracing';
|
||||
import type * as types from './types';
|
||||
import type { HeadersArray, ProxySettings } from './types';
|
||||
import { kMaxCookieExpiresDateInSeconds } from './network';
|
||||
import { getMatchingTLSOptionsForOrigin, rewriteOpenSSLErrorIfNeeded } from './socksClientCertificatesInterceptor';
|
||||
|
||||
type FetchRequestOptions = {
|
||||
|
|
@ -640,27 +639,10 @@ function toHeadersArray(rawHeaders: string[]): types.HeadersArray {
|
|||
const redirectStatus = [301, 302, 303, 307, 308];
|
||||
|
||||
function parseCookie(header: string): channels.NetworkCookie | 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)
|
||||
const raw = parseRawCookie(header);
|
||||
if (!raw)
|
||||
return null;
|
||||
const [name, value] = pairs[0];
|
||||
const cookie: channels.NetworkCookie = {
|
||||
name,
|
||||
value,
|
||||
domain: '',
|
||||
path: '',
|
||||
expires: -1,
|
||||
|
|
@ -668,62 +650,9 @@ function parseCookie(header: string): channels.NetworkCookie | null {
|
|||
secure: false,
|
||||
// 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.
|
||||
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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -62,21 +62,21 @@ const test = baseTest.extend<BrowserTestTestFixtures, BrowserTestWorkerFixtures>
|
|||
await run(playwright[browserName]);
|
||||
}, { scope: 'worker' }],
|
||||
|
||||
allowsThirdParty: [async ({ browserName, browserMajorVersion, channel }, run) => {
|
||||
allowsThirdParty: [async ({ browserName }, run) => {
|
||||
if (browserName === 'firefox')
|
||||
await run(true);
|
||||
else
|
||||
await run(false);
|
||||
}, { scope: 'worker' }],
|
||||
|
||||
defaultSameSiteCookieValue: [async ({ browserName, browserMajorVersion, channel, isLinux }, run) => {
|
||||
if (browserName === 'chromium')
|
||||
defaultSameSiteCookieValue: [async ({ browserName, isLinux }, run) => {
|
||||
if (browserName === 'chromium' || browserName as any === '_bidiChromium')
|
||||
await run('Lax');
|
||||
else if (browserName === 'webkit' && isLinux)
|
||||
await run('Lax');
|
||||
else if (browserName === 'webkit' && !isLinux)
|
||||
await run('None');
|
||||
else if (browserName === 'firefox')
|
||||
else if (browserName === 'firefox' || browserName as any === '_bidiFirefox')
|
||||
await run('None');
|
||||
else
|
||||
throw new Error('unknown browser - ' + browserName);
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ export type PageWorkerFixtures = {
|
|||
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';
|
||||
video: VideoMode | { mode: VideoMode, size: ViewportSize };
|
||||
browserName: 'chromium' | 'firefox' | 'webkit';
|
||||
browserName: 'chromium' | 'firefox' | 'webkit' | '_bidiFirefox' | '_bidiChromium';
|
||||
browserVersion: string;
|
||||
browserMajorVersion: number;
|
||||
electronMajorVersion: number;
|
||||
|
|
|
|||
Loading…
Reference in a new issue