"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 __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const apiMiddleware_1 = require("./apiMiddleware");
const const_1 = require("./const");
const lodash_1 = __importDefault(require("lodash"));
const listen_1 = require("./listen");
const github_1 = require("./github");
const misc_1 = require("./misc");
const promises_1 = require("dns/promises");
const net_1 = require("net");
const nat_1 = require("./nat");
const acme_1 = require("./acme");
const selfCheck_1 = require("./selfCheck");
exports.default = {
    get_nat: nat_1.getNatInfo,
    get_public_ips: nat_1.getPublicIps,
    async check_domain({ domain }) {
        (0, misc_1.apiAssertTypes)({ string: { domain } });
        const resolver = new promises_1.Resolver({ timeout: 3_000, tries: 2 });
        const prjInfo = await (0, github_1.getProjectInfo)();
        resolver.setServers(prjInfo.dnsServers);
        const timeout = 7_000; // the external timeout should be larger
        const settled = await Promise.allSettled([
            (0, misc_1.haveTimeout)(timeout, resolver.resolve(domain, 'A')),
            (0, misc_1.haveTimeout)(timeout, resolver.resolve(domain, 'AAAA')),
            (0, misc_1.haveTimeout)(timeout, (0, promises_1.lookup)(domain).then(x => [x.address])),
        ]);
        if (settled[0].status === 'rejected' && settled[0].reason.code === 'ECONNREFUSED')
            return new apiMiddleware_1.ApiError(const_1.HTTP_SERVICE_UNAVAILABLE, "cannot resolve domain");
        // merge all results
        const domainIps = lodash_1.default.uniq((0, misc_1.onlyTruthy)(settled.map(x => x.status === 'fulfilled' && x.value)).flat());
        if (!domainIps.length)
            return new apiMiddleware_1.ApiError(const_1.HTTP_FAILED_DEPENDENCY, "domain not working");
        const publicIps = await (0, nat_1.getPublicIps)(); // do this before stopping the server
        for (const v6 of [false, true]) {
            const domainIpsThisVersion = domainIps.filter(x => (0, net_1.isIPv6)(x) === v6);
            const ipsThisVersion = publicIps.filter(x => (0, net_1.isIPv6)(x) === v6);
            if (domainIpsThisVersion.length && ipsThisVersion.length && !lodash_1.default.intersection(domainIpsThisVersion, ipsThisVersion).length)
                return new apiMiddleware_1.ApiError(const_1.HTTP_PRECONDITION_FAILED, `configure your domain to point to ${ipsThisVersion} (currently on ${domainIpsThisVersion[0]}) – a change can take hours to be effective`);
        }
        return {};
    },
    async map_port({ external, internal }) {
        (0, misc_1.apiAssertTypes)({ number_undefined: { external, internal } });
        const { upnp, externalPort, internalPort } = await (0, nat_1.getNatInfo)();
        if (!upnp)
            return new apiMiddleware_1.ApiError(const_1.HTTP_SERVICE_UNAVAILABLE, "upnp failed");
        if (!internalPort)
            return new apiMiddleware_1.ApiError(const_1.HTTP_FAILED_DEPENDENCY, "no internal port");
        if (externalPort)
            try {
                await (0, nat_1.getUpnpClient)().removeMapping({ public: { host: '', port: externalPort } });
            }
            catch (e) {
                return new apiMiddleware_1.ApiError(const_1.HTTP_SERVER_ERROR, "removeMapping failed: " + String(e));
            }
        if (external)
            await (0, nat_1.getUpnpClient)().createMapping((0, nat_1.upnpMappingParam)(internal || internalPort, external))
                .catch(res => {
                throw new apiMiddleware_1.ApiError(res.errorCode || const_1.HTTP_SERVER_ERROR, res.errorCode === 718 ? "Port not available" : res.errorDescription || res.message || "unknown error");
            });
        nat_1.mappedPort.set(external || 0); // remember only successful HFS mappings
        return {};
    },
    async self_check({ url }) {
        (0, misc_1.apiAssertTypes)({ string_undefined: { url } });
        if (url)
            return await (0, selfCheck_1.selfCheck)(url)
                || new apiMiddleware_1.ApiError(const_1.HTTP_SERVICE_UNAVAILABLE);
        const [publicIps, nat] = await Promise.all([(0, nat_1.getPublicIps)(), (0, nat_1.getNatInfo)()]);
        if (!publicIps.length)
            return new apiMiddleware_1.ApiError(const_1.HTTP_FAILED_DEPENDENCY, 'cannot detect public ip');
        if (!nat.internalPort)
            return new apiMiddleware_1.ApiError(const_1.HTTP_FAILED_DEPENDENCY, 'no internal port');
        const finalPort = nat.externalPort || nat.internalPort;
        const proto = nat.proto || ((0, listen_1.getCertObject)() ? 'https' : 'http');
        const defPort = proto === 'https' ? 443 : 80;
        const results = (0, misc_1.onlyTruthy)(await (0, misc_1.promiseBestEffort)(publicIps.map(ip => (0, selfCheck_1.selfCheck)(`${proto}://${ip}${finalPort === defPort ? '' : ':' + finalPort}`))));
        return results.length ? results : new apiMiddleware_1.ApiError(const_1.HTTP_SERVICE_UNAVAILABLE);
    },
    async make_cert({ domain, email, altNames }) {
        (0, misc_1.apiAssertTypes)({ string: { domain }, string_undefined: { email }, array_undefined: { altNames } });
        if (altNames?.some((name) => typeof name !== 'string'))
            return new apiMiddleware_1.ApiError(const_1.HTTP_BAD_REQUEST, 'bad altNames');
        await (0, acme_1.makeCert)(domain, email, altNames).catch(e => {
            throw new apiMiddleware_1.ApiError(const_1.HTTP_SERVER_ERROR, e.message || String(e));
        });
        return {};
    },
    get_cert() {
        return (0, listen_1.getCertObject)() || { none: true };
    }
};
