UNPKG

@homebridge/ciao

Version:

ciao is a RFC 6763 compliant dns-sd library, advertising on multicast dns (RFC 6762) implemented in plain Typescript/JavaScript

749 lines (744 loc) 37.1 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.CiaoService = exports.InternalServiceEvent = exports.ServiceEvent = exports.ServiceState = exports.ServiceType = void 0; const tslib_1 = require("tslib"); /* eslint-disable @typescript-eslint/no-unsafe-declaration-merging */ const assert_1 = tslib_1.__importDefault(require("assert")); const debug_1 = tslib_1.__importDefault(require("debug")); const events_1 = require("events"); const net_1 = tslib_1.__importDefault(require("net")); const AAAARecord_1 = require("./coder/records/AAAARecord"); const ARecord_1 = require("./coder/records/ARecord"); const NSECRecord_1 = require("./coder/records/NSECRecord"); const PTRRecord_1 = require("./coder/records/PTRRecord"); const SRVRecord_1 = require("./coder/records/SRVRecord"); const TXTRecord_1 = require("./coder/records/TXTRecord"); const ResourceRecord_1 = require("./coder/ResourceRecord"); const index_1 = require("./index"); const NetworkManager_1 = require("./NetworkManager"); const dns_equal_1 = require("./util/dns-equal"); const domainFormatter = tslib_1.__importStar(require("./util/domain-formatter")); const domain_formatter_1 = require("./util/domain-formatter"); const debug = (0, debug_1.default)("ciao:CiaoService"); const numberedServiceNamePattern = /^(.*) \((\d+)\)$/; // matches a name lik "My Service (2)" const numberedHostnamePattern = /^(.*)-\((\d+)\)(\.\w{2,}.)$/; // matches a hostname like "My-Computer-(2).local." /** * This enum defines some commonly used service types. * This is also referred to as service name (as of RFC 6763). * A service name must not be longer than 15 characters (RFC 6763 7.2). */ var ServiceType; (function (ServiceType) { // noinspection JSUnusedGlobalSymbols ServiceType["AIRDROP"] = "airdrop"; ServiceType["AIRPLAY"] = "airplay"; ServiceType["AIRPORT"] = "airport"; ServiceType["COMPANION_LINK"] = "companion-link"; ServiceType["DACP"] = "dacp"; ServiceType["HAP"] = "hap"; ServiceType["HOMEKIT"] = "homekit"; ServiceType["HTTP"] = "http"; ServiceType["HTTP_ALT"] = "http_alt"; ServiceType["IPP"] = "ipp"; ServiceType["IPPS"] = "ipps"; ServiceType["RAOP"] = "raop"; ServiceType["scanner"] = "scanner"; ServiceType["TOUCH_ABLE"] = "touch-able"; ServiceType["DNS_SD"] = "dns-sd"; ServiceType["PRINTER"] = "printer"; })(ServiceType || (exports.ServiceType = ServiceType = {})); /** * @private */ var ServiceState; (function (ServiceState) { ServiceState["UNANNOUNCED"] = "unannounced"; ServiceState["PROBING"] = "probing"; ServiceState["PROBED"] = "probed"; ServiceState["ANNOUNCING"] = "announcing"; ServiceState["ANNOUNCED"] = "announced"; })(ServiceState || (exports.ServiceState = ServiceState = {})); /** * Events thrown by a CiaoService */ var ServiceEvent; (function (ServiceEvent) { /** * Event is called when the Prober identifies that the name for the service is already used * and thus resolve the name conflict by adjusting the name (e.g. adding '(2)' to the name). * This change must be persisted and thus a listener must hook up to this event * in order for the name to be persisted. */ ServiceEvent["NAME_CHANGED"] = "name-change"; /** * Event is called when the Prober identifies that the hostname for the service is already used * and thus resolve the name conflict by adjusting the hostname (e.g. adding '(2)' to the hostname). * The name change must be persisted. As the hostname is an optional parameter, it is derived * from the service name if not supplied. * If you supply a custom hostname (not automatically derived from the service name) you must * hook up a listener to this event in order for the hostname to be persisted. */ ServiceEvent["HOSTNAME_CHANGED"] = "hostname-change"; })(ServiceEvent || (exports.ServiceEvent = ServiceEvent = {})); /** * Events thrown by a CiaoService, internal use only! * @private */ var InternalServiceEvent; (function (InternalServiceEvent) { InternalServiceEvent["PUBLISH"] = "publish"; InternalServiceEvent["UNPUBLISH"] = "unpublish"; InternalServiceEvent["REPUBLISH"] = "republish"; InternalServiceEvent["RECORD_UPDATE"] = "records-update"; InternalServiceEvent["RECORD_UPDATE_ON_INTERFACE"] = "records-update-interface"; })(InternalServiceEvent || (exports.InternalServiceEvent = InternalServiceEvent = {})); /** * The CiaoService class represents a service which can be advertised on the network. * * A service is identified by its fully qualified domain name (FQDN), which consist of * the service name, the service type, the protocol and the service domain (.local by default). * * The service defines a hostname and a port where the advertised service can be reached. * * Additionally, a TXT record can be published, which can contain information (in form of key-value pairs), * which might be useful to a querier. * * A CiaoService class is always bound to a {@link Responder} and can be created using the * {@link Responder.createService} method in the Responder class. * Once the instance is created, {@link advertise} can be called to announce the service on the network. */ class CiaoService extends events_1.EventEmitter { /** * Constructs a new service. Please use {@link Responder.createService} to create new service. * When calling the constructor a callee must listen to certain events in order to provide * correct functionality. * @private used by the Responder instance to create a new service */ constructor(networkManager, options) { super(); /** * this field is entirely controlled by the Responder class * @private use by the Responder to set the current service state */ this.serviceState = "unannounced" /* ServiceState.UNANNOUNCED */; this.destroyed = false; (0, assert_1.default)(networkManager, "networkManager is required"); (0, assert_1.default)(options, "parameters options is required"); (0, assert_1.default)(options.name, "service options parameter 'name' is required"); (0, assert_1.default)(options.type, "service options parameter 'type' is required"); (0, assert_1.default)(options.type.length <= 15, "service options parameter 'type' must not be longer than 15 characters"); this.networkManager = networkManager; this.name = options.name; this.type = options.type; this.subTypes = options.subtypes; this.protocol = options.protocol || "tcp" /* Protocol.TCP */; this.serviceDomain = options.domain || "local"; this.fqdn = this.formatFQDN(); this.loweredFqdn = (0, dns_equal_1.dnsLowerCase)(this.fqdn); this.typePTR = domainFormatter.stringify({ type: this.type, protocol: this.protocol, domain: this.serviceDomain, }); this.loweredTypePTR = (0, dns_equal_1.dnsLowerCase)(this.typePTR); if (this.subTypes) { this.subTypePTRs = this.subTypes.map(subtype => domainFormatter.stringify({ subtype: subtype, type: this.type, protocol: this.protocol, domain: this.serviceDomain, })).map(dns_equal_1.dnsLowerCase); } this.hostname = domainFormatter.formatHostname(options.hostname || this.name, this.serviceDomain) .replace(/ /g, "-"); // replacing all spaces with dashes in the hostname this.loweredHostname = (0, dns_equal_1.dnsLowerCase)(this.hostname); this.port = options.port; if (options.restrictedAddresses) { (0, assert_1.default)(options.restrictedAddresses.length, "The service property 'restrictedAddresses' cannot be an empty array!"); this.restrictedAddresses = new Map(); for (const entry of options.restrictedAddresses) { if (net_1.default.isIP(entry)) { if (entry === "0.0.0.0" || entry === "::") { throw new Error(`[${this.fqdn}] Unspecified ip address (${entry}) cannot be used to restrict on to!`); } const interfaceName = NetworkManager_1.NetworkManager.resolveInterface(entry); if (!interfaceName) { throw new Error(`[${this.fqdn}] Could not restrict service to address ${entry} as we could not resolve it to an interface name!`); } const current = this.restrictedAddresses.get(interfaceName); if (current) { // empty interface signals "catch all" was already configured for this if (current.length && !current.includes(entry)) { current.push(entry); } } else { this.restrictedAddresses.set(interfaceName, [entry]); } } else { this.restrictedAddresses.set(entry, []); // empty array signals "use all addresses for interface" } } } this.disableIpv6 = options.disabledIpv6; this.txt = options.txt ? CiaoService.txtBuffersFromRecord(options.txt) : []; // checks if hostname or name are already numbered and adjusts the numbers if necessary this.incrementName(true); } /** * This method start the advertising process of the service: * - The service name (and hostname) will be probed unique on all interfaces (as defined in RFC 6762 8.1). * - Once probed unique the service will be announced (as defined in RFC 6762 8.3). * * The returned promise resolves once the last announcement packet was successfully sent on all network interfaces. * The promise might be rejected caused by one of the following reasons: * - A probe query could not be sent successfully * - Prober could not find a unique service name while trying for a minute (timeout) * - One of the announcement packets could not be sent successfully */ advertise() { (0, assert_1.default)(!this.destroyed, "Cannot publish destroyed service!"); (0, assert_1.default)(this.port, "Service port must be defined before advertising the service on the network!"); if (this.listeners("name-change" /* ServiceEvent.NAME_CHANGED */).length === 0) { debug("[%s] WARN: No listeners found for a potential name change on the 'name-change' event!", this.name); } return new Promise((resolve, reject) => { this.emit("publish" /* InternalServiceEvent.PUBLISH */, error => error ? reject(error) : resolve()); }); } /** * This method will remove the advertisement for the service on all connected network interfaces. * If the service is still in the Probing state, probing will simply be cancelled. * * @returns Promise will resolve once the last goodbye packet was sent out */ end() { (0, assert_1.default)(!this.destroyed, "Cannot end destroyed service!"); if (this.serviceState === "unannounced" /* ServiceState.UNANNOUNCED */) { return Promise.resolve(); } return new Promise((resolve, reject) => { this.emit("unpublish" /* InternalServiceEvent.UNPUBLISH */, error => error ? reject(error) : resolve()); }); } /** * This method must be called if you want to free the memory used by this service. * The service instance is not usable anymore after this call. * * If the service is still announced, the service will first be removed * from the network by calling {@link end}. * * @returns */ async destroy() { await this.end(); this.destroyed = true; this.removeAllListeners(); } /** * @returns The fully qualified domain name of the service, used to identify the service. */ getFQDN() { return this.fqdn; } /** * @returns The service type pointer. */ getTypePTR() { return this.typePTR; } /** * @returns Array of subtype pointers (undefined if no subtypes are specified). */ getLowerCasedSubtypePTRs() { return this.subTypePTRs; } /** * @returns The current hostname of the service. */ getHostname() { return this.hostname; } /** * @returns The port the service is advertising for. * {@code -1} is returned when the port is not yet set. */ getPort() { return this.port || -1; } /** * @returns The current TXT of the service represented as Buffer array. * @private There is not need for this to be public API */ getTXT() { return this.txt; } /** * @private used for internal comparison {@link dnsLowerCase} */ getLowerCasedFQDN() { return this.loweredFqdn; } /** * @private used for internal comparison {@link dnsLowerCase} */ getLowerCasedTypePTR() { return this.loweredTypePTR; } /** * @private used for internal comparison {@link dnsLowerCase} */ getLowerCasedHostname() { return this.loweredHostname; } /** * Sets or updates the txt of the service. * * @param {ServiceTxt} txt - The updated txt record. * @param {boolean} silent - If set to true no announcement is sent for the updated record. */ updateTxt(txt, silent = false) { (0, assert_1.default)(!this.destroyed, "Cannot update destroyed service!"); (0, assert_1.default)(txt, "txt cannot be undefined"); this.txt = CiaoService.txtBuffersFromRecord(txt); debug("[%s] Updating txt record%s...", this.name, silent ? " silently" : ""); if (this.serviceState === "announcing" /* ServiceState.ANNOUNCING */) { this.rebuildServiceRecords(); if (silent) { return; } if (this.currentAnnouncer.hasSentLastAnnouncement()) { // if the announcer hasn't sent the last announcement, the above call of rebuildServiceRecords will // result in updated records on the next announcement. Otherwise, we still need to announce the updated records this.currentAnnouncer.awaitAnnouncement().then(() => { this.queueTxtUpdate(); }); } } else if (this.serviceState === "announced" /* ServiceState.ANNOUNCED */) { this.rebuildServiceRecords(); if (silent) { return; } this.queueTxtUpdate(); } } queueTxtUpdate() { if (this.txtTimer) { return; } else { // we debounce txt updates, otherwise if api users would spam txt updates, we would receive the txt record // while we already update our txt to the next call, thus causing a conflict being detected. // We would then continue with Probing (to ensure uniqueness) and could then receive following spammed updates as conflicts, // and we would change our name without it being necessary this.txtTimer = setTimeout(() => { this.txtTimer = undefined; if (this.serviceState !== "announced" /* ServiceState.ANNOUNCED */) { // stuff changed in the last 50 milliseconds return; } this.emit("records-update" /* InternalServiceEvent.RECORD_UPDATE */, { answers: [this.txtRecord()], additionals: [this.serviceNSECRecord()], }); }, 50); } } /** * Sets or updates the port of the service. * A new port number can only be set when the service is still UNANNOUNCED. * Otherwise, an assertion error will be thrown. * * @param {number} port - The new port number. */ updatePort(port) { (0, assert_1.default)(this.serviceState === "unannounced" /* ServiceState.UNANNOUNCED */, "Port number cannot be changed when service is already advertised!"); this.port = port; } /** * This method updates the name of the service. * @param name - The new service name. * @private Currently not public API and only used for bonjour conformance testing. */ updateName(name) { if (this.serviceState === "unannounced" /* ServiceState.UNANNOUNCED */) { this.name = name; this.fqdn = this.formatFQDN(); this.loweredFqdn = (0, dns_equal_1.dnsLowerCase)(this.fqdn); return Promise.resolve(); } else { return this.end() // send goodbye packets for the current name .then(() => { this.name = name; this.fqdn = this.formatFQDN(); this.loweredFqdn = (0, dns_equal_1.dnsLowerCase)(this.fqdn); // service records are going to be rebuilt on the 'advertise' step return this.advertise(); }); } } static txtBuffersFromRecord(txt) { const result = []; Object.entries(txt).forEach(([key, value]) => { const entry = key + "=" + value; result.push(Buffer.from(entry)); }); return result; } /** * @param networkUpdate * @private */ handleNetworkInterfaceUpdate(networkUpdate) { (0, assert_1.default)(!this.destroyed, "Cannot update network of destroyed service!"); // this will currently only be called when service is ANNOUNCED or in ANNOUNCING state if (this.serviceState !== "announced" /* ServiceState.ANNOUNCED */) { if (this.serviceState === "announcing" /* ServiceState.ANNOUNCING */) { this.rebuildServiceRecords(); if (this.currentAnnouncer.hasSentLastAnnouncement()) { // if the announcer hasn't sent the last announcement, the above call of rebuildServiceRecords will // result in updated records on the next announcement. Otherwise, we still need to announce the updated records this.currentAnnouncer.awaitAnnouncement().then(() => { this.handleNetworkInterfaceUpdate(networkUpdate); }); } } return; // service records are rebuilt short before the 'announce' step } // we don't care about removed interfaces. We can't send goodbye records on a non-existing interface this.rebuildServiceRecords(); // records for a removed interface are now no longer present after the call above // records for a new interface got now built by the call above /* logic disabled for now if (networkUpdate.changes) { // we could optimize this and don't send the announcement of records if we have also added a new interface // Though probing will take at least 750 ms and thus sending it out immediately will get the information out faster. for (const change of networkUpdate.changes) { if (!this.advertisesOnInterface(change.name, true)) { continue; } let restrictedAddresses: IPAddress[] | undefined = this.restrictedAddresses? this.restrictedAddresses.get(change.name): undefined; if (restrictedAddresses && restrictedAddresses.length === 0) { restrictedAddresses = undefined; } const records: ResourceRecord[] = []; if (change.outdatedIpv4 && (!restrictedAddresses || restrictedAddresses.includes(change.outdatedIpv4))) { records.push(new ARecord(this.hostname, change.outdatedIpv4, true, 0)); // records.push(new PTRRecord(formatReverseAddressPTRName(change.outdatedIpv4), this.hostname, false, 0)); } if (change.outdatedIpv6 && !this.disableIpv6 && (!restrictedAddresses || restrictedAddresses.includes(change.outdatedIpv6))) { records.push(new AAAARecord(this.hostname, change.outdatedIpv6, true, 0)); // records.push(new PTRRecord(formatReverseAddressPTRName(change.outdatedIpv6), this.hostname, false, 0)); } if (change.outdatedGloballyRoutableIpv6 && !this.disableIpv6 && (!restrictedAddresses || restrictedAddresses.includes(change.outdatedGloballyRoutableIpv6))) { records.push(new AAAARecord(this.hostname, change.outdatedGloballyRoutableIpv6, true, 0)); // records.push(new PTRRecord(formatReverseAddressPTRName(change.outdatedGloballyRoutableIpv6), this.hostname, false, 0)); } if (change.outdatedUniqueLocalIpv6 && !this.disableIpv6 && (!restrictedAddresses || restrictedAddresses.includes(change.outdatedUniqueLocalIpv6))) { records.push(new AAAARecord(this.hostname, change.outdatedUniqueLocalIpv6, true, 0)); // records.push(new PTRRecord(formatReverseAddressPTRName(change.outdatedUniqueLocalIpv6), this.hostname, false, 0)); } if (change.updatedIpv4 && (!restrictedAddresses || restrictedAddresses.includes(change.updatedIpv4))) { records.push(new ARecord(this.hostname, change.updatedIpv4, true)); // records.push(new PTRRecord(formatReverseAddressPTRName(change.updatedIpv4), this.hostname)); } if (change.updatedIpv6 && !this.disableIpv6 && (!restrictedAddresses || restrictedAddresses.includes(change.updatedIpv6))) { records.push(new AAAARecord(this.hostname, change.updatedIpv6, true)); // records.push(new PTRRecord(formatReverseAddressPTRName(change.updatedIpv6), this.hostname)); } if (change.updatedGloballyRoutableIpv6 && !this.disableIpv6 && (!restrictedAddresses || restrictedAddresses.includes(change.updatedGloballyRoutableIpv6))) { records.push(new AAAARecord(this.hostname, change.updatedGloballyRoutableIpv6, true)); // records.push(new PTRRecord(formatReverseAddressPTRName(change.updatedGloballyRoutableIpv6), this.hostname)); } if (change.updatedUniqueLocalIpv6 && !this.disableIpv6 && (!restrictedAddresses || restrictedAddresses.includes(change.updatedUniqueLocalIpv6))) { records.push(new AAAARecord(this.hostname, change.updatedUniqueLocalIpv6, true)); // records.push(new PTRRecord(formatReverseAddressPTRName(change.updatedUniqueLocalIpv6), this.hostname)); } this.emit(InternalServiceEvent.RECORD_UPDATE_ON_INTERFACE, change.name, records); } } */ if (networkUpdate.added || networkUpdate.changes) { // a new network interface got added. We must return into probing state, // as we don't know if we still own uniqueness for our service name on the new network. // To make things easy and keep the SAME name on all networks, we probe on ALL interfaces. // at this moment the new socket won't be bound. Though probing steps are delayed, // thus, when sending the first request, the socket will be bound, and we don't need to wait here this.emit("republish" /* InternalServiceEvent.REPUBLISH */, error => { if (error) { console.log("FATAL Error occurred trying to re-announce service " + this.fqdn + "! We can't recover from this!"); console.log(error.stack); process.exit(1); // we have a service which should be announced, though we failed to re-announce. // if this should ever happen in reality, whe might want to introduce a more sophisticated recovery // for situations where it makes sense } }); } } /** * This method is called by the Prober when encountering a conflict on the network. * It advises the service to change its name, like incrementing a number appended to the name. * So "My Service" will become "My Service (2)", and "My Service (2)" would become "My Service (3)" * @private must only be called by the {@link Prober} */ incrementName(nameCheckOnly) { if (this.serviceState !== "unannounced" /* ServiceState.UNANNOUNCED */) { throw new Error("Service name can only be incremented when in state UNANNOUNCED!"); } const oldName = this.name; const oldHostname = this.hostname; let nameBase; let nameNumber; let hostnameBase; let hostnameTLD; let hostnameNumber; const nameMatcher = this.name.match(numberedServiceNamePattern); if (nameMatcher) { // if it matched. Extract the current nameNumber nameBase = nameMatcher[1]; nameNumber = parseInt(nameMatcher[2]); (0, assert_1.default)(nameNumber, `Failed to extract name number from ${this.name}. Resulted in ${nameNumber}`); } else { nameBase = this.name; nameNumber = 1; } const hostnameMatcher = this.hostname.match(numberedHostnamePattern); if (hostnameMatcher) { // if it matched. Extract the current nameNumber hostnameBase = hostnameMatcher[1]; hostnameTLD = hostnameMatcher[3]; hostnameNumber = parseInt(hostnameMatcher[2]); (0, assert_1.default)(hostnameNumber, `Failed to extract hostname number from ${this.hostname}. Resulted in ${hostnameNumber}`); } else { // we need to substring, to not match the root label "." const lastDot = this.hostname.substring(0, this.hostname.length - 1).lastIndexOf("."); hostnameBase = this.hostname.slice(0, lastDot); hostnameTLD = this.hostname.slice(lastDot); hostnameNumber = 1; } if (!nameCheckOnly) { // increment the numbers nameNumber++; hostnameNumber++; } const newNumber = Math.max(nameNumber, hostnameNumber); // reassemble the name this.name = newNumber === 1 ? nameBase : `${nameBase} (${newNumber})`; this.hostname = newNumber === 1 ? `${hostnameBase}${hostnameTLD}` : `${hostnameBase}-(${newNumber})${hostnameTLD}`; this.loweredHostname = (0, dns_equal_1.dnsLowerCase)(this.hostname); this.fqdn = this.formatFQDN(); // update the fqdn this.loweredFqdn = (0, dns_equal_1.dnsLowerCase)(this.fqdn); // we must inform the user that the names changed, so the new names can be persisted // This is done after the Probing finish, as multiple name changes could happen in one probing session // It is the responsibility of the Prober to call the informAboutNameUpdates function if (this.name !== oldName || this.hostname !== oldHostname) { debug("[%s] Service changed name '%s' -> '%s', '%s' -> '%s'", this.name, oldName, this.name, oldHostname, this.hostname); } if (!nameCheckOnly) { this.rebuildServiceRecords(); // rebuild all services } } /** * @private called by the Prober once finished with probing to signal a (or more) * name change(s) happened {@see incrementName}. */ informAboutNameUpdates() { // we trust the prober that this function is only called when the name was actually changed const nameCalled = this.emit("name-change" /* ServiceEvent.NAME_CHANGED */, this.name); const hostnameCalled = this.emit("hostname-change" /* ServiceEvent.HOSTNAME_CHANGED */, domainFormatter.removeTLD(this.hostname)); // at least one event should be listened to. We can figure out the number from one or another if (!nameCalled && !hostnameCalled) { console.warn(`CIAO: [${this.name}] Service changed name but nobody was listening on the 'name-change' event!`); } } formatFQDN() { if (this.serviceState !== "unannounced" /* ServiceState.UNANNOUNCED */) { throw new Error("Name can't be changed after service was already announced!"); } const fqdn = domainFormatter.stringify({ name: this.name, type: this.type, protocol: this.protocol, domain: this.serviceDomain, }); (0, assert_1.default)(fqdn.length <= 255, "A fully qualified domain name cannot be longer than 255 characters"); return fqdn; } /** * @private called once the service data/state is updated and the records should be updated with the new data */ rebuildServiceRecords() { (0, assert_1.default)(this.port, "port must be set before building records"); debug("[%s] Rebuilding service records...", this.name); const aRecordMap = {}; const aaaaRecordMap = {}; const aaaaRoutableRecordMap = {}; const aaaaUniqueLocalRecordMap = {}; const reverseAddressMap = {}; let subtypePTRs = undefined; for (const [name, networkInterface] of this.networkManager.getInterfaceMap()) { if (!this.advertisesOnInterface(name, true)) { continue; } let restrictedAddresses = this.restrictedAddresses ? this.restrictedAddresses.get(name) : undefined; if (restrictedAddresses && restrictedAddresses.length === 0) { restrictedAddresses = undefined; } if (networkInterface.ipv4 && (!restrictedAddresses || restrictedAddresses.includes(networkInterface.ipv4))) { aRecordMap[name] = new ARecord_1.ARecord(this.hostname, networkInterface.ipv4, true); reverseAddressMap[networkInterface.ipv4] = new PTRRecord_1.PTRRecord((0, domain_formatter_1.formatReverseAddressPTRName)(networkInterface.ipv4), this.hostname); } if (networkInterface.ipv6 && !this.disableIpv6 && (!restrictedAddresses || restrictedAddresses.includes(networkInterface.ipv6))) { aaaaRecordMap[name] = new AAAARecord_1.AAAARecord(this.hostname, networkInterface.ipv6, true); reverseAddressMap[networkInterface.ipv6] = new PTRRecord_1.PTRRecord((0, domain_formatter_1.formatReverseAddressPTRName)(networkInterface.ipv6), this.hostname); } if (networkInterface.globallyRoutableIpv6 && !this.disableIpv6 && (!restrictedAddresses || restrictedAddresses.includes(networkInterface.globallyRoutableIpv6))) { aaaaRoutableRecordMap[name] = new AAAARecord_1.AAAARecord(this.hostname, networkInterface.globallyRoutableIpv6, true); reverseAddressMap[networkInterface.globallyRoutableIpv6] = new PTRRecord_1.PTRRecord((0, domain_formatter_1.formatReverseAddressPTRName)(networkInterface.globallyRoutableIpv6), this.hostname); } if (networkInterface.uniqueLocalIpv6 && !this.disableIpv6 && (!restrictedAddresses || restrictedAddresses.includes(networkInterface.uniqueLocalIpv6))) { aaaaUniqueLocalRecordMap[name] = new AAAARecord_1.AAAARecord(this.hostname, networkInterface.uniqueLocalIpv6, true); reverseAddressMap[networkInterface.uniqueLocalIpv6] = new PTRRecord_1.PTRRecord((0, domain_formatter_1.formatReverseAddressPTRName)(networkInterface.uniqueLocalIpv6), this.hostname); } } if (this.subTypePTRs) { subtypePTRs = []; for (const ptr of this.subTypePTRs) { subtypePTRs.push(new PTRRecord_1.PTRRecord(ptr, this.fqdn)); } } this.serviceRecords = { ptr: new PTRRecord_1.PTRRecord(this.typePTR, this.fqdn), subtypePTRs: subtypePTRs, // possibly undefined metaQueryPtr: new PTRRecord_1.PTRRecord(index_1.Responder.SERVICE_TYPE_ENUMERATION_NAME, this.typePTR), srv: new SRVRecord_1.SRVRecord(this.fqdn, this.hostname, this.port, true), txt: new TXTRecord_1.TXTRecord(this.fqdn, this.txt, true), serviceNSEC: new NSECRecord_1.NSECRecord(this.fqdn, this.fqdn, [16 /* RType.TXT */, 33 /* RType.SRV */], 4500, true), // 4500 ttl of src and txt a: aRecordMap, aaaa: aaaaRecordMap, aaaaR: aaaaRoutableRecordMap, aaaaULA: aaaaUniqueLocalRecordMap, reverseAddressPTRs: reverseAddressMap, addressNSEC: new NSECRecord_1.NSECRecord(this.hostname, this.hostname, [1 /* RType.A */, 28 /* RType.AAAA */], 120, true), // 120 TTL of A and AAAA records }; } /** * Returns if the given service is advertising on the provided network interface. * * @param name - The desired interface name. * @param skipAddressCheck - If true it is not checked if the service actually has * an address record for the given interface. * @private returns if the service should be advertised on the given service */ advertisesOnInterface(name, skipAddressCheck) { var _a, _b, _c, _d; return !this.restrictedAddresses || this.restrictedAddresses.has(name) && (skipAddressCheck || // must have at least one address record on the given interface !!((_a = this.serviceRecords) === null || _a === void 0 ? void 0 : _a.a[name]) || !!((_b = this.serviceRecords) === null || _b === void 0 ? void 0 : _b.aaaa[name]) || !!((_c = this.serviceRecords) === null || _c === void 0 ? void 0 : _c.aaaaR[name]) || !!((_d = this.serviceRecords) === null || _d === void 0 ? void 0 : _d.aaaaULA[name])); } /** * @private used to get a copy of the main PTR record */ ptrRecord() { return this.serviceRecords.ptr.clone(); } /** * @private used to get a copy of the array of subtype PTR records */ subtypePtrRecords() { return this.serviceRecords.subtypePTRs ? ResourceRecord_1.ResourceRecord.clone(this.serviceRecords.subtypePTRs) : []; } /** * @private used to get a copy of the meta-query PTR record */ metaQueryPtrRecord() { return this.serviceRecords.metaQueryPtr.clone(); } /** * @private used to get a copy of the SRV record */ srvRecord() { return this.serviceRecords.srv.clone(); } /** * @private used to get a copy of the TXT record */ txtRecord() { return this.serviceRecords.txt.clone(); } /** * @private used to get a copy of the A record */ aRecord(name) { const record = this.serviceRecords.a[name]; return record ? record.clone() : undefined; } /** * @private used to get a copy of the AAAA record for the link-local ipv6 address */ aaaaRecord(name) { const record = this.serviceRecords.aaaa[name]; return record ? record.clone() : undefined; } /** * @private used to get a copy of the AAAA record for the routable ipv6 address */ aaaaRoutableRecord(name) { const record = this.serviceRecords.aaaaR[name]; return record ? record.clone() : undefined; } /** * @private used to get a copy of the AAAA for the unique local ipv6 address */ aaaaUniqueLocalRecord(name) { const record = this.serviceRecords.aaaaULA[name]; return record ? record.clone() : undefined; } /** * @private used to get a copy of the A and AAAA records */ allAddressRecords() { const records = []; Object.values(this.serviceRecords.a).forEach(record => { records.push(record.clone()); }); Object.values(this.serviceRecords.aaaa).forEach(record => { records.push(record.clone()); }); Object.values(this.serviceRecords.aaaaR).forEach(record => { records.push(record.clone()); }); Object.values(this.serviceRecords.aaaaULA).forEach(record => { records.push(record.clone()); }); return records; } /** * @private used to get a copy of the address NSEC record */ addressNSECRecord() { return this.serviceRecords.addressNSEC.clone(); } /** * @private user to get a copy of the service NSEC record */ serviceNSECRecord(shortenTTL = false) { const record = this.serviceRecords.serviceNSEC.clone(); if (shortenTTL) { record.ttl = 120; } return record; } /** * @param address - The IP address to check. * @private used to check if given address is exposed by this service */ hasAddress(address) { return !!this.serviceRecords.reverseAddressPTRs[address]; } } exports.CiaoService = CiaoService; //# sourceMappingURL=CiaoService.js.map