"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.httpsPortCfg = exports.privateKey = exports.cert = exports.portCfg = exports.baseUrl = void 0;
exports.getBaseUrlOrDefault = getBaseUrlOrDefault;
exports.getHttpsWorkingPort = getHttpsWorkingPort;
exports.openAdmin = openAdmin;
exports.getCertObject = getCertObject;
exports.startServer = startServer;
exports.stopServer = stopServer;
exports.getServerStatus = getServerStatus;
exports.getIps = getIps;
exports.getUrls = getUrls;
const http = __importStar(require("http"));
const config_1 = require("./config");
const index_1 = require("./index");
const https = __importStar(require("https"));
const watchLoad_1 = require("./watchLoad");
const os_1 = require("os");
const connections_1 = require("./connections");
const open_1 = __importDefault(require("open"));
const misc_1 = require("./misc");
const const_1 = require("./const");
const find_process_1 = __importDefault(require("find-process"));
const adminApis_1 = require("./adminApis");
const lodash_1 = __importDefault(require("lodash"));
const crypto_1 = require("crypto");
const events_1 = __importDefault(require("./events"));
const net_1 = require("net");
const nat_1 = require("./nat");
const persistence_1 = require("./persistence");
const argv_1 = require("./argv");
const consoleLog_1 = require("./consoleLog");
const first_1 = require("./first");
const fileAttr_1 = require("./fileAttr");
let httpSrv;
let httpsSrv;
// the update relaunch can keep a bridge process alive, so we proactively close listeners here to release ports before the next binary binds; do it before (5) the storage file is closed, because sockets write there
(0, first_1.onProcessExit)(() => Promise.all([stopServer(httpSrv), stopServer(httpsSrv)]), 5);
const openBrowserAtStart = (0, config_1.defineConfig)('open_browser_at_start', true);
exports.baseUrl = (0, config_1.defineConfig)(misc_1.CFG.base_url, '', x => /(?<=\/\/)[^\/]+/.exec(x)?.[0]); // compiled is host only
async function getBaseUrlOrDefault() {
    return exports.baseUrl.get() || await nat_1.defaultBaseUrl.get();
}
function getHttpsWorkingPort() {
    return httpsSrv?.listening && httpsSrv.address()?.port;
}
const commonServerOptions = {
    requestTimeout: 0, // disable timeout on long uploads
    maxHeaderSize: 32768, // allow a larger header for a larger list in ?get=zip
};
// these are properties that can be assigned to the server object
const commonServerAssign = { headersTimeout: 30_000, timeout: misc_1.MINUTE }; // 'headersTimeout' is not recognized by type lib, and 'timeout' is not effective when passed in parameters
const readyToListen = Promise.all([persistence_1.storedMap.isOpening(), fileAttr_1.fileAttrDb.isOpening(), events_1.default.once('app')]);
const considerHttp = (0, misc_1.debounceAsync)(async () => {
    await readyToListen;
    void stopServer(httpSrv);
    httpSrv = Object.assign(http.createServer(commonServerOptions, index_1.app.callback()), { name: 'http' }, commonServerAssign);
    const host = listenInterface.get();
    const port = exports.portCfg.get();
    if (port === const_1.PORT_DISABLED)
        return;
    if (!await startServer(httpSrv, { port, host }))
        if (port !== 80)
            return (0, consoleLog_1.consoleHint)(`try specifying a different port, enter this command: config ${exports.portCfg.key()} 1080`);
        else if (!await startServer(httpSrv, { port: 8080, host }))
            return;
    httpSrv.on('connection', connections_1.newConnection);
    printUrls(httpSrv.name);
    if (openBrowserAtStart.get() && !argv_1.argv.updated && openOnce) {
        openOnce = false;
        openAdmin();
    }
});
let openOnce = true;
exports.portCfg = (0, config_1.defineConfig)('port', 80);
const listenInterface = (0, config_1.defineConfig)('listen_interface', '');
(0, config_1.subMultipleConfigs)(considerHttp, [exports.portCfg, listenInterface]);
function openAdmin() {
    for (const srv of [httpSrv, httpsSrv]) {
        const a = srv?.address();
        if (!a || typeof a === 'string')
            continue;
        const i = listenInterface.get();
        // open() will fail with ::1, don't know why, as my browser correctly opens the resulting url
        const hostname = i === '::1' || i in genericInterfaceNames ? 'localhost' : i;
        const baseUrl = `${srv.name}://${hostname}:${a.port}`;
        (0, open_1.default)(baseUrl + const_1.ADMIN_URI, { wait: true }).catch(async (e) => {
            console.debug(String(e));
            console.warn("Cannot launch browser on this machine >PLEASE< open your browser and reach one of these (you may need a different address)", ...Object.values(await getUrls()).flat().map(x => '\n - ' + x + const_1.ADMIN_URI));
            if (!(0, adminApis_1.anyAccountCanLoginAdmin)())
                (0, consoleLog_1.consoleHint)(`you can enter this command: create-admin YOUR_PASSWORD`);
        });
        return true;
    }
    console.log("OpenAdmin failed");
}
function getCertObject() {
    const c = exports.cert.compiled();
    if (!c)
        return;
    const all = new crypto_1.X509Certificate(c);
    const some = lodash_1.default.pick(all, ['subject', 'issuer', 'validFrom', 'validTo']);
    const ret = lodash_1.default.mapValues(some, v => v?.includes('=') ? Object.fromEntries(v.split('\n').map(x => x.split('='))) : v);
    return Object.assign(ret, { altNames: all.subjectAltName?.replace(/DNS:/g, '').split(/, */) });
}
const considerHttps = (0, misc_1.debounceAsync)(async () => {
    await readyToListen;
    void stopServer(httpsSrv);
    nat_1.defaultBaseUrl.proto = 'http';
    nat_1.defaultBaseUrl.port = getCurrentPort(httpSrv) ?? 0;
    let port = exports.httpsPortCfg.get();
    try {
        const moreOptions = Object.assign({}, ...await events_1.default.emitAsync('httpsServerOptions') || []); // emitAsync returns an array of objects
        httpsSrv = Object.assign(https.createServer(port === const_1.PORT_DISABLED ? {} : {
            ...commonServerOptions,
            key: exports.privateKey.compiled(),
            cert: exports.cert.compiled(),
            ...moreOptions,
        }, index_1.app.callback()), { name: 'https' }, commonServerAssign);
        if (port >= 0) {
            const certObj = getCertObject();
            if (certObj) {
                const cn = certObj.subject?.CN;
                if (cn)
                    console.log("Certificate loaded for", certObj.altNames?.join(' + ') || cn);
                const now = new Date();
                const from = new Date(certObj.validFrom);
                const to = new Date(certObj.validTo);
                updateError(); // error will change at from and to dates of the certificate
                const cancelTo = (0, misc_1.runAt)(to.getTime(), updateError);
                const cancelFrom = (0, misc_1.runAt)(from.getTime(), updateError);
                httpsSrv.on('close', () => {
                    cancelTo();
                    cancelFrom();
                });
                function updateError() {
                    if (!httpsSrv)
                        return;
                    httpsSrv.error = from > now ? "certificate not valid yet" : to < now ? "certificate expired" : undefined;
                }
            }
            const namesForOutput = { cert: 'certificate', private_key: 'private key' };
            for (const x of httpsNeeds)
                if (!x.get())
                    return httpsSrv.error = "missing " + namesForOutput[x.key()];
                else if (!x.compiled())
                    return httpsSrv.error = "cannot read " + namesForOutput[x.key()];
        }
    }
    catch (e) {
        httpsSrv ||= Object.assign(https.createServer({}), { name: 'https' }); // a dummy container, in case creation failed because of certificate errors
        httpsSrv.error = "bad private key or certificate";
        console.error("Failed to create https server: check your private key and certificate", e.message);
        return;
    }
    httpsSrv.on('connection', connections_1.newConnection); // this event is emitted as soon as the tcp layer is connected
    httpsSrv.on('secureConnection', (socket) => {
        for (const c of (0, connections_1.getConnections)()) // TLSSocket shares the same ip:port, so we can find its matching Connection
            if (socket.remoteAddress === c.socket.remoteAddress
                && socket.remotePort === c.socket.remotePort)
                return c.socket.emit('secure', socket); // let know Connection about the secure socket
    });
    port = await startServer(httpsSrv, { port, host: listenInterface.get() });
    if (!port)
        return;
    printUrls(httpsSrv.name);
    events_1.default.emit('httpsReady');
    nat_1.defaultBaseUrl.proto = 'https';
    nat_1.defaultBaseUrl.port = getCurrentPort(httpsSrv) ?? 0;
}, { wait: 200 }); // give time to have key and cert ready
exports.cert = (0, config_1.defineConfig)('cert', '', load);
exports.privateKey = (0, config_1.defineConfig)('private_key', '', load);
const httpsNeeds = [exports.cert, exports.privateKey];
function load(v, { object }) {
    object.watcher?.unwatch();
    if (!v || v.includes('\n'))
        return v;
    // v is a path, we'll watch the file for changes
    object.watcher = (0, watchLoad_1.watchLoad)(v, x => object.setCompiled(x), { immediateFirst: true });
    return '';
}
exports.httpsPortCfg = (0, config_1.defineConfig)('https_port', const_1.PORT_DISABLED);
(0, config_1.subMultipleConfigs)(considerHttps, [exports.httpsPortCfg, listenInterface, ...httpsNeeds]);
const genericInterfaceNames = {
    '0.0.0.0': "any IPv4",
    '::': "any IPv6",
    '': "any network",
};
function renderHost(host) {
    return (0, misc_1.xlate)(host, genericInterfaceNames);
}
function startServer(srv, { port, host }) {
    return new Promise(async (resolve) => {
        if (!srv)
            return resolve(0);
        try {
            if (port === const_1.PORT_DISABLED)
                return resolve(0);
            if (!host && !await testIpV4()) // !host means ipV4+6, and if v4 port alone is busy, we won't be notified of the failure, so we'll first test it on its own
                throw srv.error;
            // from a few tests, this seems enough to support the expect-100 http/1.1 mechanism, at least with curl -T, not used by chrome|firefox anyway
            srv.on('checkContinue', (req, res) => srv.emit('request', req, res));
            port = await listen(host);
            if (port)
                console.log(`Serving ${srv.name} on ${renderHost(host || '')} port ${port}`);
            resolve(port);
        }
        catch (e) {
            srv.error = String(e);
            console.error(srv.name, `Couldn't listen on port ${port}:`, srv.error);
            resolve(0);
        }
    });
    async function testIpV4() {
        const res = await listen('0.0.0.0', true);
        await new Promise(res => srv?.close(res)); // close, if any, and wait
        return res > 0;
    }
    function listen(host, silence = false) {
        return new Promise(async (resolve, reject) => {
            srv?.on('error', onError).listen({ port, host }, () => {
                const ad = srv.address();
                if (!ad)
                    return reject('no address');
                if (typeof ad === 'string') {
                    srv.close();
                    return reject('type of socket not supported');
                }
                srv.removeListener('error', onError); // necessary in case someone calls stop/start many times
                events_1.default.emit('listening', { server: srv, port: ad.port });
                resolve(ad.port);
            });
            async function onError(e) {
                if (!srv)
                    return;
                srv.error = String(e);
                srv.busy = undefined;
                const { code } = e;
                if (code)
                    srv.busy = (0, find_process_1.default)('port', port).then(res => res?.map(x => (0, misc_1.prefix)("Service", x.name === 'svchost.exe' && x.cmd.split(x.name)[1]?.trim()) || x.name).join(' + '), () => '');
                if (code === 'EACCES' && port < 1024 && !srv.busy) // on Windows, when port is used by a service, we get EACCES
                    srv.error = `lacking permission on port ${port}, try with permission (${const_1.IS_WINDOWS ? 'administrator' : 'sudo'}) or port > 1024`;
                if (code === 'EADDRINUSE' || srv.busy)
                    srv.error = `port ${port} busy: ${await srv.busy || "unknown process"}`;
                if (!silence)
                    console.error(srv.name, srv.error);
                resolve(0);
            }
        });
    }
}
function stopServer(srv) {
    return new Promise(resolve => {
        if (!srv?.listening)
            return resolve(null);
        const ad = srv.address();
        if (ad && typeof ad !== 'string')
            console.log("Stopped port", ad.port);
        srv.close(err => {
            if (err && err.code !== 'ERR_SERVER_NOT_RUNNING')
                console.debug("Failed to stop server", String(err));
            resolve(err);
        });
        if (first_1.quitting)
            srv.closeAllConnections();
    });
}
function getCurrentPort(srv) {
    return srv?.address()?.port;
}
async function getServerStatus(includeSrv = true) {
    return {
        http: await serverStatus(httpSrv, exports.portCfg.get()),
        https: await serverStatus(httpsSrv, exports.httpsPortCfg.get()),
    };
    async function serverStatus(srv, configuredPort) {
        const busy = await srv?.busy;
        await (0, misc_1.wait)(0); // simple trick to wait for also .error to be updated. If this trickery becomes necessary elsewhere, then we should make also error a Promise.
        return {
            ...lodash_1.default.pick(srv, ['listening', 'error']),
            busy,
            port: getCurrentPort(srv) || configuredPort,
            configuredPort,
            srv: includeSrv ? srv : undefined,
        };
    }
}
const ignore = /^(lo|.*loopback.*|virtualbox.*|.*\(wsl\).*|llw\d|awdl\d|utun\d|anpi\d)$/i; // avoid giving too much information
// AKA auto-ip https://en.wikipedia.org/wiki/Link-local_address
const isLinkLocal = (0, misc_1.makeNetMatcher)('169.254.0.0/16|FE80::/10');
async function getIps(external = true) {
    const only = { '0.0.0.0': 'IPv4', '::': 'IPv6' }[listenInterface.get()] || '';
    const ips = (0, misc_1.onlyTruthy)(Object.entries((0, os_1.networkInterfaces)()).flatMap(([name, nets]) => nets && !ignore.test(name) && nets.map(net => !net.internal && (!only || only === net.family) && net.address)));
    const e = external && nat_1.defaultBaseUrl.externalIp;
    if (e && !ips.includes(e))
        ips.push(e);
    const noLinkLocal = ips.filter(x => !isLinkLocal(x));
    const ret = lodash_1.default.sortBy(noLinkLocal.length ? noLinkLocal : ips, [
        x => x !== nat_1.defaultBaseUrl.localIp, // use the "nat" info to put best ip first
        net_1.isIPv6 // false=IPV4 comes first
    ]);
    nat_1.defaultBaseUrl.localIp ||= ret[0] || '';
    return ret;
}
async function getUrls() {
    const on = listenInterface.get();
    const ips = on === renderHost(on) ? [on] : await getIps();
    return Object.fromEntries((0, misc_1.onlyTruthy)([httpSrv, httpsSrv].map(srv => {
        if (!srv?.listening)
            return false;
        const port = srv?.address()?.port;
        const appendPort = port === (srv.name === 'https' ? 443 : 80) ? '' : ':' + port;
        const urls = ips.map(ip => `${srv.name}://${(0, misc_1.ipForUrl)(ip)}${appendPort}`);
        return urls.length && [srv.name, urls];
    })));
}
function printUrls(srvName) {
    getUrls().then(urls => lodash_1.default.each(urls[srvName], url => console.log('Serving on', url)));
}
