Source code

Revision control

Copy as Markdown

Other Tools

/**
* @license
* Copyright 2020 Google Inc.
* SPDX-License-Identifier: Apache-2.0
*/
import type * as Bidi from 'chromium-bidi/lib/cjs/protocol/protocol.js';
import type {CDPSession} from '../api/CDPSession.js';
import type {
ContinueRequestOverrides,
ResponseForRequest,
} from '../api/HTTPRequest.js';
import {
HTTPRequest,
STATUS_TEXTS,
type ResourceType,
handleError,
} from '../api/HTTPRequest.js';
import {PageEvent} from '../api/Page.js';
import {UnsupportedOperation} from '../common/Errors.js';
import {stringToBase64} from '../util/encoding.js';
import type {Request} from './core/Request.js';
import type {BidiFrame} from './Frame.js';
import {BidiHTTPResponse} from './HTTPResponse.js';
export const requests = new WeakMap<Request, BidiHTTPRequest>();
/**
* @internal
*/
export class BidiHTTPRequest extends HTTPRequest {
static from(
bidiRequest: Request,
frame: BidiFrame,
redirect?: BidiHTTPRequest
): BidiHTTPRequest {
const request = new BidiHTTPRequest(bidiRequest, frame, redirect);
request.#initialize();
return request;
}
#redirectChain: BidiHTTPRequest[];
#response: BidiHTTPResponse | null = null;
override readonly id: string;
readonly #frame: BidiFrame;
readonly #request: Request;
private constructor(
request: Request,
frame: BidiFrame,
redirect?: BidiHTTPRequest
) {
super();
requests.set(request, this);
this.interception.enabled = request.isBlocked;
this.#request = request;
this.#frame = frame;
this.#redirectChain = redirect ? redirect.#redirectChain : [];
this.id = request.id;
}
override get client(): CDPSession {
return this.#frame.client;
}
#initialize() {
this.#request.on('redirect', request => {
const httpRequest = BidiHTTPRequest.from(request, this.#frame, this);
this.#redirectChain.push(this);
request.once('success', () => {
this.#frame
.page()
.trustedEmitter.emit(PageEvent.RequestFinished, httpRequest);
});
request.once('error', () => {
this.#frame
.page()
.trustedEmitter.emit(PageEvent.RequestFailed, httpRequest);
});
void httpRequest.finalizeInterceptions();
});
this.#request.once('success', data => {
this.#response = BidiHTTPResponse.from(
data,
this,
this.#frame.page().browser().cdpSupported
);
});
this.#request.on('authenticate', this.#handleAuthentication);
this.#frame.page().trustedEmitter.emit(PageEvent.Request, this);
if (this.#hasInternalHeaderOverwrite) {
this.interception.handlers.push(async () => {
await this.continue(
{
headers: this.headers(),
},
0
);
});
}
}
override url(): string {
return this.#request.url;
}
override resourceType(): ResourceType {
if (!this.#frame.page().browser().cdpSupported) {
throw new UnsupportedOperation();
}
return (
this.#request.resourceType || 'other'
).toLowerCase() as ResourceType;
}
override method(): string {
return this.#request.method;
}
override postData(): string | undefined {
if (!this.#frame.page().browser().cdpSupported) {
throw new UnsupportedOperation();
}
return this.#request.postData;
}
override hasPostData(): boolean {
if (!this.#frame.page().browser().cdpSupported) {
throw new UnsupportedOperation();
}
return this.#request.hasPostData;
}
override async fetchPostData(): Promise<string | undefined> {
throw new UnsupportedOperation();
}
get #hasInternalHeaderOverwrite(): boolean {
return Boolean(
Object.keys(this.#extraHTTPHeaders).length ||
Object.keys(this.#userAgentHeaders).length
);
}
get #extraHTTPHeaders(): Record<string, string> {
return this.#frame?.page()._extraHTTPHeaders ?? {};
}
get #userAgentHeaders(): Record<string, string> {
return this.#frame?.page()._userAgentHeaders ?? {};
}
override headers(): Record<string, string> {
const headers: Record<string, string> = {};
for (const header of this.#request.headers) {
headers[header.name.toLowerCase()] = header.value.value;
}
return {
...headers,
...this.#extraHTTPHeaders,
...this.#userAgentHeaders,
};
}
override response(): BidiHTTPResponse | null {
return this.#response;
}
override failure(): {errorText: string} | null {
if (this.#request.error === undefined) {
return null;
}
return {errorText: this.#request.error};
}
override isNavigationRequest(): boolean {
return this.#request.navigation !== undefined;
}
override initiator(): Bidi.Network.Initiator {
return this.#request.initiator;
}
override redirectChain(): BidiHTTPRequest[] {
return this.#redirectChain.slice();
}
override frame(): BidiFrame {
return this.#frame;
}
override async continue(
overrides?: ContinueRequestOverrides,
priority?: number | undefined
): Promise<void> {
return await super.continue(
{
headers: this.#hasInternalHeaderOverwrite ? this.headers() : undefined,
...overrides,
},
priority
);
}
override async _continue(
overrides: ContinueRequestOverrides = {}
): Promise<void> {
const headers: Bidi.Network.Header[] = getBidiHeaders(overrides.headers);
this.interception.handled = true;
return await this.#request
.continueRequest({
url: overrides.url,
method: overrides.method,
body: overrides.postData
? {
type: 'base64',
value: stringToBase64(overrides.postData),
}
: undefined,
headers: headers.length > 0 ? headers : undefined,
})
.catch(error => {
this.interception.handled = false;
return handleError(error);
});
}
override async _abort(): Promise<void> {
this.interception.handled = true;
return await this.#request.failRequest().catch(error => {
this.interception.handled = false;
throw error;
});
}
override async _respond(
response: Partial<ResponseForRequest>,
_priority?: number
): Promise<void> {
this.interception.handled = true;
let parsedBody:
| {
contentLength: number;
base64: string;
}
| undefined;
if (response.body) {
parsedBody = HTTPRequest.getResponse(response.body);
}
const headers: Bidi.Network.Header[] = getBidiHeaders(response.headers);
const hasContentLength = headers.some(header => {
return header.name === 'content-length';
});
if (response.contentType) {
headers.push({
name: 'content-type',
value: {
type: 'string',
value: response.contentType,
},
});
}
if (parsedBody?.contentLength && !hasContentLength) {
headers.push({
name: 'content-length',
value: {
type: 'string',
value: String(parsedBody.contentLength),
},
});
}
const status = response.status || 200;
return await this.#request
.provideResponse({
statusCode: status,
headers: headers.length > 0 ? headers : undefined,
reasonPhrase: STATUS_TEXTS[status],
body: parsedBody?.base64
? {
type: 'base64',
value: parsedBody?.base64,
}
: undefined,
})
.catch(error => {
this.interception.handled = false;
throw error;
});
}
#authenticationHandled = false;
#handleAuthentication = async () => {
if (!this.#frame) {
return;
}
const credentials = this.#frame.page()._credentials;
if (credentials && !this.#authenticationHandled) {
this.#authenticationHandled = true;
void this.#request.continueWithAuth({
action: 'provideCredentials',
credentials: {
type: 'password',
username: credentials.username,
password: credentials.password,
},
});
} else {
void this.#request.continueWithAuth({
action: 'cancel',
});
}
};
timing(): Bidi.Network.FetchTimingInfo {
return this.#request.timing();
}
}
function getBidiHeaders(rawHeaders?: Record<string, unknown>) {
const headers: Bidi.Network.Header[] = [];
for (const [name, value] of Object.entries(rawHeaders ?? [])) {
if (!Object.is(value, undefined)) {
const values = Array.isArray(value) ? value : [value];
for (const value of values) {
headers.push({
name: name.toLowerCase(),
value: {
type: 'string',
value: String(value),
},
});
}
}
}
return headers;
}