"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.sessionMiddleware = exports.paramsDecoder = exports.prepareState = exports.someSecurity = exports.cloudflareDetected = exports.headRequests = exports.gzipper = exports.sessionDuration = void 0;
exports.getProxyDetected = getProxyDetected;
exports.failAllowNet = failAllowNet;
const koa_compress_1 = __importDefault(require("koa-compress"));
const const_1 = require("./const");
const misc_1 = require("./misc");
const stream_1 = require("stream");
const block_1 = require("./block");
const perm_1 = require("./perm");
const connections_1 = require("./connections");
const auth_1 = require("./auth");
const zlib_1 = require("zlib");
const listen_1 = require("./listen");
const config_1 = require("./config");
const koa_session_1 = __importDefault(require("koa-session"));
const index_1 = require("./index");
const events_1 = __importDefault(require("./events"));
const forceHttps = (0, config_1.defineConfig)('force_https', true);
(0, config_1.defineConfig)('ignore_proxies', false);
const allowAuthorizationHeader = (0, config_1.defineConfig)('authorization_header', true);
exports.sessionDuration = (0, config_1.defineConfig)('session_duration', Number(process.env.SESSION_DURATION) || misc_1.DAY / 1000, v => v * 1000);
exports.gzipper = (0, koa_compress_1.default)({
    threshold: 2048,
    gzip: { flush: zlib_1.constants.Z_SYNC_FLUSH }, // flush is necessary for SSE, at least in Chrome145
    deflate: { flush: zlib_1.constants.Z_SYNC_FLUSH },
    zstd: { flush: zlib_1.constants.Z_SYNC_FLUSH },
    br: false, // disable brotli
    filter(type) {
        return /text|javascript|style/i.test(type);
    },
});
const headRequests = async (ctx, next) => {
    const head = ctx.method === 'HEAD';
    if (head)
        ctx.method = 'GET'; // let other middlewares work, so we can collect the size at the end
    await next();
    if (!head || ctx.body === undefined)
        return;
    const { length, status } = ctx.response;
    if (ctx.body)
        ctx.body = stream_1.Readable.from(''); // empty the body for this is a HEAD request. Using Readable avoids koa from trying to set length to 0
    ctx.status = status;
    if (length)
        ctx.response.length = length;
};
exports.headRequests = headRequests;
let proxyDetected;
const someSecurity = (ctx, next) => {
    const ss = ctx.session;
    if (ss?.username && !ss?.[misc_1.ALLOW_SESSION_IP_CHANGE])
        if (!ss.ip)
            ss.ip = ctx.ip;
        else if (ss.ip !== ctx.ip) {
            delete ss.username;
            ss.ip = ctx.ip;
        }
    if (!ctx.state.skipFilters && (0, block_1.applyBlock)(ctx.socket, ctx.ip))
        return;
    const decodedPath = (0, misc_1.try_)(() => decodeURI(ctx.path));
    if (!decodedPath || (0, misc_1.hasDirTraversal)(decodedPath))
        return;
    if (ctx.get('X-Forwarded-For')
        // we have some dev-proxies to ignore
        && !(const_1.DEV && [process.env.FRONTEND_PROXY, process.env.ADMIN_PROXY].includes(ctx.get('X-Forwarded-port')))) {
        proxyDetected = ctx;
        ctx.state.whenProxyDetected = new Date();
    }
    if (ctx.get('cf-ray'))
        exports.cloudflareDetected = new Date();
    if (!ctx.secure && forceHttps.get() && (0, listen_1.getHttpsWorkingPort)() && !(0, misc_1.isLocalHost)(ctx)) {
        const { URL } = ctx;
        URL.protocol = 'https';
        URL.port = (0, listen_1.getHttpsWorkingPort)();
        ctx.status = 307; // this ensures the client doesn't switch to a simpler GET request
        return ctx.redirect(URL.href);
    }
    return next();
};
exports.someSecurity = someSecurity;
// limited to http proxies
function getProxyDetected() {
    if (Number(proxyDetected?.state.whenProxyDetected) < Date.now() - misc_1.DAY) // detection is reset after a day
        proxyDetected = undefined;
    return proxyDetected && { from: proxyDetected.socket.remoteAddress, for: proxyDetected.get('X-Forwarded-For') };
}
const prepareState = async (ctx, next) => {
    // normalize once so auth, filters and logging agree on the same client address
    ctx.request.ip = (0, connections_1.normalizeIp)(ctx.ip);
    const s = ctx.session;
    if (s?.username) {
        if (s.ts < auth_1.invalidateSessionBefore.get(s?.username))
            delete s.username;
        s.maxAge = exports.sessionDuration.compiled();
    }
    // calculate these once and for all
    ctx.state.connection = (0, connections_1.socket2connection)(ctx.socket);
    // explicit credentials and existing sessions must take precedence, so a matching IP cannot override a chosen account
    let a = await urlLogin() || await getHttpAccount() || !s?.username && autoLogin();
    const loggedInNotBySession = a;
    ctx.state.account = a ||= (0, perm_1.getAccount)(s?.username, false); // with least precedence, we consider session
    if (a)
        if (!(0, perm_1.accountCanLogin)(a) || failAllowNet(ctx, a)) // enforce allow_net also after login
            await (0, auth_1.setLoggedIn)(ctx, false);
        else if (loggedInNotBySession) {
            if (a.username)
                await (0, auth_1.setLoggedIn)(ctx, a.username);
            ctx.headers['x-username'] = a.username; // give an easier way to determine if the login was successful
        }
    ctx.state.revProxyPath = ctx.get('x-forwarded-prefix');
    (0, connections_1.updateConnectionForCtx)(ctx);
    await next();
    function urlLogin() {
        const { login } = ctx.query;
        if (!login)
            return;
        const [u, p] = (0, misc_1.splitAt)(':', String(login));
        ctx.redirect(ctx.originalUrl.slice(0, -ctx.querystring.length - 1)); // redirect to hide credentials
        return u && (0, auth_1.clearTextLogin)(ctx, u, p, 'url');
    }
    function getHttpAccount() {
        const b64 = allowAuthorizationHeader.get() && ctx.get('authorization')?.split(' ')[1];
        if (!b64)
            return;
        try {
            const [u, p] = atob(b64).split(':');
            if (!u || u === s?.username)
                return; // providing credentials, but not needed
            return (0, auth_1.clearTextLogin)(ctx, u, p || '', 'header');
        }
        catch { }
    }
    function autoLogin() {
        // keep the mask direct so group inheritance cannot make identity depend on account order
        return Object.values(perm_1.accounts.get()).find(a => (0, perm_1.accountCanLogin)(a) && a.auto_login_net && (0, misc_1.netMatches)(ctx.ip, a.auto_login_net, true));
    }
};
exports.prepareState = prepareState;
function failAllowNet(ctx, a) {
    const cached = ctx.session?.allowNet; // won't reflect changes until session is terminated
    const mask = cached ?? (0, perm_1.getFromAccount)(a || '', a => a.allow_net);
    if (!cached && mask && ctx.session?.username)
        ctx.session.allowNet = mask; // must be deleted on logout by setLoggedIn
    const ret = mask && !(0, misc_1.netMatches)(ctx.ip, mask, true);
    if (ret)
        console.debug("Login failed: allow_net");
    return ret;
}
const paramsDecoder = async (ctx, next) => {
    ctx.state.params = ctx.method === 'POST' && ctx.originalUrl.startsWith(const_1.API_URI)
        && ((0, misc_1.tryJson)(await (0, misc_1.stream2string)(ctx.req)) || {});
    await next();
};
exports.paramsDecoder = paramsDecoder;
// once https cookie is created, http cannot do the same. The solution is to use 2 different cookies.
// But koa-session doesn't support 2 cookies, so I made this hacky solution: keep track of the options object, to modify the key at run-time.
let internalSessionMw;
let options;
events_1.default.once('app', () => // wait for app to be defined
 internalSessionMw = (0, koa_session_1.default)(options = { signed: true, renew: true, sameSite: 'lax' }, index_1.app));
const sessionMiddleware = (ctx, next) => {
    options.key = 'hfs_' + ctx.protocol;
    return internalSessionMw(ctx, next);
};
exports.sessionMiddleware = sessionMiddleware;
