"use strict";
/*
 * Copyright 2023 Google LLC.
 * Copyright (c) Microsoft Corporation.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 */
Object.defineProperty(exports, "__esModule", { value: true });
exports.NetworkRequest = void 0;
const protocol_js_1 = require("../../../protocol/protocol.js");
const assert_js_1 = require("../../../utils/assert.js");
const Deferred_js_1 = require("../../../utils/Deferred.js");
const NetworkUtils_js_1 = require("./NetworkUtils.js");
/** Abstracts one individual network request. */
class NetworkRequest {
    static #unknown = 'UNKNOWN';
    /**
     * Each network request has an associated request id, which is a string
     * uniquely identifying that request.
     *
     * The identifier for a request resulting from a redirect matches that of the
     * request that initiated it.
     */
    #id;
    #fetchId;
    /**
     * Indicates the network intercept phase, if the request is currently blocked.
     * Undefined necessarily implies that the request is not blocked.
     */
    #interceptPhase;
    #servedFromCache = false;
    #redirectCount;
    #request = {};
    #response = {};
    #beforeRequestSentDeferred = new Deferred_js_1.Deferred();
    #responseStartedDeferred = new Deferred_js_1.Deferred();
    #responseCompletedDeferred = new Deferred_js_1.Deferred();
    #eventManager;
    #networkStorage;
    #cdpTarget;
    constructor(id, eventManager, networkStorage, cdpTarget, redirectCount = 0) {
        this.#id = id;
        this.#eventManager = eventManager;
        this.#networkStorage = networkStorage;
        this.#cdpTarget = cdpTarget;
        this.#redirectCount = redirectCount;
    }
    get id() {
        return this.#id;
    }
    get fetchId() {
        return this.#fetchId;
    }
    get phase() {
        return this.#interceptPhase;
    }
    get url() {
        return (this.#response.info?.url ??
            this.#request.info?.request.url ??
            this.#request.paused?.request.url ??
            this.#response.paused?.request.url);
    }
    get redirectCount() {
        return this.#redirectCount;
    }
    get blocked() {
        return this.#isBlockedInPhase(this.#interceptPhase);
    }
    get cdpClient() {
        return this.#cdpTarget.cdpClient;
    }
    isRedirecting() {
        return Boolean(this.#request.info);
    }
    #isBlockedInPhase(phase) {
        return this.#networkStorage.requestBlockedBy(this, phase).size > 0;
    }
    handleRedirect(event) {
        this.#queueResponseStartedEvent();
        this.#queueResponseCompletedEvent();
        this.#response.hasExtraInfo = event.redirectHasExtraInfo;
        this.#response.info = event.redirectResponse;
        this.#emitEventsIfReady(true);
    }
    #emitEventsIfReady(wasRedirected = false) {
        const requestExtraInfoCompleted = 
        // Flush redirects
        wasRedirected ||
            Boolean(this.#request.extraInfo) ||
            // Requests from cache don't have extra info
            this.#servedFromCache ||
            // Sometimes there is no extra info and the response
            // is the only place we can find out
            Boolean(this.#response.info && !this.#response.hasExtraInfo);
        const requestInterceptionExpected = this.#isBlockedInPhase("beforeRequestSent" /* Network.InterceptPhase.BeforeRequestSent */);
        const requestInterceptionCompleted = (requestInterceptionExpected && Boolean(this.#request.paused)) ||
            !requestInterceptionExpected;
        if (Boolean(this.#request.info) &&
            (requestInterceptionExpected
                ? requestInterceptionCompleted
                : requestExtraInfoCompleted)) {
            this.#beforeRequestSentDeferred.resolve({
                kind: 'success',
                value: undefined,
            });
        }
        const responseExtraInfoCompleted = Boolean(this.#response.extraInfo) ||
            // Response from cache don't have extra info
            this.#servedFromCache ||
            // Don't expect extra info if the flag is false
            Boolean(this.#response.info && !this.#response.hasExtraInfo);
        const responseInterceptionExpected = this.#isBlockedInPhase("responseStarted" /* Network.InterceptPhase.ResponseStarted */);
        const responseInterceptionCompleted = (responseInterceptionExpected && Boolean(this.#response.paused)) ||
            !responseInterceptionExpected;
        if (responseInterceptionExpected && Boolean(this.#response.paused)) {
            this.#responseStartedDeferred.resolve({
                kind: 'success',
                value: undefined,
            });
        }
        else if (this.#response.info) {
            this.#responseStartedDeferred.resolve({
                kind: 'success',
                value: undefined,
            });
        }
        if (Boolean(this.#response.info) &&
            responseExtraInfoCompleted &&
            responseInterceptionCompleted) {
            this.#responseCompletedDeferred.resolve({
                kind: 'success',
                value: undefined,
            });
        }
    }
    onRequestWillBeSentEvent(event) {
        this.#request.info = event;
        this.#queueBeforeRequestSentEvent();
        this.#emitEventsIfReady();
    }
    onRequestWillBeSentExtraInfoEvent(event) {
        this.#request.extraInfo = event;
        this.#emitEventsIfReady();
    }
    onResponseReceivedExtraInfoEvent(event) {
        this.#response.extraInfo = event;
        this.#emitEventsIfReady();
    }
    onResponseReceivedEvent(event) {
        this.#response.hasExtraInfo = event.hasExtraInfo;
        this.#response.info = event.response;
        this.#queueResponseStartedEvent();
        this.#queueResponseCompletedEvent();
        this.#emitEventsIfReady();
    }
    onServedFromCache() {
        this.#servedFromCache = true;
        this.#emitEventsIfReady();
    }
    onLoadingFailedEvent(event) {
        this.#beforeRequestSentDeferred.resolve({
            kind: 'success',
            value: undefined,
        });
        this.#responseStartedDeferred.resolve({
            kind: 'error',
            error: new Error('Network event loading failed'),
        });
        this.#responseCompletedDeferred.resolve({
            kind: 'error',
            error: new Error('Network event loading failed'),
        });
        this.#queueEventPromise(Promise.resolve({ kind: 'success', value: undefined }), () => {
            return {
                params: {
                    ...this.#getBaseEventParams(),
                    errorText: event.errorText,
                },
                method: protocol_js_1.ChromiumBidi.Network.EventNames.FetchError,
                type: 'event',
            };
        }, protocol_js_1.ChromiumBidi.Network.EventNames.FetchError);
    }
    /** @see https://chromedevtools.github.io/devtools-protocol/tot/Fetch/#method-failRequest */
    async failRequest(errorReason) {
        (0, assert_js_1.assert)(this.#fetchId, 'Network Interception not set-up.');
        await this.cdpClient.sendCommand('Fetch.failRequest', {
            requestId: this.#fetchId,
            errorReason,
        });
        this.#interceptPhase = undefined;
    }
    onRequestPaused(event) {
        this.#fetchId = event.requestId;
        // CDP https://chromedevtools.github.io/devtools-protocol/tot/Fetch/#event-requestPaused
        if (event.responseStatusCode || event.responseErrorReason) {
            this.#interceptPhase = "responseStarted" /* Network.InterceptPhase.ResponseStarted */;
            this.#response.paused = event;
            this.#emitEventsIfReady();
            if (!this.blocked) {
                void this.continueResponse();
            }
            else if (!this.#response.info) {
                this.#queueResponseStartedEvent();
                this.#queueResponseCompletedEvent();
            }
        }
        else {
            this.#request.paused = event;
            this.#interceptPhase = "beforeRequestSent" /* Network.InterceptPhase.BeforeRequestSent */;
            this.#emitEventsIfReady();
            if (!this.blocked) {
                void this.continueRequest();
            }
        }
    }
    onAuthRequired(_event) {
        this.#interceptPhase = "authRequired" /* Network.InterceptPhase.AuthRequired */;
        if (!this.blocked) {
            void this.continueWithAuth();
        }
        this.#queueEventPromise(Promise.resolve({ kind: 'success', value: undefined }), () => {
            return {
                params: {
                    ...this.#getBaseEventParams("authRequired" /* Network.InterceptPhase.AuthRequired */),
                    // TODO: Why is this on the Spec
                    // How are we suppose to know the response if we are blocked by Auth
                    response: {},
                },
                method: protocol_js_1.ChromiumBidi.Network.EventNames.AuthRequired,
                type: 'event',
            };
        }, protocol_js_1.ChromiumBidi.Network.EventNames.AuthRequired);
    }
    /** @see https://chromedevtools.github.io/devtools-protocol/tot/Fetch/#method-continueRequest */
    async continueRequest(url, method, headers) {
        (0, assert_js_1.assert)(this.#fetchId, 'Network Interception not set-up.');
        // TODO: Expand.
        await this.cdpClient.sendCommand('Fetch.continueRequest', {
            requestId: this.#fetchId,
            url,
            method,
            headers,
            // TODO: Set?
            // postData:,
            // interceptResponse:,
        });
        this.#interceptPhase = undefined;
    }
    /** @see https://chromedevtools.github.io/devtools-protocol/tot/Fetch/#method-continueResponse */
    async continueResponse({ responseCode, responsePhrase, responseHeaders, } = {}) {
        (0, assert_js_1.assert)(this.#fetchId, 'Network Interception not set-up.');
        await this.cdpClient.sendCommand('Fetch.continueResponse', {
            requestId: this.#fetchId,
            responseCode,
            responsePhrase,
            responseHeaders,
        });
        this.#interceptPhase = undefined;
    }
    /** @see https://chromedevtools.github.io/devtools-protocol/tot/Fetch/#method-continueWithAuth */
    async continueWithAuth(authChallengeResponse = {
        response: 'Default',
    }) {
        (0, assert_js_1.assert)(this.#fetchId, 'Network Interception not set-up.');
        await this.cdpClient.sendCommand('Fetch.continueWithAuth', {
            requestId: this.#fetchId,
            authChallengeResponse,
        });
        this.#interceptPhase = undefined;
    }
    /** @see https://chromedevtools.github.io/devtools-protocol/tot/Fetch/#method-provideResponse */
    async provideResponse({ responseCode, responsePhrase, responseHeaders, body, }) {
        (0, assert_js_1.assert)(this.#fetchId, 'Network Interception not set-up.');
        await this.cdpClient.sendCommand('Fetch.fulfillRequest', {
            requestId: this.#fetchId,
            responseCode,
            responsePhrase,
            responseHeaders,
            ...(body ? { body: btoa(body) } : {}), // TODO: Double-check if btoa usage is correct.
        });
        this.#interceptPhase = undefined;
    }
    dispose() {
        const result = {
            kind: 'error',
            error: new Error('Network processor detached'),
        };
        this.#beforeRequestSentDeferred.resolve(result);
        this.#responseStartedDeferred.resolve(result);
        this.#responseCompletedDeferred.resolve(result);
    }
    get #context() {
        return this.#request.info?.frameId ?? null;
    }
    /** Returns the HTTP status code associated with this request if any. */
    get statusCode() {
        return (this.#response.info?.status ?? this.#response.extraInfo?.statusCode ?? -1 // TODO: Throw an exception or use some other status code?
        );
    }
    #queueEventPromise(deferred, getEventData, eventName) {
        if (this.#isIgnoredEvent()) {
            return;
        }
        this.#eventManager.registerPromiseEvent(deferred.then((result) => {
            if (result.kind === 'success') {
                try {
                    return {
                        kind: 'success',
                        value: getEventData(),
                    };
                }
                catch (error) {
                    return {
                        kind: 'error',
                        error: error instanceof Error ? error : new Error('Unknown'),
                    };
                }
            }
            return result;
        }), this.#context, eventName);
    }
    #getBaseEventParams(phase) {
        const interceptProps = {
            isBlocked: false,
        };
        if (phase && phase === this.#interceptPhase) {
            const blockedBy = this.#networkStorage.requestBlockedBy(this, phase);
            interceptProps.isBlocked = blockedBy.size > 0;
            if (interceptProps.isBlocked) {
                interceptProps.intercepts = [...blockedBy];
            }
        }
        return {
            context: this.#context,
            navigation: this.#getNavigationId(),
            redirectCount: this.#redirectCount,
            request: this.#getRequestData(),
            // Timestamp should be in milliseconds, while CDP provides it in seconds.
            timestamp: Math.round((this.#request.info?.wallTime ?? 0) * 1000),
            // Contains isBlocked and intercepts
            ...interceptProps,
        };
    }
    #getNavigationId() {
        if (!this.#request.info ||
            !this.#request.info.loaderId ||
            // When we navigate all CDP network events have `loaderId`
            // CDP's `loaderId` and `requestId` match when
            // that request triggered the loading
            this.#request.info.loaderId !== this.#request.info.requestId) {
            return null;
        }
        return this.#request.info.loaderId;
    }
    #getRequestData() {
        const cookies = this.#request.extraInfo
            ? NetworkRequest.#getCookies(this.#request.extraInfo.associatedCookies)
            : [];
        const headers = (0, NetworkUtils_js_1.bidiNetworkHeadersFromCdpNetworkHeaders)(this.#request.info?.request.headers);
        return {
            request: this.#request.info?.requestId ?? NetworkRequest.#unknown,
            url: this.#request.info?.request.url ?? NetworkRequest.#unknown,
            method: this.#request.info?.request.method ?? NetworkRequest.#unknown,
            headers,
            cookies,
            headersSize: (0, NetworkUtils_js_1.computeHeadersSize)(headers),
            // TODO: implement.
            bodySize: 0,
            timings: this.#getTimings(),
        };
    }
    // TODO: implement.
    #getTimings() {
        return {
            timeOrigin: 0,
            requestTime: 0,
            redirectStart: 0,
            redirectEnd: 0,
            fetchStart: 0,
            dnsStart: 0,
            dnsEnd: 0,
            connectStart: 0,
            connectEnd: 0,
            tlsStart: 0,
            requestStart: 0,
            responseStart: 0,
            responseEnd: 0,
        };
    }
    #queueBeforeRequestSentEvent() {
        this.#queueEventPromise(this.#beforeRequestSentDeferred, () => {
            return Object.assign(this.#getBeforeRequestEvent(), {
                type: 'event',
            });
        }, protocol_js_1.ChromiumBidi.Network.EventNames.BeforeRequestSent);
    }
    #getBeforeRequestEvent() {
        (0, assert_js_1.assert)(this.#request.info, 'RequestWillBeSentEvent is not set');
        return {
            method: protocol_js_1.ChromiumBidi.Network.EventNames.BeforeRequestSent,
            params: {
                ...this.#getBaseEventParams("beforeRequestSent" /* Network.InterceptPhase.BeforeRequestSent */),
                initiator: {
                    type: NetworkRequest.#getInitiatorType(this.#request.info.initiator.type),
                },
            },
        };
    }
    #queueResponseStartedEvent() {
        this.#queueEventPromise(this.#responseStartedDeferred, () => {
            return Object.assign(this.#getResponseStartedEvent(), {
                type: 'event',
            });
        }, protocol_js_1.ChromiumBidi.Network.EventNames.ResponseStarted);
    }
    #getResponseStartedEvent() {
        (0, assert_js_1.assert)(this.#request.info, 'RequestWillBeSentEvent is not set');
        (0, assert_js_1.assert)(
        // The response paused comes before any data for the response
        this.#response.paused || this.#response.info, 'ResponseReceivedEvent is not set');
        // Chromium sends wrong extraInfo events for responses served from cache.
        // See https://github.com/puppeteer/puppeteer/issues/9965 and
        // https://crbug.com/1340398.
        if (this.#response.info?.fromDiskCache) {
            this.#response.extraInfo = undefined;
        }
        // TODO: get headers from Fetch.requestPaused
        const headers = (0, NetworkUtils_js_1.bidiNetworkHeadersFromCdpNetworkHeaders)(this.#response.info?.headers);
        return {
            method: protocol_js_1.ChromiumBidi.Network.EventNames.ResponseStarted,
            params: {
                ...this.#getBaseEventParams("responseStarted" /* Network.InterceptPhase.ResponseStarted */),
                response: {
                    url: this.#response.info?.url ??
                        this.#response.paused?.request.url ??
                        NetworkRequest.#unknown,
                    protocol: this.#response.info?.protocol ?? '',
                    status: this.statusCode || this.#response.paused?.responseStatusCode || 0,
                    statusText: this.#response.info?.statusText ||
                        this.#response.paused?.responseStatusText ||
                        '',
                    fromCache: this.#response.info?.fromDiskCache ||
                        this.#response.info?.fromPrefetchCache ||
                        this.#servedFromCache,
                    headers,
                    mimeType: this.#response.info?.mimeType || '',
                    bytesReceived: this.#response.info?.encodedDataLength || 0,
                    headersSize: (0, NetworkUtils_js_1.computeHeadersSize)(headers),
                    // TODO: consider removing from spec.
                    bodySize: 0,
                    content: {
                        // TODO: consider removing from spec.
                        size: 0,
                    },
                },
            },
        };
    }
    #queueResponseCompletedEvent() {
        this.#queueEventPromise(this.#responseCompletedDeferred, () => {
            return Object.assign(this.#getResponseReceivedEvent(), {
                type: 'event',
            });
        }, protocol_js_1.ChromiumBidi.Network.EventNames.ResponseCompleted);
    }
    #getResponseReceivedEvent() {
        (0, assert_js_1.assert)(this.#request.info, 'RequestWillBeSentEvent is not set');
        (0, assert_js_1.assert)(this.#response.info, 'ResponseReceivedEvent is not set');
        // Chromium sends wrong extraInfo events for responses served from cache.
        // See https://github.com/puppeteer/puppeteer/issues/9965 and
        // https://crbug.com/1340398.
        if (this.#response.info.fromDiskCache) {
            this.#response.extraInfo = undefined;
        }
        const headers = (0, NetworkUtils_js_1.bidiNetworkHeadersFromCdpNetworkHeaders)(this.#response.info.headers);
        return {
            method: protocol_js_1.ChromiumBidi.Network.EventNames.ResponseCompleted,
            params: {
                ...this.#getBaseEventParams(),
                response: {
                    url: this.#response.info.url ?? NetworkRequest.#unknown,
                    protocol: this.#response.info.protocol ?? '',
                    status: this.statusCode,
                    statusText: this.#response.info.statusText,
                    fromCache: this.#response.info.fromDiskCache ||
                        this.#response.info.fromPrefetchCache ||
                        this.#servedFromCache,
                    headers,
                    mimeType: this.#response.info.mimeType,
                    bytesReceived: this.#response.info.encodedDataLength,
                    headersSize: (0, NetworkUtils_js_1.computeHeadersSize)(headers),
                    // TODO: consider removing from spec.
                    bodySize: 0,
                    content: {
                        // TODO: consider removing from spec.
                        size: 0,
                    },
                },
            },
        };
    }
    #isIgnoredEvent() {
        return this.#request.info?.request.url.endsWith('/favicon.ico') ?? false;
    }
    static #getInitiatorType(initiatorType) {
        switch (initiatorType) {
            case 'parser':
            case 'script':
            case 'preflight':
                return initiatorType;
            default:
                return 'other';
        }
    }
    static #getCookies(associatedCookies) {
        return associatedCookies
            .filter(({ blockedReasons }) => {
            return !Array.isArray(blockedReasons) || blockedReasons.length === 0;
        })
            .map(({ cookie }) => (0, NetworkUtils_js_1.cdpToBiDiCookie)(cookie));
    }
}
exports.NetworkRequest = NetworkRequest;
//# sourceMappingURL=NetworkRequest.js.map