"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.NetworkProcessor = void 0;
const protocol_js_1 = require("../../../protocol/protocol.js");
const NetworkUtils_js_1 = require("./NetworkUtils.js");
/** Dispatches Network domain commands. */
class NetworkProcessor {
    #browsingContextStorage;
    #networkStorage;
    constructor(browsingContextStorage, networkStorage) {
        this.#browsingContextStorage = browsingContextStorage;
        this.#networkStorage = networkStorage;
    }
    async addIntercept(params) {
        this.#browsingContextStorage.verifyTopLevelContextsList(params.contexts);
        const urlPatterns = params.urlPatterns ?? [];
        const parsedUrlPatterns = NetworkProcessor.parseUrlPatterns(urlPatterns);
        const intercept = this.#networkStorage.addIntercept({
            urlPatterns: parsedUrlPatterns,
            phases: params.phases,
            contexts: params.contexts,
        });
        await Promise.all(
        // TODO: We should to this for other CDP targets as well
        this.#browsingContextStorage.getTopLevelContexts().map((context) => {
            return context.cdpTarget.toggleFetchIfNeeded();
        }));
        return {
            intercept,
        };
    }
    async continueRequest(params) {
        const { url, method, headers: commandHeaders, body, request: networkId, } = params;
        if (params.url !== undefined) {
            NetworkProcessor.parseUrlString(params.url);
        }
        const request = this.#getBlockedRequestOrFail(networkId, [
            "beforeRequestSent" /* Network.InterceptPhase.BeforeRequestSent */,
        ]);
        const headers = (0, NetworkUtils_js_1.cdpFetchHeadersFromBidiNetworkHeaders)(commandHeaders);
        // TODO: Set / expand.
        // ; Step 9. cookies
        await request.continueRequest({
            url,
            method,
            headers,
            postData: getCdpBodyFromBiDiBytesValue(body),
        });
        return {};
    }
    async continueResponse(params) {
        const { request: networkId, statusCode, reasonPhrase, headers } = params;
        const responseHeaders = (0, NetworkUtils_js_1.cdpFetchHeadersFromBidiNetworkHeaders)(headers);
        const request = this.#getBlockedRequestOrFail(networkId, [
            "authRequired" /* Network.InterceptPhase.AuthRequired */,
            "responseStarted" /* Network.InterceptPhase.ResponseStarted */,
        ]);
        if (request.interceptPhase === "authRequired" /* Network.InterceptPhase.AuthRequired */) {
            if (params.credentials) {
                await Promise.all([
                    request.waitNextPhase,
                    request.continueWithAuth({
                        response: 'ProvideCredentials',
                        username: params.credentials.username,
                        password: params.credentials.password,
                    }),
                ]);
            }
            else {
                // We need to use `ProvideCredentials`
                // As `Default` may cancel the request
                await request.continueWithAuth({
                    response: 'ProvideCredentials',
                });
                return {};
            }
        }
        if (request.interceptPhase === "responseStarted" /* Network.InterceptPhase.ResponseStarted */) {
            // TODO: Set / expand.
            // ; Step 10. cookies
            await request.continueResponse({
                responseCode: statusCode,
                responsePhrase: reasonPhrase,
                responseHeaders,
            });
        }
        return {};
    }
    async continueWithAuth(params) {
        const networkId = params.request;
        const request = this.#getBlockedRequestOrFail(networkId, [
            "authRequired" /* Network.InterceptPhase.AuthRequired */,
        ]);
        let username;
        let password;
        if (params.action === 'provideCredentials') {
            const { credentials } = params;
            username = credentials.username;
            password = credentials.password;
        }
        const response = (0, NetworkUtils_js_1.cdpAuthChallengeResponseFromBidiAuthContinueWithAuthAction)(params.action);
        await request.continueWithAuth({
            response,
            username,
            password,
        });
        return {};
    }
    async failRequest({ request: networkId, }) {
        const request = this.#getRequestOrFail(networkId);
        if (request.interceptPhase === "authRequired" /* Network.InterceptPhase.AuthRequired */) {
            throw new protocol_js_1.InvalidArgumentException(`Request '${networkId}' in 'authRequired' phase cannot be failed`);
        }
        if (!request.interceptPhase) {
            throw new protocol_js_1.NoSuchRequestException(`No blocked request found for network id '${networkId}'`);
        }
        await request.failRequest('Failed');
        return {};
    }
    async provideResponse(params) {
        const { statusCode, reasonPhrase: responsePhrase, headers, body, request: networkId, } = params;
        // TODO: Step 6
        // https://w3c.github.io/webdriver-bidi/#command-network-continueResponse
        const responseHeaders = (0, NetworkUtils_js_1.cdpFetchHeadersFromBidiNetworkHeaders)(headers);
        // TODO: Set / expand.
        // ; Step 10. cookies
        // ; Step 11. credentials
        const request = this.#getBlockedRequestOrFail(networkId, [
            "beforeRequestSent" /* Network.InterceptPhase.BeforeRequestSent */,
            "responseStarted" /* Network.InterceptPhase.ResponseStarted */,
            "authRequired" /* Network.InterceptPhase.AuthRequired */,
        ]);
        // We need to pass through if the request is already in
        // AuthRequired phase
        if (request.interceptPhase === "authRequired" /* Network.InterceptPhase.AuthRequired */) {
            // We need to use `ProvideCredentials`
            // As `Default` may cancel the request
            await request.continueWithAuth({
                response: 'ProvideCredentials',
            });
            return {};
        }
        // If we con't modify the response
        // Just continue the request
        if (!body && !headers) {
            await request.continueRequest();
            return {};
        }
        const responseCode = statusCode ?? request.statusCode ?? 200;
        await request.provideResponse({
            responseCode,
            responsePhrase,
            responseHeaders,
            body: getCdpBodyFromBiDiBytesValue(body),
        });
        return {};
    }
    async removeIntercept(params) {
        this.#networkStorage.removeIntercept(params.intercept);
        await Promise.all(
        // TODO: We should to this for other CDP targets as well
        this.#browsingContextStorage.getTopLevelContexts().map((context) => {
            return context.cdpTarget.toggleFetchIfNeeded();
        }));
        return {};
    }
    #getRequestOrFail(id) {
        const request = this.#networkStorage.getRequestById(id);
        if (!request) {
            throw new protocol_js_1.NoSuchRequestException(`Network request with ID '${id}' doesn't exist`);
        }
        return request;
    }
    #getBlockedRequestOrFail(id, phases) {
        const request = this.#getRequestOrFail(id);
        if (!request.interceptPhase) {
            throw new protocol_js_1.NoSuchRequestException(`No blocked request found for network id '${id}'`);
        }
        if (request.interceptPhase && !phases.includes(request.interceptPhase)) {
            throw new protocol_js_1.InvalidArgumentException(`Blocked request for network id '${id}' is in '${request.interceptPhase}' phase`);
        }
        return request;
    }
    /**
     * Attempts to parse the given url.
     * Throws an InvalidArgumentException if the url is invalid.
     */
    static parseUrlString(url) {
        try {
            return new URL(url);
        }
        catch (error) {
            throw new protocol_js_1.InvalidArgumentException(`Invalid URL '${url}': ${error}`);
        }
    }
    static parseUrlPatterns(urlPatterns) {
        return urlPatterns.map((urlPattern) => {
            switch (urlPattern.type) {
                case 'string': {
                    NetworkProcessor.parseUrlString(urlPattern.pattern);
                    return urlPattern;
                }
                case 'pattern':
                    // No params signifies intercept all
                    if (urlPattern.protocol === undefined &&
                        urlPattern.hostname === undefined &&
                        urlPattern.port === undefined &&
                        urlPattern.pathname === undefined &&
                        urlPattern.search === undefined) {
                        return urlPattern;
                    }
                    if (urlPattern.protocol) {
                        urlPattern.protocol = unescapeURLPattern(urlPattern.protocol);
                        if (!urlPattern.protocol.match(/^[a-zA-Z+-.]+$/)) {
                            throw new protocol_js_1.InvalidArgumentException('Forbidden characters');
                        }
                    }
                    if (urlPattern.hostname) {
                        urlPattern.hostname = unescapeURLPattern(urlPattern.hostname);
                    }
                    if (urlPattern.port) {
                        urlPattern.port = unescapeURLPattern(urlPattern.port);
                    }
                    if (urlPattern.pathname) {
                        urlPattern.pathname = unescapeURLPattern(urlPattern.pathname);
                        if (urlPattern.pathname[0] !== '/') {
                            urlPattern.pathname = `/${urlPattern.pathname}`;
                        }
                        if (urlPattern.pathname.includes('#') ||
                            urlPattern.pathname.includes('?')) {
                            throw new protocol_js_1.InvalidArgumentException('Forbidden characters');
                        }
                    }
                    else if (urlPattern.pathname === '') {
                        urlPattern.pathname = '/';
                    }
                    if (urlPattern.search) {
                        urlPattern.search = unescapeURLPattern(urlPattern.search);
                        if (urlPattern.search[0] !== '?') {
                            urlPattern.search = `?${urlPattern.search}`;
                        }
                        if (urlPattern.search.includes('#')) {
                            throw new protocol_js_1.InvalidArgumentException('Forbidden characters');
                        }
                    }
                    if (urlPattern.protocol === '') {
                        throw new protocol_js_1.InvalidArgumentException(`URL pattern must specify a protocol`);
                    }
                    if (urlPattern.hostname === '') {
                        throw new protocol_js_1.InvalidArgumentException(`URL pattern must specify a hostname`);
                    }
                    if ((urlPattern.hostname?.length ?? 0) > 0) {
                        if (urlPattern.protocol?.match(/^file/i)) {
                            throw new protocol_js_1.InvalidArgumentException(`URL pattern protocol cannot be 'file'`);
                        }
                        if (urlPattern.hostname?.includes(':')) {
                            throw new protocol_js_1.InvalidArgumentException(`URL pattern hostname must not contain a colon`);
                        }
                    }
                    if (urlPattern.port === '') {
                        throw new protocol_js_1.InvalidArgumentException(`URL pattern must specify a port`);
                    }
                    try {
                        new URLPattern(urlPattern);
                    }
                    catch (error) {
                        throw new protocol_js_1.InvalidArgumentException(`${error}`);
                    }
                    return urlPattern;
            }
        });
    }
}
exports.NetworkProcessor = NetworkProcessor;
/**
 * See https://w3c.github.io/webdriver-bidi/#unescape-url-pattern
 */
function unescapeURLPattern(pattern) {
    const forbidden = new Set(['(', ')', '*', '{', '}']);
    let result = '';
    let isEscaped = false;
    for (const c of pattern) {
        if (!isEscaped) {
            if (forbidden.has(c)) {
                throw new protocol_js_1.InvalidArgumentException('Forbidden characters');
            }
            if (c === '\\') {
                isEscaped = true;
                continue;
            }
        }
        result += c;
        isEscaped = false;
    }
    return result;
}
function getCdpBodyFromBiDiBytesValue(body) {
    let parsedBody;
    if (body?.type === 'string') {
        parsedBody = btoa(body.value);
    }
    else if (body?.type === 'base64') {
        parsedBody = body.value;
    }
    return parsedBody;
}
//# sourceMappingURL=NetworkProcessor.js.map