chore: cookies in intercepted bidi requests (#32623)

This commit is contained in:
Yury Semikhatsky 2024-09-13 18:29:35 -07:00 committed by GitHub
parent f2a974b045
commit 34876e9291
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 139 additions and 87 deletions

View file

@ -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;
}

View file

@ -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;

View file

@ -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;
} }

View file

@ -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);

View file

@ -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;