"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 });
exports.title = exports.favicon = exports.adminNet = exports.localhostAdmin = exports.adminApis = void 0;
exports.ctxAdminAccess = ctxAdminAccess;
exports.anyAccountCanLoginAdmin = anyAccountCanLoginAdmin;
exports.preventAdminAccess = preventAdminAccess;
const apiMiddleware_1 = require("./apiMiddleware");
const config_1 = require("./config");
const listen_1 = require("./listen");
const const_1 = require("./const");
const api_vfs_1 = __importDefault(require("./api.vfs"));
const api_accounts_1 = __importDefault(require("./api.accounts"));
const api_plugins_1 = __importDefault(require("./api.plugins"));
const api_monitor_1 = __importDefault(require("./api.monitor"));
const api_lang_1 = __importDefault(require("./api.lang"));
const api_net_1 = __importDefault(require("./api.net"));
const api_log_1 = __importDefault(require("./api.log"));
const api_cert_1 = __importDefault(require("./api.cert"));
const connections_1 = require("./connections");
const misc_1 = require("./misc");
const perm_1 = require("./perm");
const middlewares_1 = require("./middlewares");
const child_process_1 = require("child_process");
const util_1 = require("util");
const customHtml_1 = require("./customHtml");
const lodash_1 = __importDefault(require("lodash"));
const update_1 = require("./update");
const path_1 = require("path");
const errorPages_1 = require("./errorPages");
const geo_1 = require("./geo");
const roots_1 = require("./roots");
const SendList_1 = require("./SendList");
const ddns_1 = require("./ddns");
const block_1 = require("./block");
const github_1 = require("./github");
const acme_1 = require("./acme");
exports.adminApis = {
    ...api_vfs_1.default,
    ...api_accounts_1.default,
    ...api_plugins_1.default,
    ...api_monitor_1.default,
    ...api_lang_1.default,
    ...api_net_1.default,
    ...api_log_1.default,
    ...api_cert_1.default,
    get_dynamic_dns_error: ddns_1.get_dynamic_dns_error,
    async set_config({ values }) {
        (0, misc_1.apiAssertTypes)({ object: { values } });
        await (0, config_1.setConfig)(values);
        if (values.port === 0 || values.https_port === 0)
            return await (0, misc_1.waitFor)(async () => {
                const st = await (0, listen_1.getServerStatus)();
                // wait for all random ports to be done, so we communicate new numbers
                if ((values.port !== 0 || st.http.listening)
                    && (values.https_port !== 0 || st.https.listening))
                    return st;
            }, { timeout: 1000 })
                ?? new apiMiddleware_1.ApiError(const_1.HTTP_SERVER_ERROR, "something went wrong changing ports");
        return {};
    },
    get_config: config_1.getWholeConfig,
    get_config_text() {
        return {
            path: config_1.configFile.getPath(),
            fullPath: (0, path_1.resolve)(config_1.configFile.getPath()),
            text: config_1.configFile.getText(),
            customHtml: customHtml_1.customHtml.getText(),
        };
    },
    set_config_text: ({ text }) => {
        (0, misc_1.apiAssertTypes)({ string: { text } });
        return config_1.configFile.save(text, { reparse: true });
    },
    update: ({ tag }) => {
        (0, misc_1.apiAssertTypes)({ string_undefined: { tag } });
        return (0, update_1.update)(tag).catch(e => {
            throw e.cause?.statusCode ? new apiMiddleware_1.ApiError(e.cause?.statusCode) : e;
        });
    },
    async check_update() {
        try {
            return { options: await (0, update_1.getUpdates)() };
        }
        catch (e) {
            return new apiMiddleware_1.ApiError(const_1.HTTP_SERVER_ERROR, e?.message);
        }
    },
    async get_other_versions() {
        try {
            return { options: await (0, update_1.getVersions)(r => !r.prerelease, 20) };
        }
        catch (e) {
            return new apiMiddleware_1.ApiError(const_1.HTTP_SERVER_ERROR, e?.message);
        }
    },
    async wait_project_info() {
        await (0, github_1.getProjectInfo)();
        return {};
    },
    async ip_country({ ips }) {
        (0, misc_1.apiAssertTypes)({
            array: { ips },
            string: { ips0: ips[0] }
        });
        const res = await Promise.allSettled(ips.map(geo_1.ip2country));
        return {
            codes: res.map(x => x.status === 'rejected' || x.value === '-' ? '' : x.value)
        };
    },
    is_ip_blocked({ ips }) {
        (0, misc_1.apiAssertTypes)({
            array: { ips },
            string: { ips0: ips[0] }
        });
        return { blocked: ips.map((x) => (0, block_1.isBlocked)(x) ? 1 : 0) };
    },
    get_custom_html() {
        return {
            enabled: !customHtml_1.disableCustomHtml.get(),
            sections: Object.fromEntries([
                ...customHtml_1.customHtmlSections.concat((0, errorPages_1.getErrorSections)()).map(k => [k, '']), // be sure to output all sections
                ...customHtml_1.customHtml.sections // override entries above
            ]),
        };
    },
    async set_custom_html({ sections }) {
        (0, misc_1.apiAssertTypes)({ object: { sections } });
        await (0, customHtml_1.saveCustomHtml)(sections);
        return {};
    },
    quit() {
        setTimeout(() => process.exit());
        return {};
    },
    async get_status() {
        return {
            started: const_1.HFS_STARTED,
            build: const_1.BUILD_TIMESTAMP,
            version: const_1.VERSION,
            apiVersion: const_1.API_VERSION,
            compatibleApiVersion: const_1.COMPATIBLE_API_VERSION,
            ...await (0, listen_1.getServerStatus)(false),
            platform: process.platform,
            cwd: process.cwd(),
            configFile: config_1.configFile.getPath(),
            urls: await (0, listen_1.getUrls)(),
            ips: await (0, listen_1.getIps)(false),
            baseUrl: await (0, listen_1.getBaseUrlOrDefault)(),
            roots: roots_1.roots.get(),
            anyAccountCanLoginAdmin: anyAccountCanLoginAdmin(),
            updatePossible: !await (0, update_1.updateSupported)() ? false : (await (0, update_1.localUpdateAvailable)()) ? 'local' : true,
            previousVersionAvailable: await (0, update_1.previousAvailable)(),
            autoCheckUpdateResult: update_1.autoCheckUpdateResult.get(), // in this form, we get the same type of the serialized json
            alerts: // in this form, we get the same type of the serialized json
            github_1.alerts,
            proxyDetected: (0, middlewares_1.getProxyDetected)(),
            cloudflareDetected: middlewares_1.cloudflareDetected,
            ram: process.memoryUsage.rss(),
            acmeRenewError: acme_1.acmeRenewError,
            blacklistedInstalledPlugins: github_1.blacklistedInstalledPlugins,
            frpDetected: exports.localhostAdmin.get() && !(0, middlewares_1.getProxyDetected)()
                && (0, connections_1.getConnections)().every(misc_1.isLocalHost)
                && await frpDebounced(),
        };
    },
    async add_block({ merge, ip, expire, comment }) {
        (0, misc_1.apiAssertTypes)({
            string: { ip },
            string_undefined: { comment, expire },
            object_undefined: { merge },
        });
        const optionals = lodash_1.default.pickBy({ expire, comment }, v => v !== undefined); // passing undefined-s would override values in merge
        (0, block_1.addBlock)({ ip, ...optionals }, merge);
        return {};
    },
    async geo_ip({ ip }) {
        (0, misc_1.apiAssertTypes)({ string: { ip } });
        return { country: await (0, geo_1.ip2country)(ip) };
    },
    validate_net_mask({ mask }) {
        (0, misc_1.apiAssertTypes)({ string: { mask } });
        return { result: Boolean((0, misc_1.try_)(() => (0, misc_1.makeNetMatcher)(mask))) };
    },
};
for (const [k, was] of (0, misc_1.typedEntries)(exports.adminApis))
    exports.adminApis[k] = ((params, ctx) => {
        if (ctxAdminAccess(ctx))
            return was(params, ctx);
        const props = { possible: anyAccountCanLoginAdmin() };
        return ctx.headers.accept === 'text/event-stream'
            ? new SendList_1.SendListReadable({ doAtStart: x => x.error(const_1.HTTP_UNAUTHORIZED, true, props) })
            : new apiMiddleware_1.ApiError(const_1.HTTP_UNAUTHORIZED, props);
    });
exports.localhostAdmin = (0, config_1.defineConfig)('localhost_admin', true);
exports.adminNet = (0, config_1.defineConfig)('admin_net', '', v => (0, misc_1.makeNetMatcher)(v, true));
exports.favicon = (0, config_1.defineConfig)('favicon', '');
exports.title = (0, config_1.defineConfig)('title', "File server");
function ctxAdminAccess(ctx) {
    if (preventAdminAccess(ctx))
        return false;
    // whenProxyDetected covers both trusted and misconfigured proxies, so localhost_admin never trusts proxied localhost claims
    return !ctx.state.whenProxyDetected && exports.localhostAdmin.get() && (0, misc_1.isLocalHost)(ctx)
        || ctx.state.account && (0, perm_1.accountCanLoginAdmin)(ctx.state.account);
}
const frpDebounced = (0, misc_1.debounceAsync)(async () => {
    if (!const_1.IS_WINDOWS)
        return false;
    try { // guy with win11 reported missing tasklist, so don't take it for granted
        const { stdout } = await (0, util_1.promisify)(child_process_1.execFile)('tasklist', ['/fi', 'imagename eq frpc.exe', '/nh']);
        return stdout.includes('frpc');
    }
    catch {
        return false;
    }
}, { retain: 10_000 });
function anyAccountCanLoginAdmin() {
    return lodash_1.default.some(perm_1.accounts.get(), perm_1.accountCanLoginAdmin);
}
function preventAdminAccess(ctx) {
    return !(0, misc_1.isLocalHost)(ctx) && !exports.adminNet.compiled()(ctx.ip);
}
