"use strict";
// This file is part of HFS - Copyright 2021-2023, Massimo Melina <a@rejetto.com> - License https://www.gnu.org/licenses/gpl-3.0.txt
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    var desc = Object.getOwnPropertyDescriptor(m, k);
    if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
      desc = { enumerable: true, get: function() { return m[k]; } };
    }
    Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
    Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
    o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
    var ownKeys = function(o) {
        ownKeys = Object.getOwnPropertyNames || function (o) {
            var ar = [];
            for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
            return ar;
        };
        return ownKeys(o);
    };
    return function (mod) {
        if (mod && mod.__esModule) return mod;
        var result = {};
        if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
        __setModuleDefault(result, mod);
        return result;
    };
})();
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.stream2string = void 0;
exports.httpString = httpString;
exports.httpWithBody = httpWithBody;
exports.httpStream = httpStream;
exports.parseHttpUrl = parseHttpUrl;
const node_url_1 = require("node:url");
const node_https_1 = __importDefault(require("node:https"));
const node_http_1 = __importDefault(require("node:http"));
const node_stream_1 = require("node:stream");
const lodash_1 = __importDefault(require("lodash"));
const consumers_1 = require("node:stream/consumers");
Object.defineProperty(exports, "stream2string", { enumerable: true, get: function () { return consumers_1.text; } });
const tls = __importStar(require("node:tls"));
const cross_1 = require("./cross");
async function httpString(url, options) {
    return await (0, consumers_1.text)(await httpStream(url, options));
}
async function httpWithBody(url, options) {
    const req = await httpStream(url, options);
    return Object.assign(req, {
        ok: lodash_1.default.inRange(req.statusCode, 200, 300),
        body: req.statusCode ? await (0, consumers_1.buffer)(req) : undefined,
    });
}
function httpStream(url, { body, proxy, jar, noRedirect, httpThrow = true, ...options } = {}, redirected = []) {
    const controller = new AbortController();
    options.signal ??= controller.signal;
    return Object.assign(new Promise(async (resolve, reject) => {
        proxy ??= httpStream.defaultProxy;
        options.headers ??= {};
        if (httpStream.defaultUA && !Object.keys(options.headers).find(k => k.toLowerCase() === 'user-agent'))
            options.headers['user-agent'] = httpStream.defaultUA;
        if (body) {
            options.method ||= 'POST';
            if (lodash_1.default.isPlainObject(body)) {
                options.headers['content-type'] ??= 'application/json';
                body = JSON.stringify(body);
            }
            if (!(body instanceof node_stream_1.Readable))
                options.headers['content-length'] ??= Buffer.byteLength(body);
        }
        const { auth, ...parsed } = parseHttpUrl(url);
        const hostJar = jar && (jar[parsed.hostname || ''] ||= {});
        if (hostJar) {
            options.headers.cookie = lodash_1.default.map(hostJar, (v, k) => `${k}=${v}; `).join('')
                + (options.headers.cookie || ''); // preserve parameter
        }
        const proxyParsed = proxy ? parseHttpUrl(proxy) : null;
        Object.assign(options, lodash_1.default.pick(proxyParsed || parsed, ['hostname', 'port', 'path', 'protocol']));
        if (auth) {
            options.auth = auth;
            if (proxy)
                url = parsed.protocol + '//' + parsed.host + parsed.path; // rewrite without authentication part
        }
        if (proxy) {
            options.path = url; // full url as path
            options.headers.host ??= parsed.host || undefined; // keep original host header
        }
        // this needs the prefix "proxy-"
        const proxyAuth = proxyParsed?.auth ? { 'proxy-authorization': `Basic ${Buffer.from(proxyParsed.auth, 'utf8').toString('base64')}` } : undefined;
        // https through proxy is better with CONNECT
        if (!proxy || parsed.protocol === 'http:' || !await connect())
            Object.assign(options.headers, proxyAuth);
        const proto = options.protocol === 'https:' ? node_https_1.default : node_http_1.default;
        const req = proto.request(options, res => {
            console.debug("HTTP responded", res.statusCode, "to", url);
            if (hostJar)
                for (const entry of res.headers['set-cookie'] || []) {
                    const [, k, v] = /(.+?)=([^;]+)/.exec(entry) || [];
                    if (!k)
                        continue;
                    if (v)
                        hostJar[k] = v;
                    else
                        delete hostJar[k];
                }
            if (!res.statusCode || httpThrow && res.statusCode >= 400)
                return reject(Error(String(res.statusCode), { cause: res }));
            let r = res.headers.location;
            if (r && !noRedirect) {
                const dest = new URL(r, url); // rewrite in case r is just a path, and thus relative to the current url
                r = dest.toString();
                const src = new URL(url);
                const sameOrigin = src.protocol === dest.protocol && src.host === dest.host;
                return redirected.includes(r) ? reject(Error('endless http redirection'))
                    : redirected.length > 20 ? reject(Error('excessive http redirection'))
                        : resolve(httpStream(r, {
                            httpThrow, jar, proxy,
                            ...lodash_1.default.pick(options, ['agent', 'rejectUnauthorized', 'timeout']),
                            // forward some headers and exclude authorization if it's cross-origin
                            headers: options.headers && lodash_1.default.omit(options.headers, ['content-length', 'host', 'connection', 'transfer-encoding', sameOrigin ? '' : 'authorization']),
                        }, [...redirected, r]));
            }
            resolve(res);
        }).on('error', (e) => {
            if (proxy && e?.code === 'ECONNREFUSED')
                console.debug("Cannot connect to proxy ", proxy);
            e.cause ??= req; // enrich the error
            reject(e);
        });
        if (options.timeout) // node only emits the timeout event, so destroy the request to unblock callers waiting for the body
            req.setTimeout(options.timeout, () => req.destroy(Object.assign(Error('timeout'), { code: 'ETIMEDOUT' })));
        if (body && body instanceof node_stream_1.Readable)
            body.pipe(req).on('end', () => req.end());
        else
            req.end(body);
        function connect() {
            return proxyParsed && new Promise(resolve => {
                const path = `${parsed.hostname}:${parsed.port || 443}`;
                (proxyParsed.protocol === 'https:' ? node_https_1.default : node_http_1.default).request({
                    ...proxyParsed,
                    auth: undefined, // void proxyParsed.auth
                    method: 'CONNECT',
                    path,
                    headers: { Host: path, ...proxyAuth }
                }).on('connect', (res, socket) => {
                    if (res.statusCode !== 200)
                        return resolve(false);
                    // we are creating a TLS for every request, very inefficient. Consider optimizing in the future, especially for reading plugins from github, which makes tens of requests.
                    options.createConnection = () => tls.connect({ socket, servername: parsed.hostname || undefined });
                    resolve(true);
                }).on('response', res => {
                    console.debug("Proxy CONNECT response", res.statusCode, res.statusMessage);
                    resolve(false);
                }).on('error', reject)
                    .end();
            });
        }
    }), {
        abort() { controller.abort(); }
    });
}
// works the same way as the now deprecated url.parse()
function parseHttpUrl(url) {
    const parsed = new URL(url);
    const options = (0, node_url_1.urlToHttpOptions)(parsed);
    const withoutHash = url.split('#', 1)[0];
    const authority = /^[a-z][a-z\d+.-]*:\/\/[^/?#]*/i.exec(withoutHash)?.[0];
    const unresolvedPath = !authority ? '/' : (0, cross_1.enforceStarting)('/', withoutHash.slice(authority.length));
    return {
        ...options,
        host: parsed.host,
        path: unresolvedPath.replace(/[\u0000-\u0020\u0100-\u{10FFFF}]/gu, encodeURIComponent), // keep unresolved paths for tests, but escape characters that Node request rejects as unescaped
    };
}
