"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.parseRawHttpResponse = exports.tryToParseHttpRequest = exports.waitForCompletedResponse = exports.trackResponse = exports.waitForCompletedRequest = exports.buildInitiatedRequest = exports.parseRequestBody = exports.buildBodyReader = exports.isMockttpBody = exports.decodeBodyBuffer = exports.encodeBodyBuffer = exports.isHttp2 = exports.writeHead = exports.shouldKeepAlive = exports.getEffectivePort = exports.getPathFromAbsoluteUrl = exports.getUrlWithoutProtocol = exports.isAbsoluteProtocollessUrl = exports.isRelativeUrl = exports.isAbsoluteUrl = void 0;
const _ = require("lodash");
const tls_1 = require("tls");
const http2 = require("http2");
const stream = require("stream");
const querystring = require("querystring");
const multipart = require("parse-multipart-data");
const now = require("performance-now");
const url = require("url");
const util_1 = require("./util");
const buffer_utils_1 = require("./buffer-utils");
const header_utils_1 = require("./header-utils");
// Is this URL fully qualified?
// Note that this supports only HTTP - no websockets or anything else.
const isAbsoluteUrl = (url) => url.toLowerCase().startsWith('http://') ||
    url.toLowerCase().startsWith('https://');
exports.isAbsoluteUrl = isAbsoluteUrl;
const isRelativeUrl = (url) => url.startsWith('/');
exports.isRelativeUrl = isRelativeUrl;
const isAbsoluteProtocollessUrl = (url) => !(0, exports.isAbsoluteUrl)(url) && !(0, exports.isRelativeUrl)(url);
exports.isAbsoluteProtocollessUrl = isAbsoluteProtocollessUrl;
const getUrlWithoutProtocol = (url) => {
    return url.split('://', 2).slice(-1).join('');
};
exports.getUrlWithoutProtocol = getUrlWithoutProtocol;
const getPathFromAbsoluteUrl = (url) => {
    const pathIndex = (0, util_1.nthIndexOf)(url, '/', 3);
    if (pathIndex !== -1) {
        return url.slice(pathIndex);
    }
    else {
        return '';
    }
};
exports.getPathFromAbsoluteUrl = getPathFromAbsoluteUrl;
const getEffectivePort = (url) => {
    if (url.port) {
        return parseInt(url.port, 10);
    }
    else if (url.protocol === 'https:' || url.protocol === 'wss:') {
        return 443;
    }
    else {
        return 80;
    }
};
exports.getEffectivePort = getEffectivePort;
const shouldKeepAlive = (req) => req.httpVersion !== '1.0' &&
    req.headers['connection'] !== 'close' &&
    req.headers['proxy-connection'] !== 'close';
exports.shouldKeepAlive = shouldKeepAlive;
const writeHead = (response, status, statusMessage, headers) => {
    const flatHeaders = headers === undefined
        ? {}
        : isHttp2(response)
            // Due to a Node.js bug, H2 never expects flat headers
            ? headers
            : !Array.isArray(headers)
                ? (0, header_utils_1.objectHeadersToFlat)(headers)
                // RawHeaders for H1, must be flattened:
                : (0, header_utils_1.flattenPairedRawHeaders)(headers);
    // We aim to always pass flat headers to writeHead instead of calling setHeader because
    // in most cases it's more flexible about supporting raw data, e.g. multiple headers with
    // different casing can't be represented with setHeader at all (the latter overwrites).
    if (statusMessage === undefined) {
        response.writeHead(status, flatHeaders);
    }
    else {
        response.writeHead(status, statusMessage, flatHeaders);
    }
};
exports.writeHead = writeHead;
function isHttp2(message) {
    return ('httpVersion' in message && !!message.httpVersion?.startsWith('2')) || // H2 request
        ('stream' in message && 'createPushResponse' in message); // H2 response
}
exports.isHttp2 = isHttp2;
async function encodeBodyBuffer(buffer, headers) {
    const contentEncoding = headers['content-encoding'];
    // We skip encodeBuffer entirely if possible - this isn't strictly necessary, but it's useful
    // so you can drop the http-encoding package in bundling downstream without issue in cases
    // where you don't actually use any encodings.
    if (!contentEncoding)
        return buffer;
    return await (await Promise.resolve().then(() => require('http-encoding'))).encodeBuffer(buffer, contentEncoding, { level: 1 });
}
exports.encodeBodyBuffer = encodeBodyBuffer;
async function decodeBodyBuffer(buffer, headers) {
    const contentEncoding = headers['content-encoding'];
    // We skip decodeBuffer entirely if possible - this isn't strictly necessary, but it's useful
    // so you can drop the http-encoding package in bundling downstream without issue in cases
    // where you don't actually use any encodings.
    if (!contentEncoding)
        return buffer;
    return await (await Promise.resolve().then(() => require('http-encoding'))).decodeBuffer(buffer, contentEncoding);
}
exports.decodeBodyBuffer = decodeBodyBuffer;
// Parse an in-progress request or response stream, i.e. where the body or possibly even the headers have
// not been fully received/sent yet.
const parseBodyStream = (bodyStream, maxSize, getHeaders) => {
    let bufferPromise = null;
    let completedBuffer = null;
    let body = {
        // Returns a stream for the full body, not the live streaming body.
        // Each call creates a new stream, which starts with the already seen
        // and buffered data, and then continues with the live stream, if active.
        // Listeners to this stream *must* be attached synchronously after this call.
        asStream() {
            // If we've already buffered the whole body, just stream it out:
            if (completedBuffer)
                return (0, buffer_utils_1.bufferToStream)(completedBuffer);
            // Otherwise, we want to start buffering now, and wrap that with
            // a stream that can live-stream the buffered data on demand:
            const buffer = body.asBuffer();
            buffer.catch(() => { }); // Errors will be handled via the stream, so silence unhandled rejections here.
            return (0, buffer_utils_1.bufferThenStream)(buffer, bodyStream);
        },
        asBuffer() {
            if (!bufferPromise) {
                bufferPromise = (0, buffer_utils_1.streamToBuffer)(bodyStream, maxSize);
                bufferPromise
                    .then((buffer) => completedBuffer = buffer)
                    .catch(() => { }); // If we get no body, completedBuffer stays null
            }
            return bufferPromise;
        },
        async asDecodedBuffer() {
            const buffer = await body.asBuffer();
            return decodeBodyBuffer(buffer, getHeaders());
        },
        asText(encoding = 'utf8') {
            return body.asDecodedBuffer().then((b) => b.toString(encoding));
        },
        asJson() {
            return body.asText().then((t) => JSON.parse(t));
        },
        asFormData() {
            return body.asText().then((t) => querystring.parse(t));
        },
    };
    return body;
};
async function runAsyncOrUndefined(func) {
    try {
        return await func();
    }
    catch {
        return undefined;
    }
}
const waitForBody = async (body, headers) => {
    const bufferBody = await body.asBuffer();
    return (0, exports.buildBodyReader)(bufferBody, headers);
};
const isMockttpBody = (body) => {
    return body.hasOwnProperty('getDecodedBuffer');
};
exports.isMockttpBody = isMockttpBody;
const buildBodyReader = (body, headers) => {
    const completedBody = {
        buffer: body,
        async getDecodedBuffer() {
            return runAsyncOrUndefined(async () => (0, buffer_utils_1.asBuffer)(await decodeBodyBuffer(this.buffer, headers)));
        },
        async getText() {
            return runAsyncOrUndefined(async () => (await this.getDecodedBuffer()).toString());
        },
        async getJson() {
            return runAsyncOrUndefined(async () => JSON.parse((await completedBody.getText())));
        },
        async getUrlEncodedFormData() {
            return runAsyncOrUndefined(async () => {
                const contentType = headers["content-type"];
                if (contentType?.includes("multipart/form-data"))
                    return; // Actively ignore multipart data - won't work as expected
                const text = await completedBody.getText();
                return text ? querystring.parse(text) : undefined;
            });
        },
        async getMultipartFormData() {
            return runAsyncOrUndefined(async () => {
                const contentType = headers["content-type"];
                if (!contentType?.includes("multipart/form-data"))
                    return;
                const boundary = contentType.match(/;\s*boundary=(\S+)/);
                // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Type#boundary
                // `boundary` is required for multipart entities.
                if (!boundary)
                    return;
                const multipartBodyBuffer = (0, buffer_utils_1.asBuffer)(await decodeBodyBuffer(this.buffer, headers));
                return multipart.parse(multipartBodyBuffer, boundary[1]);
            });
        },
        async getFormData() {
            return runAsyncOrUndefined(async () => {
                // Return multi-part data if present, or fallback to default URL-encoded
                // parsing for all other cases. Data is returned in the same format regardless.
                const multiPartBody = await completedBody.getMultipartFormData();
                if (multiPartBody) {
                    const formData = {};
                    multiPartBody.forEach((part) => {
                        const name = part.name;
                        if (name === undefined) {
                            // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Disposition#as_a_header_for_a_multipart_body,
                            // The header must include `name` property to identify the field name.
                            // So we ignore parts without a name, treating it as invalid multipart form data.
                        }
                        else {
                            // We do not use `filename` or `type` here, because return value of `getFormData` must be string or string array.
                            const prevValue = formData[name];
                            if (prevValue === undefined) {
                                formData[name] = part.data.toString();
                            }
                            else if (Array.isArray(prevValue)) {
                                prevValue.push(part.data.toString());
                            }
                            else {
                                formData[name] = [prevValue, part.data.toString()];
                            }
                        }
                    });
                    return formData;
                }
                else {
                    return completedBody.getUrlEncodedFormData();
                }
            });
        }
    };
    return completedBody;
};
exports.buildBodyReader = buildBodyReader;
const parseRequestBody = (req, options) => {
    let transformedRequest = req;
    transformedRequest.body = parseBodyStream(req, options.maxSize, () => req.headers);
};
exports.parseRequestBody = parseRequestBody;
/**
 * Build an initiated request: the external representation of a request
 * that's just started.
 */
function buildInitiatedRequest(request) {
    return {
        ..._.pick(request, 'id', 'matchedRuleId', 'protocol', 'httpVersion', 'method', 'url', 'path', 'remoteIpAddress', 'remotePort', 'hostname', 'headers', 'rawHeaders', 'tags'),
        timingEvents: request.timingEvents
    };
}
exports.buildInitiatedRequest = buildInitiatedRequest;
/**
 * Build a completed request: the external representation of a request
 * that's been completely received (but not necessarily replied to).
 */
async function waitForCompletedRequest(request) {
    const body = await waitForBody(request.body, request.headers);
    const requestData = buildInitiatedRequest(request);
    return { ...requestData, body };
}
exports.waitForCompletedRequest = waitForCompletedRequest;
function trackResponse(response, timingEvents, tags, options) {
    let trackedResponse = response;
    trackedResponse.timingEvents = timingEvents;
    trackedResponse.tags = tags;
    // Headers are sent when .writeHead or .write() are first called
    const trackingStream = new stream.PassThrough();
    const originalWriteHeader = trackedResponse.writeHead;
    const originalWrite = trackedResponse.write;
    const originalEnd = trackedResponse.end;
    const originalGetHeaders = trackedResponse.getHeaders;
    let writtenHeaders;
    trackedResponse.getRawHeaders = () => writtenHeaders ?? [];
    trackedResponse.getHeaders = () => (0, header_utils_1.rawHeadersToObject)(trackedResponse.getRawHeaders());
    trackedResponse.writeHead = function (...args) {
        if (!timingEvents.headersSentTimestamp) {
            timingEvents.headersSentTimestamp = now();
        }
        // HTTP/2 responses shouldn't have a status message:
        if (isHttp2(trackedResponse) && typeof args[1] === 'string') {
            args[1] = undefined;
        }
        let headersArg;
        if (args[2]) {
            headersArg = args[2];
        }
        else if (typeof args[1] !== 'string') {
            headersArg = args[1];
        }
        // Two legal formats of header args (flat & object), one unofficial (tuple array)
        if (Array.isArray(headersArg)) {
            if (!Array.isArray(headersArg[0])) {
                // Flat -> Raw tuples
                writtenHeaders = (0, header_utils_1.pairFlatRawHeaders)(headersArg);
            }
            else {
                // Already raw tuples, cheeky
                writtenHeaders = headersArg;
            }
        }
        else {
            // Headers object -> raw tuples
            writtenHeaders = (0, header_utils_1.objectHeadersToRaw)(headersArg ?? {});
        }
        // Headers might also have been set with setHeader before. They'll be combined, with headers
        // here taking precendence. We simulate this by pulling in all values from getHeaders() and
        // remembering any of those that we're not about to override.
        const storedHeaders = originalGetHeaders.apply(this);
        const writtenHeaderKeys = writtenHeaders.map(([key]) => key.toLowerCase());
        const storedHeaderKeys = Object.keys(storedHeaders);
        if (storedHeaderKeys.length) {
            storedHeaderKeys
                .filter((key) => !writtenHeaderKeys.includes(key))
                .reverse() // We're unshifting (these were set first) so we have to reverse to keep order.
                .forEach((key) => {
                const value = storedHeaders[key];
                if (Array.isArray(value)) {
                    value.reverse().forEach(v => writtenHeaders?.unshift([key, v]));
                }
                else if (value !== undefined) {
                    writtenHeaders?.unshift([key, value]);
                }
            });
        }
        return originalWriteHeader.apply(this, args);
    };
    const trackingWrite = function (...args) {
        trackingStream.write.apply(trackingStream, args);
        return originalWrite.apply(this, args);
    };
    trackedResponse.write = trackingWrite;
    trackedResponse.end = function (...args) {
        // We temporarily disable write tracking here, as .end
        // can call this.write, but that write should not be
        // tracked, or we'll get duplicate writes when trackingStream
        // calls it on itself too.
        trackedResponse.write = originalWrite;
        trackingStream.end.apply(trackingStream, args);
        let result = originalEnd.apply(this, args);
        trackedResponse.write = trackingWrite;
        return result;
    };
    trackedResponse.body = parseBodyStream(trackingStream, options.maxSize, () => trackedResponse.getHeaders());
    // Proxy errors (e.g. write-after-end) to the response, so they can be
    // handled elsewhere, rather than killing the process outright.
    trackingStream.on('error', (e) => trackedResponse.emit('error', e));
    return trackedResponse;
}
exports.trackResponse = trackResponse;
/**
 * Build a completed response: the external representation of a response
 * that's been completely written out and sent back to the client.
 */
async function waitForCompletedResponse(response) {
    // Ongoing response has 'getHeaders' - completed has 'headers'.
    if ('headers' in response)
        return response;
    const body = await waitForBody(response.body, response.getHeaders());
    response.timingEvents.responseSentTimestamp = response.timingEvents.responseSentTimestamp || now();
    const completedResponse = _(response).pick([
        'id',
        'statusCode',
        'timingEvents',
        'tags'
    ]).assign({
        statusMessage: '',
        headers: response.getHeaders(),
        rawHeaders: response.getRawHeaders(),
        body: body
    }).valueOf();
    if (!(response instanceof http2.Http2ServerResponse)) {
        // H2 has no status messages, and generates a warning if you look for one
        completedResponse.statusMessage = response.statusMessage;
    }
    return completedResponse;
}
exports.waitForCompletedResponse = waitForCompletedResponse;
// Take raw HTTP request bytes received, have a go at parsing something useful out of them.
// Very lax - this is a method to use when normal parsing has failed, not as standard
function tryToParseHttpRequest(input, socket) {
    const req = {};
    try {
        req.protocol = socket.__lastHopEncrypted ? "https" : "http"; // Wild guess really
        // For TLS sockets, we default the hostname to the name given by SNI. Might be overridden
        // by the URL or Host header later, if available.
        if (socket instanceof tls_1.TLSSocket)
            req.hostname = socket.servername;
        const lines = (0, buffer_utils_1.splitBuffer)(input, '\r\n');
        const requestLine = lines[0].slice(0, lines[0].length).toString('ascii');
        const [method, rawUri, httpProtocol] = requestLine.split(" ");
        if (method)
            req.method = method.slice(0, 15); // With overflows this could be *anything*. Limit it slightly.
        // An empty line delineates the headers from the body
        const emptyLineIndex = _.findIndex(lines, (line) => line.length === 0);
        try {
            const headerLines = lines.slice(1, emptyLineIndex === -1 ? undefined : emptyLineIndex);
            const rawHeaders = headerLines
                .map((line) => (0, buffer_utils_1.splitBuffer)(line, ':', 2))
                .filter((line) => line.length > 1)
                .map((headerParts) => headerParts.map(p => p.toString('utf8').trim()));
            req.rawHeaders = rawHeaders;
            req.headers = (0, header_utils_1.rawHeadersToObject)(rawHeaders);
        }
        catch (e) { }
        try {
            const parsedUrl = url.parse(rawUri);
            req.path = parsedUrl.path ?? undefined;
            const hostHeader = _.find(req.headers, (_value, key) => key.toLowerCase() === 'host');
            if (hostHeader) {
                req.hostname = Array.isArray(hostHeader) ? hostHeader[0] : hostHeader;
            }
            else if (parsedUrl.hostname) {
                req.hostname = parsedUrl.hostname;
            }
            if (rawUri.includes('://') || !req.hostname) {
                // URI is absolute, or we have no way to guess the host at all
                req.url = rawUri;
            }
            else {
                // URI is relative (or invalid) and we have a host: use it
                req.url = `${req.protocol}://${req.hostname}${rawUri.startsWith('/') ? '' : '/' // Add a slash if the URI is garbage
                }${rawUri}`;
            }
        }
        catch (e) { }
        try {
            const httpVersion = httpProtocol.split('/')[1];
            req.httpVersion = httpVersion;
        }
        catch (e) { }
    }
    catch (e) { }
    return req;
}
exports.tryToParseHttpRequest = tryToParseHttpRequest;
// Take raw HTTP response bytes received, parse something useful out of them. This is *not*
// very lax, and will throw errors due to unexpected response data, but it's used when we
// ourselves generate the data (for websocket responses that 'ws' writes directly to the
// socket invisibly). Fortunately all responses are very simple:
function parseRawHttpResponse(input, request) {
    const { id, tags, timingEvents } = request;
    const lines = (0, buffer_utils_1.splitBuffer)(input, '\r\n');
    const responseLine = lines[0].subarray(0, lines[0].length).toString('ascii');
    const [_httpVersion, rawStatusCode, ...restResponseLine] = responseLine.split(" ");
    const statusCode = parseInt(rawStatusCode, 10);
    const statusMessage = restResponseLine.join(' ');
    // An empty line delineates the headers from the body
    const emptyLineIndex = _.findIndex(lines, (line) => line.length === 0);
    const headerLines = lines.slice(1, emptyLineIndex === -1 ? undefined : emptyLineIndex);
    const rawHeaders = headerLines
        .map((line) => (0, buffer_utils_1.splitBuffer)(line, ':', 2))
        .map((headerParts) => headerParts.map(p => p.toString('utf8').trim()));
    const headers = (0, header_utils_1.rawHeadersToObject)(rawHeaders);
    const body = (0, exports.buildBodyReader)(Buffer.from([]), {});
    return {
        id,
        tags,
        timingEvents,
        statusCode,
        statusMessage,
        rawHeaders,
        headers,
        body
    };
}
exports.parseRawHttpResponse = parseRawHttpResponse;
//# sourceMappingURL=request-utils.js.map