"use strict";
/*
 * Copyright 2019 gRPC authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
Object.defineProperty(exports, "__esModule", { value: true });
exports.DEFAULT_PORT = void 0;
exports.setup = setup;
const resolver_1 = require("./resolver");
const dns_1 = require("dns");
const service_config_1 = require("./service-config");
const constants_1 = require("./constants");
const call_interface_1 = require("./call-interface");
const metadata_1 = require("./metadata");
const logging = require("./logging");
const constants_2 = require("./constants");
const uri_parser_1 = require("./uri-parser");
const net_1 = require("net");
const backoff_timeout_1 = require("./backoff-timeout");
const environment_1 = require("./environment");
const TRACER_NAME = 'dns_resolver';
function trace(text) {
    logging.trace(constants_2.LogVerbosity.DEBUG, TRACER_NAME, text);
}
/**
 * The default TCP port to connect to if not explicitly specified in the target.
 */
exports.DEFAULT_PORT = 443;
const DEFAULT_MIN_TIME_BETWEEN_RESOLUTIONS_MS = 30000;
/**
 * Resolver implementation that handles DNS names and IP addresses.
 */
class DnsResolver {
    constructor(target, listener, channelOptions) {
        var _a, _b, _c;
        this.target = target;
        this.listener = listener;
        this.pendingLookupPromise = null;
        this.pendingTxtPromise = null;
        this.latestLookupResult = null;
        this.latestServiceConfigResult = null;
        this.continueResolving = false;
        this.isNextResolutionTimerRunning = false;
        this.isServiceConfigEnabled = true;
        this.returnedIpResult = false;
        this.alternativeResolver = new dns_1.promises.Resolver();
        trace('Resolver constructed for target ' + (0, uri_parser_1.uriToString)(target));
        if (target.authority) {
            this.alternativeResolver.setServers([target.authority]);
        }
        const hostPort = (0, uri_parser_1.splitHostPort)(target.path);
        if (hostPort === null) {
            this.ipResult = null;
            this.dnsHostname = null;
            this.port = null;
        }
        else {
            if ((0, net_1.isIPv4)(hostPort.host) || (0, net_1.isIPv6)(hostPort.host)) {
                this.ipResult = [
                    {
                        addresses: [
                            {
                                host: hostPort.host,
                                port: (_a = hostPort.port) !== null && _a !== void 0 ? _a : exports.DEFAULT_PORT,
                            },
                        ],
                    },
                ];
                this.dnsHostname = null;
                this.port = null;
            }
            else {
                this.ipResult = null;
                this.dnsHostname = hostPort.host;
                this.port = (_b = hostPort.port) !== null && _b !== void 0 ? _b : exports.DEFAULT_PORT;
            }
        }
        this.percentage = Math.random() * 100;
        if (channelOptions['grpc.service_config_disable_resolution'] === 1) {
            this.isServiceConfigEnabled = false;
        }
        this.defaultResolutionError = {
            code: constants_1.Status.UNAVAILABLE,
            details: `Name resolution failed for target ${(0, uri_parser_1.uriToString)(this.target)}`,
            metadata: new metadata_1.Metadata(),
        };
        const backoffOptions = {
            initialDelay: channelOptions['grpc.initial_reconnect_backoff_ms'],
            maxDelay: channelOptions['grpc.max_reconnect_backoff_ms'],
        };
        this.backoff = new backoff_timeout_1.BackoffTimeout(() => {
            if (this.continueResolving) {
                this.startResolutionWithBackoff();
            }
        }, backoffOptions);
        this.backoff.unref();
        this.minTimeBetweenResolutionsMs =
            (_c = channelOptions['grpc.dns_min_time_between_resolutions_ms']) !== null && _c !== void 0 ? _c : DEFAULT_MIN_TIME_BETWEEN_RESOLUTIONS_MS;
        this.nextResolutionTimer = setTimeout(() => { }, 0);
        clearTimeout(this.nextResolutionTimer);
    }
    /**
     * If the target is an IP address, just provide that address as a result.
     * Otherwise, initiate A, AAAA, and TXT lookups
     */
    startResolution() {
        if (this.ipResult !== null) {
            if (!this.returnedIpResult) {
                trace('Returning IP address for target ' + (0, uri_parser_1.uriToString)(this.target));
                setImmediate(() => {
                    this.listener((0, call_interface_1.statusOrFromValue)(this.ipResult), {}, null, '');
                });
                this.returnedIpResult = true;
            }
            this.backoff.stop();
            this.backoff.reset();
            this.stopNextResolutionTimer();
            return;
        }
        if (this.dnsHostname === null) {
            trace('Failed to parse DNS address ' + (0, uri_parser_1.uriToString)(this.target));
            setImmediate(() => {
                this.listener((0, call_interface_1.statusOrFromError)({
                    code: constants_1.Status.UNAVAILABLE,
                    details: `Failed to parse DNS address ${(0, uri_parser_1.uriToString)(this.target)}`
                }), {}, null, '');
            });
            this.stopNextResolutionTimer();
        }
        else {
            if (this.pendingLookupPromise !== null) {
                return;
            }
            trace('Looking up DNS hostname ' + this.dnsHostname);
            /* We clear out latestLookupResult here to ensure that it contains the
             * latest result since the last time we started resolving. That way, the
             * TXT resolution handler can use it, but only if it finishes second. We
             * don't clear out any previous service config results because it's
             * better to use a service config that's slightly out of date than to
             * revert to an effectively blank one. */
            this.latestLookupResult = null;
            const hostname = this.dnsHostname;
            this.pendingLookupPromise = this.lookup(hostname);
            this.pendingLookupPromise.then(addressList => {
                if (this.pendingLookupPromise === null) {
                    return;
                }
                this.pendingLookupPromise = null;
                this.latestLookupResult = (0, call_interface_1.statusOrFromValue)(addressList.map(address => ({
                    addresses: [address],
                })));
                const allAddressesString = '[' +
                    addressList.map(addr => addr.host + ':' + addr.port).join(',') +
                    ']';
                trace('Resolved addresses for target ' +
                    (0, uri_parser_1.uriToString)(this.target) +
                    ': ' +
                    allAddressesString);
                /* If the TXT lookup has not yet finished, both of the last two
                 * arguments will be null, which is the equivalent of getting an
                 * empty TXT response. When the TXT lookup does finish, its handler
                 * can update the service config by using the same address list */
                const healthStatus = this.listener(this.latestLookupResult, {}, this.latestServiceConfigResult, '');
                this.handleHealthStatus(healthStatus);
            }, err => {
                if (this.pendingLookupPromise === null) {
                    return;
                }
                trace('Resolution error for target ' +
                    (0, uri_parser_1.uriToString)(this.target) +
                    ': ' +
                    err.message);
                this.pendingLookupPromise = null;
                this.stopNextResolutionTimer();
                this.listener((0, call_interface_1.statusOrFromError)(this.defaultResolutionError), {}, this.latestServiceConfigResult, '');
            });
            /* If there already is a still-pending TXT resolution, we can just use
             * that result when it comes in */
            if (this.isServiceConfigEnabled && this.pendingTxtPromise === null) {
                /* We handle the TXT query promise differently than the others because
                 * the name resolution attempt as a whole is a success even if the TXT
                 * lookup fails */
                this.pendingTxtPromise = this.resolveTxt(hostname);
                this.pendingTxtPromise.then(txtRecord => {
                    if (this.pendingTxtPromise === null) {
                        return;
                    }
                    this.pendingTxtPromise = null;
                    let serviceConfig;
                    try {
                        serviceConfig = (0, service_config_1.extractAndSelectServiceConfig)(txtRecord, this.percentage);
                        if (serviceConfig) {
                            this.latestServiceConfigResult = (0, call_interface_1.statusOrFromValue)(serviceConfig);
                        }
                        else {
                            this.latestServiceConfigResult = null;
                        }
                    }
                    catch (err) {
                        this.latestServiceConfigResult = (0, call_interface_1.statusOrFromError)({
                            code: constants_1.Status.UNAVAILABLE,
                            details: `Parsing service config failed with error ${err.message}`
                        });
                    }
                    if (this.latestLookupResult !== null) {
                        /* We rely here on the assumption that calling this function with
                         * identical parameters will be essentialy idempotent, and calling
                         * it with the same address list and a different service config
                         * should result in a fast and seamless switchover. */
                        this.listener(this.latestLookupResult, {}, this.latestServiceConfigResult, '');
                    }
                }, err => {
                    /* If TXT lookup fails we should do nothing, which means that we
                     * continue to use the result of the most recent successful lookup,
                     * or the default null config object if there has never been a
                     * successful lookup. We do not set the latestServiceConfigError
                     * here because that is specifically used for response validation
                     * errors. We still need to handle this error so that it does not
                     * bubble up as an unhandled promise rejection. */
                });
            }
        }
    }
    /**
     * The ResolverListener returns a boolean indicating whether the LB policy
     * accepted the resolution result. A false result on an otherwise successful
     * resolution should be treated as a resolution failure.
     * @param healthStatus
     */
    handleHealthStatus(healthStatus) {
        if (healthStatus) {
            this.backoff.stop();
            this.backoff.reset();
        }
        else {
            this.continueResolving = true;
        }
    }
    async lookup(hostname) {
        if (environment_1.GRPC_NODE_USE_ALTERNATIVE_RESOLVER) {
            trace('Using alternative DNS resolver.');
            const records = await Promise.allSettled([
                this.alternativeResolver.resolve4(hostname),
                this.alternativeResolver.resolve6(hostname),
            ]);
            if (records.every(result => result.status === 'rejected')) {
                throw new Error(records[0].reason);
            }
            return records
                .reduce((acc, result) => {
                return result.status === 'fulfilled'
                    ? [...acc, ...result.value]
                    : acc;
            }, [])
                .map(addr => ({
                host: addr,
                port: +this.port,
            }));
        }
        /* We lookup both address families here and then split them up later
         * because when looking up a single family, dns.lookup outputs an error
         * if the name exists but there are no records for that family, and that
         * error is indistinguishable from other kinds of errors */
        const addressList = await dns_1.promises.lookup(hostname, { all: true });
        return addressList.map(addr => ({ host: addr.address, port: +this.port }));
    }
    async resolveTxt(hostname) {
        if (environment_1.GRPC_NODE_USE_ALTERNATIVE_RESOLVER) {
            trace('Using alternative DNS resolver.');
            return this.alternativeResolver.resolveTxt(hostname);
        }
        return dns_1.promises.resolveTxt(hostname);
    }
    startNextResolutionTimer() {
        var _a, _b;
        clearTimeout(this.nextResolutionTimer);
        this.nextResolutionTimer = setTimeout(() => {
            this.stopNextResolutionTimer();
            if (this.continueResolving) {
                this.startResolutionWithBackoff();
            }
        }, this.minTimeBetweenResolutionsMs);
        (_b = (_a = this.nextResolutionTimer).unref) === null || _b === void 0 ? void 0 : _b.call(_a);
        this.isNextResolutionTimerRunning = true;
    }
    stopNextResolutionTimer() {
        clearTimeout(this.nextResolutionTimer);
        this.isNextResolutionTimerRunning = false;
    }
    startResolutionWithBackoff() {
        if (this.pendingLookupPromise === null) {
            this.continueResolving = false;
            this.backoff.runOnce();
            this.startNextResolutionTimer();
            this.startResolution();
        }
    }
    updateResolution() {
        /* If there is a pending lookup, just let it finish. Otherwise, if the
         * nextResolutionTimer or backoff timer is running, set the
         * continueResolving flag to resolve when whichever of those timers
         * fires. Otherwise, start resolving immediately. */
        if (this.pendingLookupPromise === null) {
            if (this.isNextResolutionTimerRunning || this.backoff.isRunning()) {
                if (this.isNextResolutionTimerRunning) {
                    trace('resolution update delayed by "min time between resolutions" rate limit');
                }
                else {
                    trace('resolution update delayed by backoff timer until ' +
                        this.backoff.getEndTime().toISOString());
                }
                this.continueResolving = true;
            }
            else {
                this.startResolutionWithBackoff();
            }
        }
    }
    /**
     * Reset the resolver to the same state it had when it was created. In-flight
     * DNS requests cannot be cancelled, but they are discarded and their results
     * will be ignored.
     */
    destroy() {
        this.continueResolving = false;
        this.backoff.reset();
        this.backoff.stop();
        this.stopNextResolutionTimer();
        this.pendingLookupPromise = null;
        this.pendingTxtPromise = null;
        this.latestLookupResult = null;
        this.latestServiceConfigResult = null;
        this.returnedIpResult = false;
    }
    /**
     * Get the default authority for the given target. For IP targets, that is
     * the IP address. For DNS targets, it is the hostname.
     * @param target
     */
    static getDefaultAuthority(target) {
        return target.path;
    }
}
/**
 * Set up the DNS resolver class by registering it as the handler for the
 * "dns:" prefix and as the default resolver.
 */
function setup() {
    (0, resolver_1.registerResolver)('dns', DnsResolver);
    (0, resolver_1.registerDefaultScheme)('dns');
}
//# sourceMappingURL=resolver-dns.js.map