"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.MatrixHostResolver = exports.DefaultCacheForMs = exports.MaxCacheForMs = exports.MinCacheForMs = void 0;
const url_1 = require("url");
const net_1 = require("net");
const dns_1 = require("dns");
const __1 = require("..");
const OneMinute = 1000 * 60;
const OneHour = OneMinute * 60;
exports.MinCacheForMs = OneMinute * 5;
exports.MaxCacheForMs = OneHour * 48;
exports.DefaultCacheForMs = OneHour * 24;
const CacheFailureForMS = exports.MinCacheForMs;
const DefaultMatrixServerPort = 8448;
const MaxPortNumber = 65535;
const WellKnownTimeout = 10000;
const log = new __1.Logger('MatrixHostResolver');
/**
 * Class to lookup the hostname, port and host headers of a given Matrix servername
 * according to the
 * [server discovery section of the spec](https://spec.matrix.org/v1.1/server-server-api/#server-discovery).
 */
class MatrixHostResolver {
    opts;
    fetch;
    dns;
    resultCache = new Map();
    constructor(opts = {}) {
        this.opts = opts;
        // To allow for easier mocking.
        this.fetch = opts.fetch ?? fetch;
        this.dns = opts.dns || dns_1.promises;
    }
    get currentTime() {
        return this.opts.currentTimeMs || Date.now();
    }
    static sortSrvRecords(a, b) {
        // This algorithm is intentionally simple, as we're unlikely
        // to encounter many Matrix servers that actually load balance this way.
        const diffPrio = a.priority - b.priority;
        if (diffPrio != 0) {
            return diffPrio;
        }
        return a.weight - b.weight;
    }
    static determineHostType(serverName) {
        const hostPortPair = /(.+):(\d+)/.exec(serverName);
        let host = serverName;
        let port = undefined;
        if (hostPortPair) {
            port = parseInt(hostPortPair[2]);
            if (host.startsWith('[') && host.endsWith(']')) {
                host = host.slice(1, host.length - 2);
                // IPv6 square bracket notation
                if ((0, net_1.isIP)(host) !== 6) {
                    throw Error('Unknown IPv6 notation');
                }
            }
            else if ((0, net_1.isIP)(serverName) === 6) {
                // Address is IPv6, but it doesn't have a port
                port = undefined;
                host = serverName;
            }
            else {
                host = hostPortPair[1];
            }
        }
        const ipResult = (0, net_1.isIP)(host);
        return {
            type: ipResult === 0 ? "unknown" : ipResult,
            port,
            host,
        };
    }
    async getWellKnown(serverName) {
        const url = `https://${serverName}/.well-known/matrix/server`;
        const controller = new AbortController();
        const timeout = setTimeout(() => controller.abort(), WellKnownTimeout);
        // Will throw on timeout.
        const wellKnown = await this.fetch(url, { signal: controller.signal });
        clearTimeout(timeout);
        if (wellKnown.status !== 200) {
            throw Error('Well known request returned non-200');
        }
        let wellKnownData;
        try {
            wellKnownData = await wellKnown.json();
        }
        catch (ex) {
            throw Error('Invalid datatype for well-known response');
        }
        const mServer = wellKnownData["m.server"];
        if (typeof mServer !== "string") {
            throw Error("Missing 'm.server' in well-known response");
        }
        const [host, portStr] = mServer.split(':');
        const port = portStr ? parseInt(portStr, 10) : DefaultMatrixServerPort;
        if (!host || (port && port < 1 || port > MaxPortNumber)) {
            throw Error("'m.server' was not in the format of <delegated_hostname>[:<delegated_port>]");
        }
        let cacheFor = exports.DefaultCacheForMs;
        const expiresHeader = wellKnown.headers.get('Expires');
        if (expiresHeader) {
            try {
                cacheFor = new Date(expiresHeader).getTime() - this.currentTime;
            }
            catch (ex) {
                log.warn(`Expires header provided by ${url} could not be parsed`, ex);
            }
        }
        // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control
        const cacheControlHeader = wellKnown.headers.get('Cache-Control')?.toLowerCase()
            .split(',')
            .map(s => s.trim()) || [];
        const maxAge = parseInt(cacheControlHeader.find(s => s.startsWith('max-age'))?.substr("max-age=".length) || "NaN", 10);
        if (maxAge) {
            cacheFor = Math.min(Math.max(maxAge * 1000, exports.MinCacheForMs), exports.MaxCacheForMs);
        }
        if (cacheControlHeader?.includes('no-cache') || cacheControlHeader?.includes('no-store')) {
            cacheFor = 0;
        }
        return { cacheFor, mServer };
    }
    /**
     * Resolves a Matrix serverName, fetching any delegated information.
     * This request is NOT cached. For general use, please use `resolveMatrixServer`.
     * @param hostname The Matrix `hostname` to resolve. e.g. `matrix.org`
     * @returns An object describing the delegated details for the host.
     */
    async resolveMatrixServerName(hostname) {
        // https://spec.matrix.org/v1.1/server-server-api/#resolving-server-names
        const { type, host, port } = MatrixHostResolver.determineHostType(hostname);
        // Step 1 - IP literal / Step 2
        if (type !== "unknown" || port) {
            log.debug(`Resolved ${hostname} to be IP literal / non-ip literal with port`);
            return {
                host,
                port: port || DefaultMatrixServerPort,
                // Host header should include the port
                hostname: hostname,
                cacheFor: exports.DefaultCacheForMs,
            };
        }
        // Step 3 - Well-known
        let wellKnownResponse = undefined;
        try {
            wellKnownResponse = await this.getWellKnown(hostname);
            log.debug(`Resolved ${hostname} to be well-known`);
        }
        catch (ex) {
            // Fall through to step 4.
            log.debug(`No well-known found for ${hostname}: ${ex instanceof Error ? ex.message : ex}`);
        }
        if (wellKnownResponse) {
            const { mServer, cacheFor } = wellKnownResponse;
            const wkHost = MatrixHostResolver.determineHostType(mServer);
            // 3.1 / 3.2
            if (type !== "unknown" || wkHost.port) {
                return {
                    host: wkHost.host,
                    port: wkHost.port || DefaultMatrixServerPort,
                    // Host header should include the port
                    hostname: mServer,
                    cacheFor,
                };
            }
            // 3.3
            try {
                const [srvResult] = (await this.dns.resolveSrv(`_matrix-fed._tcp.${hostname}`))
                    .sort(MatrixHostResolver.sortSrvRecords);
                return {
                    host: srvResult.name,
                    port: srvResult.port,
                    hostname: mServer,
                    cacheFor,
                };
            }
            catch (ex) {
                log.debug(`No well-known SRV (_matrix-fed) found for ${hostname}: ${ex instanceof Error ? ex.message : ex}`);
            }
            // 3.4
            try {
                // legacy
                const [srvResult] = (await this.dns.resolveSrv(`_matrix._tcp.${hostname}`))
                    .sort(MatrixHostResolver.sortSrvRecords);
                return {
                    host: srvResult.name,
                    port: srvResult.port,
                    hostname: mServer,
                    cacheFor,
                };
            }
            catch (ex) {
                log.debug(`No well-known SRV (_matrix) found for ${hostname}: ${ex instanceof Error ? ex.message : ex}`);
            }
            // 3.5
            return {
                host: wkHost.host,
                port: wkHost.port || DefaultMatrixServerPort,
                // Host header should include the port
                hostname: mServer,
                cacheFor,
            };
        }
        // Step 4 - SRV
        try {
            const [srvResult] = (await this.dns.resolveSrv(`_matrix-fed._tcp.${hostname}`))
                .sort(MatrixHostResolver.sortSrvRecords);
            return {
                host: srvResult.name,
                port: srvResult.port,
                hostname: hostname,
                cacheFor: exports.DefaultCacheForMs,
            };
        }
        catch (ex) {
            log.debug(`No SRV (_matrix-fed) found for ${hostname}: ${ex instanceof Error ? ex.message : ex}`);
        }
        try {
            // legacy
            const [srvResult] = (await this.dns.resolveSrv(`_matrix._tcp.${hostname}`))
                .sort(MatrixHostResolver.sortSrvRecords);
            return {
                host: srvResult.name,
                port: srvResult.port,
                hostname: hostname,
                cacheFor: exports.DefaultCacheForMs,
            };
        }
        catch (ex) {
            log.debug(`No SRV (_matrix) found for ${hostname}: ${ex instanceof Error ? ex.message : ex}`);
        }
        // Step 5 - Normal resolve
        return {
            host,
            port: port || DefaultMatrixServerPort,
            // Host header should include the port
            hostname: hostname,
            cacheFor: exports.DefaultCacheForMs,
        };
    }
    /**
     * Resolves a Matrix serverName into the baseURL for federated requests, and the
     * `Host` header to use when serving requests.
     *
     * Results are cached by default. Please note that failures are cached, determined by
     * the constant `CacheFailureForMS`.
     * @param hostname The Matrix `hostname` to resolve. e.g. `matrix.org`
     * @param skipCache Should the request be executed regardless of the cached value? Existing cached values will
     *                 be overwritten.
     * @returns The baseurl of the Matrix server (excluding /_matrix/federation suffix), and the hostHeader to be used.
     */
    async resolveMatrixServer(hostname, skipCache = false) {
        const cachedResult = skipCache ? false : this.resultCache.get(hostname);
        if (cachedResult) {
            const cacheAge = this.currentTime - cachedResult.timestamp;
            if ("result" in cachedResult && cacheAge <= cachedResult.result.cacheFor) {
                const result = cachedResult.result;
                log.debug(`Cached result for ${hostname}, returning (alive for ${result.cacheFor - cacheAge}ms)`);
                return {
                    url: new url_1.URL(`https://${result.host}:${result.port}/`),
                    hostHeader: result.hostname,
                };
            }
            else if ("error" in cachedResult && cacheAge <= CacheFailureForMS) {
                log.debug(`Cached error for ${hostname}, throwing (alive for ${CacheFailureForMS - cacheAge}ms)`);
                throw cachedResult.error;
            }
            // Otherwise expired entry.
        }
        try {
            const result = await this.resolveMatrixServerName(hostname);
            if (result.cacheFor) {
                this.resultCache.set(hostname, { result, timestamp: this.currentTime });
            }
            log.debug(`No result cached for ${hostname}, caching result for ${result.cacheFor}ms`);
            return {
                url: new url_1.URL(`https://${result.host}:${result.port}/`),
                hostHeader: result.hostname,
            };
        }
        catch (error) {
            this.resultCache.set(hostname, {
                timestamp: this.currentTime,
                error: error instanceof Error ? error : Error(String(error)),
            });
            log.debug(`No result cached for ${hostname}, caching error for ${CacheFailureForMS}ms`);
            throw error;
        }
    }
}
exports.MatrixHostResolver = MatrixHostResolver;
//# sourceMappingURL=matrix-host-resolver.js.map