"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.Prober = void 0;
const tslib_1 = require("tslib");
const assert_1 = tslib_1.__importDefault(require("assert"));
const debug_1 = tslib_1.__importDefault(require("debug"));
const Question_1 = require("../coder/Question");
const MDNSServer_1 = require("../MDNSServer");
const tiebreaking = tslib_1.__importStar(require("../util/tiebreaking"));
const tiebreaking_1 = require("../util/tiebreaking");
const PROBE_INTERVAL = 250; // 250ms as defined in RFC 6762 8.1.
const LIMITED_PROBE_INTERVAL = 1000;
const debug = (0, debug_1.default)("ciao:Prober");
/**
 * This class is used to execute the probing process for a given service as defined
 * in RFC 6762 8.1.
 * This ensures that we advertise the service under a unique name.
 * It also provides a conflict resolution algorithm if multiple clients probing
 * for the same name are detected.
 */
class Prober {
    constructor(responder, server, service) {
        this.records = [];
        this.currentInterval = PROBE_INTERVAL;
        this.serviceEncounteredNameChange = false;
        this.sentFirstProbeQuery = false; // we MUST ignore responses received BEFORE the first probe is sent
        this.sentQueriesForCurrentTry = 0;
        this.sentQueries = 0;
        (0, assert_1.default)(responder, "responder must be defined");
        (0, assert_1.default)(server, "server must be defined");
        (0, assert_1.default)(service, "service must be defined");
        this.responder = responder;
        this.server = server;
        this.service = service;
    }
    getService() {
        return this.service;
    }
    /**
     * This method is called to start the actual probing process.
     * Once the service is considered unique on the network and can be announced the promise returns.
     * While probing multiple name changes can happen
     *
     * @returns a promise which returns when the service is considered unique on the network
     */
    probe() {
        /*
         * Probing is basically the following process: We send three "probe" queries to check
         * if the desired service name is already on the network.
         * The request are sent with a delay of 250ms between them and the first
         * request starting with a random delay.
         * If we don't receive any response to our requests we consider the probing to be successful
         * and continue with announcing our service.
         */
        debug("Starting to probe for '%s'...", this.service.getFQDN());
        return new Promise((resolve, reject) => {
            this.promiseResolve = resolve;
            this.promiseReject = reject;
            this.timer = setTimeout(this.sendProbeRequest.bind(this), Math.random() * PROBE_INTERVAL);
            this.timer.unref();
        });
    }
    cancel() {
        this.clear();
        this.promiseReject(Prober.CANCEL_REASON);
    }
    clear() {
        if (this.timer) {
            clearTimeout(this.timer);
            this.timer = undefined;
        }
        // reset all values to default (so the Prober can be reused if it wasn't successful)
        this.sentFirstProbeQuery = false;
        this.sentQueriesForCurrentTry = 0;
    }
    /**
     * End the current ongoing probing requests. If
     * @param success
     */
    endProbing(success) {
        // reset all values to default (so the Prober can be reused if it wasn't successful)
        this.clear();
        if (success) {
            debug("Probing for '%s' finished successfully", this.service.getFQDN());
            this.promiseResolve();
            if (this.serviceEncounteredNameChange) {
                this.service.informAboutNameUpdates();
            }
        }
    }
    sendProbeRequest() {
        if (this.sentQueriesForCurrentTry === 0) { // this is the first query sent, init some stuff
            // RFC 6762 8.2. When a host is probing for a group of related records with the same
            //    name (e.g., the SRV and TXT record describing a DNS-SD service), only
            //    a single question need be placed in the Question Section, since query
            //    type "ANY" (255) is used, which will elicit answers for all records
            //    with that name.  However, for tiebreaking to work correctly in all
            //    cases, the Authority Section must contain *all* the records and
            //    proposed rdata being probed for uniqueness.
            // it states *all* records, though we include ALL A/AAAA records as well, even
            // though it may not be relevant data if the probe query is published on different interfaces.
            // Having the same "format" probed on all interfaces, the simultaneous probe tiebreaking
            // algorithm can work correctly. Otherwise, we would conflict with ourselves in a situation were
            // a device is connected to the same network via Wi-Fi and Ethernet.
            this.records = [
                this.service.srvRecord(), this.service.txtRecord(),
                this.service.ptrRecord(), ...this.service.subtypePtrRecords(),
                ...this.service.allAddressRecords(), //...this.service.allReverseAddressMappings(),
            ].sort(tiebreaking_1.rrComparator); // we sort them for the tiebreaking algorithm
            this.records.forEach(record => record.flushFlag = false);
        }
        if (this.sentQueriesForCurrentTry >= 3) {
            // we sent three requests, and it seems like we weren't canceled, so we have a success right here
            this.endProbing(true);
            return;
        }
        if (this.sentQueries >= 15) {
            this.currentInterval = LIMITED_PROBE_INTERVAL;
        }
        debug("Sending prober query number %d for '%s'...", this.sentQueriesForCurrentTry + 1, this.service.getFQDN());
        (0, assert_1.default)(this.records.length > 0, "Tried sending probing request for zero record length!");
        const questions = [
            // probes SHOULD be sent with unicast response flag as of the RFC
            // MDNServer might overwrite the QU flag to false, as we can't use unicast if there is another responder on the machine
            new Question_1.Question(this.service.getFQDN(), 255 /* QType.ANY */, true),
            new Question_1.Question(this.service.getHostname(), 255 /* QType.ANY */, true),
        ];
        this.server.sendQueryBroadcast({
            questions: questions,
            // TODO certified homekit accessories only include the main service PTR record
            authorities: this.records, // include records we want to announce in authorities to support Simultaneous Probe Tiebreaking (RFC 6762 8.2.)
        }, this.service).then(results => {
            const failRatio = (0, MDNSServer_1.SendResultFailedRatio)(results);
            if (failRatio === 1) {
                console.error((0, MDNSServer_1.SendResultFormatError)(results, `Failed to send probe queries for '${this.service.getFQDN()}'`));
                this.endProbing(false);
                this.promiseReject(new Error("Probing failed as of socket errors!"));
                return; // all failed => thus probing failed
            }
            if (failRatio > 0) {
                // some queries on some interfaces failed, but not all. We log that but consider that to be a success
                // at this point we are not responsible for removing stale network interfaces or something
                debug((0, MDNSServer_1.SendResultFormatError)(results, `Some of the probe queries for '${this.service.getFQDN()}' encountered an error`));
                // SEE no return here
            }
            if (this.service.serviceState !== "probing" /* ServiceState.PROBING */) {
                debug("Service '%s' is no longer in probing state. Stopping.", this.service.getFQDN());
                return;
            }
            this.sentFirstProbeQuery = true;
            this.sentQueriesForCurrentTry++;
            this.sentQueries++;
            this.timer = setTimeout(this.sendProbeRequest.bind(this), this.currentInterval);
            this.timer.unref();
            this.checkLocalConflicts();
        });
    }
    checkLocalConflicts() {
        let containsAnswer = false;
        for (const service of this.responder.getAnnouncedServices()) {
            if (service.getLowerCasedFQDN() === this.service.getLowerCasedFQDN() || service.getLowerCasedHostname() === this.service.getLowerCasedHostname()) {
                containsAnswer = true;
                break;
            }
        }
        if (containsAnswer) {
            debug("Probing for '%s' failed as of local service. Doing a name change", this.service.getFQDN());
            this.handleNameChange();
        }
    }
    handleResponse(packet, endpoint) {
        if (!this.sentFirstProbeQuery || !this.service.advertisesOnInterface(endpoint.interface)) {
            return;
        }
        let containsAnswer = false;
        // search answers and additionals for answers to our probe queries
        for (const record of packet.answers.values()) {
            if (record.getLowerCasedName() === this.service.getLowerCasedFQDN() || record.getLowerCasedName() === this.service.getLowerCasedHostname()) {
                containsAnswer = true;
                break;
            }
        }
        for (const record of packet.additionals.values()) {
            if (record.getLowerCasedName() === this.service.getLowerCasedFQDN() || record.getLowerCasedName() === this.service.getLowerCasedHostname()) {
                containsAnswer = true;
                break;
            }
        }
        if (containsAnswer) { // abort and cancel probes
            debug("Probing for '%s' failed. Doing a name change", this.service.getFQDN());
            this.handleNameChange();
        }
    }
    handleNameChange() {
        this.endProbing(false); // reset the prober
        this.service.serviceState = "unannounced" /* ServiceState.UNANNOUNCED */;
        this.service.incrementName();
        this.service.serviceState = "probing" /* ServiceState.PROBING */;
        this.serviceEncounteredNameChange = true;
        this.timer = setTimeout(this.sendProbeRequest.bind(this), 1000);
        this.timer.unref();
    }
    handleQuery(packet, endpoint) {
        if (!this.sentFirstProbeQuery || !this.service.advertisesOnInterface(endpoint.interface)) {
            return;
        }
        // if we are currently probing and receiving a query which is also a probing query
        // which matches the desired name we run the tiebreaking algorithm to decide on the winner
        let needsTiebreaking = false;
        for (const question of packet.questions.values()) {
            if (question.getLowerCasedName() === this.service.getLowerCasedFQDN() || question.getLowerCasedName() === this.service.getLowerCasedHostname()) {
                needsTiebreaking = true;
                break;
            }
        }
        if (needsTiebreaking) {
            this.doTiebreaking(packet);
        }
    }
    doTiebreaking(packet) {
        if (!this.sentFirstProbeQuery) { // ignore queries if we are not sending
            return;
        }
        // first of all check if the contents of authorities answers our query
        let conflict = packet.authorities.size === 0;
        for (const record of packet.authorities.values()) {
            if (record.getLowerCasedName() === this.service.getLowerCasedFQDN() || record.getLowerCasedName() === this.service.getLowerCasedHostname()) {
                conflict = true;
                break;
            }
        }
        if (!conflict) {
            return;
        }
        // now run the actual tiebreaking algorithm to decide the winner
        // tiebreaking is actually run pretty often, as we always receive our own packets
        // first of all build our own records
        const answers = this.records; // already sorted
        const opponent = Array.from(packet.authorities.values()).sort(tiebreaking.rrComparator);
        const result = tiebreaking.runTiebreaking(answers, opponent);
        if (result === 1 /* TiebreakingResult.HOST */) {
            debug("'%s' won the tiebreak. We gonna ignore the other probing request!", this.service.getFQDN());
        }
        else if (result === -1 /* TiebreakingResult.OPPONENT */) {
            debug("'%s' lost the tiebreak. We are waiting a second and try to probe again...", this.service.getFQDN());
            this.endProbing(false); // cancel the current probing
            // wait 1 second and probe again (this is to guard against stale probe packets)
            // If it wasn't a stale probe packet, the other host will correctly respond to our probe queries by then
            this.timer = setTimeout(this.sendProbeRequest.bind(this), 1000);
            this.timer.unref();
        }
        else {
            //debug("Tiebreaking for '%s' detected exact same records on the network. There is actually no conflict!", this.service.getFQDN());
        }
    }
}
exports.Prober = Prober;
Prober.CANCEL_REASON = "CIAO PROBING CANCELLED";
//# sourceMappingURL=Prober.js.map