"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.logMw = exports.loggers = void 0;
exports.zipLogFile = zipLogFile;
exports.getRotatedFiles = getRotatedFiles;
const events_1 = require("events");
const config_1 = require("./config");
const fs_1 = require("fs");
const util = __importStar(require("util"));
const lodash_1 = __importDefault(require("lodash"));
const util_files_1 = require("./util-files");
const auth_1 = require("./auth");
const misc_1 = require("./misc");
const path_1 = require("path");
const yazl_1 = __importDefault(require("yazl"));
const promises_1 = require("stream/promises");
const promises_2 = require("fs/promises");
const events_2 = __importDefault(require("./events"));
const connections_1 = require("./connections");
const index_1 = require("./index");
const serveGuiFiles_1 = require("./serveGuiFiles");
const fast_glob_1 = __importDefault(require("fast-glob"));
class Logger {
    name;
    stream;
    last;
    path = '';
    constructor(name) {
        this.name = name;
    }
    async setPath(path) {
        this.path = path;
        this.stream?.end();
        this.last = undefined;
        if (!path)
            return this.stream = undefined;
        try {
            const stats = await (0, util_files_1.statWithTimeout)(path);
            this.last = stats.mtime;
        }
        catch {
            if (await (0, util_files_1.ensureParentFolder)(path) === false)
                console.log("Cannot create folder for", path);
        }
        this.reopen();
    }
    reopen() {
        return this.stream = (0, util_files_1.createFileWithPath)(this.path, { flags: 'a' })
            ?.on('error', () => this.stream = undefined);
    }
}
// we'll have names same as config keys. These are used also by the get_log api.
const accessLogger = new Logger(misc_1.CFG.log);
const accessErrorLog = new Logger(misc_1.CFG.error_log);
exports.loggers = [accessLogger, accessErrorLog];
(0, config_1.defineConfig)(accessLogger.name, 'logs/access.log').sub(path => {
    console.debug('Access log file: ' + (path || 'disabled'));
    accessLogger.setPath(path);
});
const errorLogFile = (0, config_1.defineConfig)(accessErrorLog.name, 'logs/access-error.log');
errorLogFile.sub(path => {
    console.debug('Access error log: ' + (path || 'disabled'));
    accessErrorLog.setPath(path);
});
const logRotation = (0, config_1.defineConfig)(misc_1.CFG.log_rotation, 'weekly');
const dontLogNet = (0, config_1.defineConfig)(misc_1.CFG.dont_log_net, '127.0.0.1|::1', v => (0, misc_1.makeNetMatcher)(v));
const logUA = (0, config_1.defineConfig)(misc_1.CFG.log_ua, false);
const logSpam = (0, config_1.defineConfig)(misc_1.CFG.log_spam, false);
const debounce = lodash_1.default.debounce(cb => cb(), 1000); // with this technique, i'll be able to debounce some code respecting the references in its closure
const logMw = async (ctx, next) => {
    const reqStart = new Date(); // request start
    const userAtStart = (0, auth_1.getCurrentUsername)(ctx);
    // do it now so it's available for returning plugins
    ctx.state.completed = Promise.race([(0, events_1.once)(ctx.res, 'finish'), (0, events_1.once)(ctx.res, 'close')]);
    await next();
    if (!ctx.state.dontLog) // with Finder's webdav spam, it's best to not report even in console
        console.debug(ctx.status, ctx.method, ctx.originalUrl, ctx.isAborted() ? '(aborted)' : '');
    if (!logSpam.get()
        && (ctx.querystring.includes('{.exec|') // v2's bug
            // requests generated by security scanners, other bots, and macos' finder
            || ctx.status === misc_1.HTTP_NOT_FOUND && /wlwmanifest.xml$|\.(php)$|cgi|robots.txt$/.test(ctx.path))) {
        events_2.default.emit('spam', ctx);
        return;
    }
    if (ctx.isAborted())
        ctx.logExtra({ aborted: true });
    const conn = (0, connections_1.getConnection)(ctx); // collect reference before close
    // don't await, as we don't want to hold the middlewares chain
    ctx.state.completed.then(() => {
        if (ctx.state.dontLog || ctx.state.considerAsGui && !serveGuiFiles_1.logGui.get())
            return;
        if (dontLogNet.compiled()(ctx.ip))
            return;
        const isError = ctx.status >= 400;
        const logger = isError && accessErrorLog || accessLogger;
        let { stream, last, path } = logger;
        if (!stream)
            return;
        const rotate = logRotation.get()?.[0];
        const reqEnd = logger.last = new Date();
        if (rotate && last) { // rotation enabled and a file exists?
            const passed = Number(reqEnd) - Number(last)
                - 3600_000; // be pessimistic and count a possible DST change
            const t = reqEnd;
            if (rotate === 'm' && (passed >= 31 * misc_1.DAY || t.getMonth() !== last.getMonth())
                || rotate === 'd' && (passed >= misc_1.DAY || t.getDate() !== last.getDate()) // checking passed will solve the case when the day of the month is the same but a month has passed
                || rotate === 'w' && (passed >= 7 * misc_1.DAY || t.getDay() < last.getDay())) {
                stream.end();
                const suffix = '-' + last.getFullYear() + '-' + doubleDigit(last.getMonth() + 1) + '-' + doubleDigit(last.getDate());
                const newPath = (0, misc_1.strinsert)(path, path.length - (0, path_1.extname)(path).length, suffix);
                try { // other logging requests shouldn't happen while we are renaming. Since this is very infrequent we can tolerate solving this by making it sync.
                    (0, fs_1.renameSync)(path, newPath);
                    void zipLogFile(newPath, last).catch(console.error);
                }
                catch (e) { // ok, rename failed, but this doesn't mean we ain't gonna log
                    console.error(e.message || String(e));
                }
                stream = logger.reopen(); // keep variable updated
                if (!stream)
                    return;
            }
        }
        const format = '%s - %s [%s] "%s %s HTTP/%s" %d %s %s\n'; // Apache's Common Log Format
        const a = reqEnd.toString().split(' '); // like nginx, our default log contains the time of log writing
        const date = `${a[2]}/${a[1]}/${a[3]}:${a[4]} ${a[5]?.slice(3)}`;
        const user = (0, auth_1.getCurrentUsername)(ctx) || userAtStart;
        const length = ctx.state.length ?? ctx.length;
        const uri = ctx.originalUrl;
        const duration = (Number(reqEnd) - Number(reqStart)) / 1000;
        ctx.logExtra(ctx.vfsNode && {
            speed: Math.round(length / duration),
            ...ctx.state.includesLastByte && ctx.res.finished && { dl: 1 }
        } || ctx.state.uploadSize !== undefined && {
            ul: ctx.state.uploads,
            size: ctx.state.uploadSize,
            speed: Math.round(ctx.state.uploadSize / duration),
        });
        if (conn?.country)
            ctx.logExtra({ country: conn.country });
        if (logUA.get())
            ctx.logExtra({ ua: ctx.get('user-agent') || undefined });
        const extra = ctx.state.logExtra;
        if (events_2.default.anyListener(logger.name)) // small optimization: this event can happen often, while most times there's no listener, and the parameters object is constructed pointlessly. A benchmark measured it 20% faster (just the line), while maybe it was not necessary.
            events_2.default.emit(logger.name, { ctx, length, user, ts: reqEnd, uri, extra });
        debounce(() => // once in a while we check if the file is still good (not deleted, etc), or we'll reopen it
         (0, util_files_1.statWithTimeout)(logger.path).catch(() => logger.reopen())); // async = smoother but we may lose some entries
        stream.write(util.format(format, ctx.ip, user || '-', date, ctx.method, uri, ctx.req.httpVersion, ctx.status, length?.toString() ?? '-', lodash_1.default.isEmpty(extra) ? '' : JSON.stringify(JSON.stringify(extra))));
    }).catch(e => console.error('log completion:', e.message || String(e)));
};
exports.logMw = logMw;
events_2.default.once('app', () => {
    index_1.app.context.logExtra = function (anything, params) {
        lodash_1.default.merge(this.state, { logExtra: { ...anything, params } }); // params will be considered as parameters of the API
    };
});
async function zipLogFile(path, touch) {
    const zipPath = path + '.zip';
    try {
        const zip = new yazl_1.default.ZipFile();
        const output = (0, fs_1.createWriteStream)(zipPath);
        zip.addFile(path, (0, path_1.basename)(path));
        zip.end();
        await (0, promises_1.pipeline)(zip.outputStream, output);
    }
    catch (e) {
        await (0, promises_2.rm)(zipPath, { force: true }).catch(() => { });
        throw e;
    }
    await (0, promises_2.utimes)(zipPath, touch, touch).catch(console.error);
    await events_2.default.emitAsync('logRotated', { path, zipPath });
    await (0, promises_2.unlink)(path).catch(console.error);
}
function doubleDigit(n) {
    return n > 9 ? n : '0' + n;
}
async function getRotatedFiles() {
    return Object.fromEntries(await Promise.all(exports.loggers.map(async (x) => {
        const mask = (0, misc_1.strinsert)(x.path, x.path.length - (0, path_1.extname)(x.path).length, '-2*'); // including 2, initial digit of the year, will only take rotated files and not "-error"
        const list = await Promise.all(['', '.zip'].map(async (postfix) => // before 3.2 rotated logs were not zipped, and even today there's a very small (negligible) chance that the zipping fails
         (await (0, fast_glob_1.default)(mask + postfix, { stats: true }))
            .map(x => ({ path: x.path, size: x.stats?.size }))));
        return [x.name, list.flat()];
    })));
}
// dump console.error to file
let debugLogFile = (0, fs_1.createWriteStream)('debug.log', { flags: 'a' });
debugLogFile.once('open', () => {
    const was = console.error;
    console.error = function (...args) {
        was.apply(this, args);
        args = args.map(x => typeof x === 'string' ? x : ((0, misc_1.tryJson)(x) ?? String(x)));
        debugLogFile.write((0, misc_1.formatTimestamp)(new Date) + ' - ' + args.join(' ') + '\n');
    };
    // limit log size
    const LIMIT = 1_000_000;
    const { path } = debugLogFile;
    (0, misc_1.repeat)(misc_1.DAY, () => {
        if ((0, fs_1.statSync)(path).size < LIMIT)
            return; // no need
        (0, fs_1.renameSync)(path, 'old-' + path);
        debugLogFile = (0, fs_1.createWriteStream)(path); // new file
    });
}).on('error', () => console.log("Cannot create debug.log"));
