"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.normalizeAddress = void 0;
const fs_1 = __importDefault(require("fs"));
const net_1 = __importDefault(require("net"));
const chalk_1 = __importDefault(require("chalk"));
const helper_1 = __importDefault(require("./helper"));
const config_1 = __importDefault(require("./config"));
const log_1 = __importDefault(require("./log"));
// Node reports an IPv4 peer accepted on a dual-stack (`::`) listener as an
// IPv4-mapped IPv6 address (e.g. `::ffff:127.0.0.1`)
// See "IPv4-Mapped IPv6 Address": https://www.rfc-editor.org/rfc/rfc4291.html#section-2.5.5.2
function normalizeAddress(address) {
    if (address === undefined) {
        return undefined;
    }
    // https://regex101.com/r/QbKRF8/1
    const mapped = /^::ffff:(\d{1,3}(?:\.\d{1,3}){3})$/.exec(address);
    return mapped && net_1.default.isIPv4(mapped[1]) ? mapped[1] : address;
}
exports.normalizeAddress = normalizeAddress;
class Identification {
    connectionId;
    connections;
    oidentdFile;
    constructor(startedCallback) {
        this.connectionId = 0;
        this.connections = new Map();
        if (typeof config_1.default.values.oidentd === "string") {
            this.oidentdFile = helper_1.default.expandHome(config_1.default.values.oidentd);
            log_1.default.info(`Oidentd file: ${chalk_1.default.green(this.oidentdFile)}`);
            this.refresh();
        }
        if (config_1.default.values.identd.enable) {
            if (this.oidentdFile) {
                log_1.default.warn("Using both identd and oidentd at the same time, this is most likely not intended.");
            }
            const server = net_1.default.createServer(this.serverConnection.bind(this));
            server.on("error", (err) => {
                startedCallback(this, err);
            });
            server.listen({
                port: config_1.default.values.identd.port || 113,
                host: config_1.default.values.bind,
            }, () => {
                const address = server.address();
                if (typeof address === "string") {
                    log_1.default.info(`Identd server available on ${chalk_1.default.green(address)}`);
                }
                else if (address?.address) {
                    log_1.default.info(`Identd server available on ${chalk_1.default.green(address.address + ":" + address.port.toString())}`);
                }
                startedCallback(this);
            });
        }
        else {
            startedCallback(this);
        }
    }
    serverConnection(socket) {
        socket.on("error", (err) => log_1.default.error(`Identd socket error: ${err}`));
        socket.setTimeout(5000, () => {
            log_1.default.warn(`identd: no data received, closing connection to ${socket.remoteAddress || "undefined"}`);
            socket.destroy();
        });
        socket.once("data", (data) => {
            this.respondToIdent(socket, data);
            socket.end();
        });
    }
    respondToIdent(socket, buffer) {
        if (!socket.remoteAddress) {
            log_1.default.warn("identd: no remote address");
            return;
        }
        const data = buffer.toString().split(",");
        const lport = parseInt(data[0], 10) || 0;
        const fport = parseInt(data[1], 10) || 0;
        if (lport < 1 || fport < 1 || lport > 65535 || fport > 65535) {
            log_1.default.warn(`identd: bogus request from ${socket.remoteAddress}`);
            return;
        }
        log_1.default.debug(`identd: remote ${socket.remoteAddress} query ${lport}, ${fport}`);
        const queryRemoteAddress = normalizeAddress(socket.remoteAddress);
        const queryLocalAddress = normalizeAddress(socket.localAddress);
        for (const connection of this.connections.values()) {
            // we only want to respond if all the ip,port tuples match, to avoid user enumeration
            if (connection.socket.remotePort === fport &&
                connection.socket.localPort === lport &&
                queryRemoteAddress === normalizeAddress(connection.socket.remoteAddress) &&
                queryLocalAddress === normalizeAddress(connection.socket.localAddress)) {
                const reply = `${lport}, ${fport} : USERID : TheLounge : ${connection.user}\r\n`;
                log_1.default.debug(`identd: reply is ${reply.trimEnd()}`);
                socket.write(reply);
                return;
            }
        }
        const reply = `${lport}, ${fport} : ERROR : NO-USER\r\n`;
        log_1.default.debug(`identd: reply is ${reply.trimEnd()}`);
        socket.write(reply);
    }
    addSocket(socket, user) {
        const id = ++this.connectionId;
        this.connections.set(id, { socket, user });
        if (this.oidentdFile) {
            this.refresh();
        }
        return id;
    }
    removeSocket(id) {
        this.connections.delete(id);
        if (this.oidentdFile) {
            this.refresh();
        }
    }
    refresh() {
        let file = "# Warning: file generated by The Lounge: changes will be overwritten!\n";
        this.connections.forEach((connection, id) => {
            if (!connection.socket.remotePort || !connection.socket.localPort) {
                // Race condition: this can happen when more than one socket gets disconnected at
                // once, since we `refresh()` for each one being added/removed. This results
                // in there possibly being one or more disconnected sockets remaining when we get here.
                log_1.default.warn(`oidentd: socket has no remote or local port (id=${id}). See https://github.com/thelounge/thelounge/pull/4695.`);
                return;
            }
            if (!connection.socket.remoteAddress) {
                log_1.default.warn(`oidentd: socket has no remote address, will not respond to queries`);
                return;
            }
            if (!connection.socket.localAddress) {
                log_1.default.warn(`oidentd: socket has no local address, will not respond to queries`);
                return;
            }
            // we only want to respond if all the ip,port tuples match, to avoid user enumeration
            file +=
                `to ${normalizeAddress(connection.socket.remoteAddress)}` +
                    ` fport ${connection.socket.remotePort}` +
                    ` from ${normalizeAddress(connection.socket.localAddress)}` +
                    ` lport ${connection.socket.localPort}` +
                    ` { reply "${connection.user}" }\n`;
        });
        if (this.oidentdFile) {
            fs_1.default.writeFile(this.oidentdFile, file, { flag: "w+" }, function (err) {
                if (err) {
                    log_1.default.error("Failed to update oidentd file!", err.message);
                }
            });
        }
    }
}
exports.default = Identification;
