Next.js website for Rocky Mountain Vending company featuring: - Product catalog with Stripe integration - Service areas and parts pages - Admin dashboard with Clerk authentication - SEO optimized pages with JSON-LD structured data Co-authored-by: Cursor <cursoragent@cursor.com>
382 lines
9.4 KiB
Text
382 lines
9.4 KiB
Text
/**
|
|
* @license
|
|
* Copyright 2020 Google Inc.
|
|
* SPDX-License-Identifier: Apache-2.0
|
|
*/
|
|
import type {Protocol} from 'devtools-protocol';
|
|
import type * as Bidi from 'webdriver-bidi-protocol';
|
|
|
|
import type {CDPSession} from '../api/CDPSession.js';
|
|
import type {
|
|
ContinueRequestOverrides,
|
|
InterceptResolutionState,
|
|
ResponseForRequest,
|
|
} from '../api/HTTPRequest.js';
|
|
import {
|
|
HTTPRequest,
|
|
STATUS_TEXTS,
|
|
type ResourceType,
|
|
handleError,
|
|
InterceptResolutionAction,
|
|
} 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,
|
|
isNetworkInterceptionEnabled: boolean,
|
|
redirect?: BidiHTTPRequest,
|
|
): BidiHTTPRequest {
|
|
const request = new BidiHTTPRequest(
|
|
bidiRequest,
|
|
frame,
|
|
isNetworkInterceptionEnabled,
|
|
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,
|
|
isNetworkInterceptionEnabled: boolean,
|
|
redirect?: BidiHTTPRequest,
|
|
) {
|
|
super();
|
|
requests.set(request, this);
|
|
|
|
this.interception.enabled = isNetworkInterceptionEnabled;
|
|
|
|
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.interception.enabled,
|
|
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,
|
|
);
|
|
});
|
|
}
|
|
}
|
|
|
|
protected canBeIntercepted(): boolean {
|
|
return this.#request.isBlocked;
|
|
}
|
|
|
|
override interceptResolutionState(): InterceptResolutionState {
|
|
if (!this.#request.isBlocked) {
|
|
return {action: InterceptResolutionAction.Disabled};
|
|
}
|
|
return super.interceptResolutionState();
|
|
}
|
|
|
|
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 {
|
|
return this.#request.hasPostData;
|
|
}
|
|
|
|
override async fetchPostData(): Promise<string | undefined> {
|
|
return await this.#request.fetchPostData();
|
|
}
|
|
|
|
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> {
|
|
// Callers should not be allowed to mutate internal structure.
|
|
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(): Protocol.Network.Initiator | undefined {
|
|
return {
|
|
...this.#request.initiator,
|
|
type: this.#request.initiator?.type ?? 'other',
|
|
};
|
|
}
|
|
|
|
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();
|
|
}
|
|
|
|
getResponseContent(): Promise<Uint8Array> {
|
|
return this.#request.getResponseContent();
|
|
}
|
|
}
|
|
|
|
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;
|
|
}
|