import assert from "assert" ; import createDebug from "debug" ; import { CiaoService , InternalServiceEvent , PublishCallback , RecordsUpdateCallback , ServiceOptions , ServiceState , UnpublishCallback , } from "./CiaoService" ; import { DNSPacket , DNSResponseDefinition , QClass , QType , RType } from "./coder/DNSPacket" ; import { Question } from "./coder/Question" ; import { AAAARecord } from "./coder/records/AAAARecord" ; import { ARecord } from "./coder/records/ARecord" ; import { OPTRecord } from "./coder/records/OPTRecord" ; import { PTRRecord } from "./coder/records/PTRRecord" ; import { SRVRecord } from "./coder/records/SRVRecord" ; import { TXTRecord } from "./coder/records/TXTRecord" ; import { ResourceRecord } from "./coder/ResourceRecord" ; import { EndpointInfo , MDNSServer , MDNSServerOptions , PacketHandler , SendResultFailedRatio , SendResultFormatError , } from "./MDNSServer" ; import { InterfaceName , NetworkManagerEvent , NetworkUpdate } from "./NetworkManager" ; import { Announcer } from "./responder/Announcer" ; import { Prober } from "./responder/Prober" ; import { QueryResponse , RecordAddMethod } from "./responder/QueryResponse" ; import { QueuedResponse } from "./responder/QueuedResponse" ; import { TruncatedQuery , TruncatedQueryEvent , TruncatedQueryResult } from "./responder/TruncatedQuery" ; import { ERR_INTERFACE_NOT_FOUND , ERR_SERVER_CLOSED } from "./util/errors" ; import { PromiseTimeout } from "./util/promise-utils" ; import { sortedInsert } from "./util/sorted-array" ; import Timeout = NodeJS . Timeout ; const debug = createDebug ( "ciao:Responder" ) ; const queuedResponseComparator = ( a , b ) => { return a . estimatedTimeToBeSent - b . estimatedTimeToBeSent ; } ; const enum ConflictType { NO_CONFLICT , CONFLICTING_RDATA , CONFLICTING_TTL } export type ResponderOptions = { private periodicBroadcasts ? : boolean ; private ignoreUnicastResponseFlag ? : boolean ; } & MDNSServerOptions ; /**
 * A Responder instance represents a running MDNSServer and a set of advertised services.
 *
 * It will handle any service related operations, like advertising, sending goodbye packets or sending record updates.
 * It handles answering questions arriving on the multicast address.
 */ export class Responder implements PacketHandler { private public static readonly SERVICE_TYPE_ENUMERATION_NAME = "_services._dns-sd._udp.local." ; private static readonly INSTANCES : Map < string , Responder > = new Map ( ) ; private readonly server : MDNSServer ; private promiseChain : Promise < void > ; private refCount = 1 ; private optionsString = "" ; private bound = false ; /**
     * Announced services is indexed by the  dnsLowerCase if the fqdn (as of RFC 1035 3.1).
     * As soon as the probing step is finished the service is added to the announced services Map.
      private readonly announcedServices : Map < string , CiaoService > = new Map ( ) ; /**
     * map representing all our shared PTR records.
     * Typically we hold stuff like '_services._dns-sd._udp.local' (RFC 6763 9.), '_hap._tcp.local'.
     * Also pointers for every subtype like '_printer._sub._http._tcp.local' are inserted here.
     *
     * For every pointer we may hold multiple entries (like multiple services can advertise on _hap._tcp.local).
     * The key as well as all values are  dnsLowerCase private readonly servicePointer : Map < string , string [ ] > = new Map ( ) ; private readonly truncatedQueries : Record < string , TruncatedQuery > = { } ; private readonly delayedMulticastResponses : QueuedResponse [ ] = [ ] ; private currentProber ? : Prober ; private ignoreUnicastResponseFlag ? : boolean ; private broadcastInterval ? : Timeout ; /**
     * Refer to  getResponder in the index file
     *
     *  private public static getResponder ( options ? ) { const optionsString = options ? JSON . stringify ( options ) : "" ; const responder = this . INSTANCES . get ( optionsString ) ; if ( responder ) { responder . refCount ++ ; return responder ; } else { const responder = new Responder ( options ) ; this . INSTANCES . set ( optionsString , responder ) ; responder . optionsString = optionsString ; return responder ; } } private constructor ( options ? ) { this . server = new MDNSServer ( this , options ) ; this . promiseChain = this . start ( ) ; this . server . getNetworkManager ( ) . on ( NetworkManagerEvent . NETWORK_UPDATE , this . handleNetworkUpdate . bind ( this ) ) ; this . ignoreUnicastResponseFlag = options ?. ignoreUnicastResponseFlag ; if ( options ?. periodicBroadcasts ) { this . broadcastInterval = setTimeout ( this . handlePeriodicBroadcasts . bind ( this ) , 30000 ) . unref ( ) ; } } private handlePeriodicBroadcasts ( ) { this . broadcastInterval = undefined ; debug ( "Sending periodic announcement on " + Array . from ( this . server . getNetworkManager ( ) . getInterfaceMap ( ) . keys ( ) ) . join ( ", " ) ) ; for ( const networkInterface of this . server . getNetworkManager ( ) . getInterfaceMap ( ) . values ( ) ) { const question = new Question ( "_hap._tcp.local." , QType . PTR , false ) ; const responses = this . answerQuestion ( question , { port : 5353 , address : ( networkInterface . ipv4Netaddress || networkInterface . globallyRoutableIpv6 || networkInterface . uniqueLocalIpv6 || networkInterface . ipv6 ) ! , interface : networkInterface . name , } ) ; QueryResponse . combineResponses ( responses ) ; for ( const response of responses ) { if ( ! response . hasAnswers ( ) ) { continue ; } this . server . sendResponse ( response . asPacket ( ) , networkInterface . name ) ; } } this . broadcastInterval = setTimeout ( this . handlePeriodicBroadcasts . bind ( this ) , Math . random ( ) * 3000 + 27000 ) . unref ( ) ; } param { ServiceOptions } options returns The newly created  CiaoService instance can be used to advertise and manage the created service.
      public createService ( options ) { const service = new CiaoService ( this . server . getNetworkManager ( ) , options ) ; service . on ( InternalServiceEvent . PUBLISH , this . advertiseService . bind ( this , service ) ) ; service . on ( InternalServiceEvent . UNPUBLISH , this . unpublishService . bind ( this , service ) ) ; service . on ( InternalServiceEvent . REPUBLISH , this . republishService . bind ( this , service ) ) ; service . on ( InternalServiceEvent . RECORD_UPDATE , this . handleServiceRecordUpdate . bind ( this , service ) ) ; service . on ( InternalServiceEvent . RECORD_UPDATE_ON_INTERFACE , this . handleServiceRecordUpdateOnInterface . bind ( this , service ) ) ; return service ; } returns public shutdown ( ) { this . refCount -- ; if ( this . refCount > 0 ) { return Promise . resolve ( ) ; } if ( this . currentProber ) { this . currentProber . cancel ( ) ; } if ( this . broadcastInterval ) { clearTimeout ( this . broadcastInterval ) ; } Responder . INSTANCES . delete ( this . optionsString ) ; debug ( "Shutting down Responder..." ) ; const promises = [ ] ; for ( const service of this . announcedServices . values ( ) ) { promises . push ( this . unpublishService ( service ) ) ; } return Promise . all ( promises ) . then ( ( ) => { this . server . shutdown ( ) ; this . bound = false ; } ) ; } public getAnnouncedServices ( ) { return this . announcedServices . values ( ) ; } private start ( ) { if ( this . bound ) { throw new Error ( "Server is already bound!" ) ; } this . bound = true ; return this . server . bind ( ) ; } private advertiseService ( service , callback ) { if ( service . serviceState === ServiceState . ANNOUNCED ) { throw new Error ( "Can't publish a service that is already announced. Received " + service . serviceState + " for service " + service . getFQDN ( ) ) ; } else if ( service . serviceState === ServiceState . PROBING ) { return this . promiseChain . then ( ( ) => { if ( service . currentAnnouncer ) { return service . currentAnnouncer . awaitAnnouncement ( ) ; } } ) ; } else if ( service . serviceState === ServiceState . ANNOUNCING ) { assert ( service . currentAnnouncer , "Service is in state ANNOUNCING though has no linked announcer!" ) ; if ( service . currentAnnouncer ! . isSendingGoodbye ( ) ) { return service . currentAnnouncer ! . awaitAnnouncement ( ) . then ( ( ) => this . advertiseService ( service , callback ) ) ; } else { return service . currentAnnouncer ! . cancel ( ) . then ( ( ) => this . advertiseService ( service , callback ) ) ; } } debug ( "[%s] Going to advertise service..." , service . getFQDN ( ) ) ; this . promiseChain = this . promiseChain . then ( ( ) => service . rebuildServiceRecords ( ) ) . then ( ( ) => this . probe ( service ) ) ; return this . promiseChain . then ( ( ) => { this . announce ( service ) . catch ( reason => { console . log ( `[${ service . getFQDN ( ) }] failed announcing with reason: ${ reason }. Trying again in 2 seconds!` ) ; return PromiseTimeout ( 2000 ) . then ( ( ) => this . advertiseService ( service , ( ) => { } ) ) ; } ) ; callback ( ) ; } , reason => { if ( reason === Prober . CANCEL_REASON ) { callback ( ) ; } else { console . log ( `[${ service . getFQDN ( ) }] failed probing with reason: ${ reason }. Trying again in 2 seconds!` ) ; return PromiseTimeout ( 2000 ) . then ( ( ) => this . advertiseService ( service , callback ) ) ; } } ) ; } private republishService ( service , callback , delayAnnounce = false ) { if ( service . serviceState !== ServiceState . ANNOUNCED && service . serviceState !== ServiceState . ANNOUNCING ) { throw new Error ( "Can't unpublish a service which isn't announced yet. Received " + service . serviceState + " for service " + service . getFQDN ( ) ) ; } debug ( "[%s] Readvertising service..." , service . getFQDN ( ) ) ; if ( service . serviceState === ServiceState . ANNOUNCING ) { assert ( service . currentAnnouncer , "Service is in state ANNOUNCING though has no linked announcer!" ) ; const promise = service . currentAnnouncer ! . isSendingGoodbye ( ) ? service . currentAnnouncer ! . awaitAnnouncement ( ) : service . currentAnnouncer ! . cancel ( ) ; return promise . then ( ( ) => this . advertiseService ( service , callback ) ) ; } this . clearService ( service ) ; service . serviceState = ServiceState . UNANNOUNCED ; if ( delayAnnounce ) { return PromiseTimeout ( 1000 ) . then ( ( ) => this . advertiseService ( service , callback ) ) ; } else { return this . advertiseService ( service , callback ) ; } } private unpublishService ( service , callback ? ) { if ( service . serviceState === ServiceState . UNANNOUNCED ) { throw new Error ( "Can't unpublish a service which isn't announced yet. Received " + service . serviceState + " for service " + service . getFQDN ( ) ) ; } if ( service . serviceState === ServiceState . ANNOUNCED || service . serviceState === ServiceState . ANNOUNCING ) { if ( service . serviceState === ServiceState . ANNOUNCING ) { assert ( service . currentAnnouncer , "Service is in state ANNOUNCING though has no linked announcer!" ) ; if ( service . currentAnnouncer ! . isSendingGoodbye ( ) ) { return service . currentAnnouncer ! . awaitAnnouncement ( ) ; } return service . currentAnnouncer ! . cancel ( ) . then ( ( ) => { service . serviceState = ServiceState . ANNOUNCED ; return this . unpublishService ( service , callback ) ; } ) ; } debug ( "[%s] Removing service from the network" , service . getFQDN ( ) ) ; this . clearService ( service ) ; service . serviceState = ServiceState . UNANNOUNCED ; let promise = this . goodbye ( service ) ; if ( callback ) { promise = promise . then ( ( ) => callback ( ) , reason => { console . log ( `[${ service . getFQDN ( ) }] failed goodbye with reason: ${ reason }.` ) ; callback ( ) ; } ) ; } return promise ; } else if ( service . serviceState === ServiceState . PROBING ) { debug ( "[%s] Canceling probing" , service . getFQDN ( ) ) ; if ( this . currentProber && this . currentProber . getService ( ) === service ) { this . currentProber . cancel ( ) ; this . currentProber = undefined ; } service . serviceState = ServiceState . UNANNOUNCED ; } callback && callback ( ) ; return Promise . resolve ( ) ; } private clearService ( service ) { const serviceFQDN = service . getLowerCasedFQDN ( ) ; const typePTR = service . getLowerCasedTypePTR ( ) ; const subtypePTRs = service . getLowerCasedSubtypePTRs ( ) ; this . removePTR ( Responder . SERVICE_TYPE_ENUMERATION_NAME , typePTR ) ; this . removePTR ( typePTR , serviceFQDN ) ; if ( subtypePTRs ) { for ( const ptr of subtypePTRs ) { this . removePTR ( ptr , serviceFQDN ) ; } } this . announcedServices . delete ( service . getLowerCasedFQDN ( ) ) ; } private addPTR ( ptr , name ) { const names = this . servicePointer . get ( ptr ) ; if ( names ) { if ( ! names . includes ( name ) ) { names . push ( name ) ; } } else { this . servicePointer . set ( ptr , [ name ] ) ; } } private removePTR ( ptr , name ) { const names = this . servicePointer . get ( ptr ) ; if ( names ) { const index = names . indexOf ( name ) ; if ( index !== - 1 ) { names . splice ( index , 1 ) ; } if ( names . length === 0 ) { this . servicePointer . delete ( ptr ) ; } } } private probe ( service ) { if ( service . serviceState !== ServiceState . UNANNOUNCED ) { throw new Error ( "Can't probe for a service which is announced already. Received " + service . serviceState + " for service " + service . getFQDN ( ) ) ; } service . serviceState = ServiceState . PROBING ; assert ( this . currentProber === undefined , "Tried creating new Prober when there already was one active!" ) ; this . currentProber = new Prober ( this , this . server , service ) ; return this . currentProber . probe ( ) . then ( ( ) => { this . currentProber = undefined ; service . serviceState = ServiceState . PROBED ; } , reason => { service . serviceState = ServiceState . UNANNOUNCED ; this . currentProber = undefined ; return Promise . reject ( reason ) ; } ) ; } private announce ( service ) { if ( service . serviceState !== ServiceState . PROBED ) { throw new Error ( "Cannot announce service which was not probed unique. Received " + service . serviceState + " for service " + service . getFQDN ( ) ) ; } assert ( service . currentAnnouncer === undefined , "Service " + service . getFQDN ( ) + " is already announcing!" ) ; service . serviceState = ServiceState . ANNOUNCING ; const announcer = new Announcer ( this . server , service , { repetitions : 3 , } ) ; service . currentAnnouncer = announcer ; const serviceFQDN = service . getLowerCasedFQDN ( ) ; const typePTR = service . getLowerCasedTypePTR ( ) ; const subtypePTRs = service . getLowerCasedSubtypePTRs ( ) ; this . addPTR ( Responder . SERVICE_TYPE_ENUMERATION_NAME , typePTR ) ; this . addPTR ( typePTR , serviceFQDN ) ; if ( subtypePTRs ) { for ( const ptr of subtypePTRs ) { this . addPTR ( ptr , serviceFQDN ) ; } } this . announcedServices . set ( serviceFQDN , service ) ; return announcer . announce ( ) . then ( ( ) => { service . serviceState = ServiceState . ANNOUNCED ; service . currentAnnouncer = undefined ; } , reason => { service . serviceState = ServiceState . UNANNOUNCED ; service . currentAnnouncer = undefined ; this . clearService ( service ) ; if ( reason !== Announcer . CANCEL_REASON ) { return Promise . reject ( reason ) ; } } ) ; } private handleServiceRecordUpdate ( service , response , callback ? ) { if ( service . serviceState !== ServiceState . ANNOUNCED ) { throw new Error ( "Cannot update txt of service which is not announced yet. Received " + service . serviceState + " for service " + service . getFQDN ( ) ) ; } debug ( "[%s] Updating %d record(s) for given service!" , service . getFQDN ( ) , response . answers . length + ( response . additionals ?. length || 0 ) ) ; this . server . sendResponseBroadcast ( response , service ) . then ( results => { const failRatio = SendResultFailedRatio ( results ) ; if ( failRatio === 1 ) { console . log ( SendResultFormatError ( results , `Failed to send records update for '${ service . getFQDN ( ) }'` ) , true ) ; if ( callback ) { callback ( new Error ( "Updating records failed as of socket errors!" ) ) ; } return ; } if ( failRatio > 0 ) { debug ( SendResultFormatError ( results , `Some of the record updates for '${ service . getFQDN ( ) }' failed` ) ) ; } if ( callback ) { callback ( ) ; } } ) ; } private handleServiceRecordUpdateOnInterface ( service , name , records , callback ? ) { if ( service . serviceState !== ServiceState . ANNOUNCED ) { throw new Error ( "Cannot update txt of service which is not announced yet. Received " + service . serviceState + " for service " + service . getFQDN ( ) ) ; } debug ( "[%s] Updating %d record(s) for given service on interface %s!" , service . getFQDN ( ) , records . length , name ) ; const packet = DNSPacket . createDNSResponsePacketsFromRRSet ( { answers : records } ) ; this . server . sendResponse ( packet , name , callback ) ; } private goodbye ( service ) { assert ( service . currentAnnouncer === undefined , "Service " + service . getFQDN ( ) + " is already announcing!" ) ; service . serviceState = ServiceState . ANNOUNCING ; const announcer = new Announcer ( this . server , service , { repetitions : 1 , goodbye : true , } ) ; service . currentAnnouncer = announcer ; return announcer . announce ( ) . then ( ( ) => { service . serviceState = ServiceState . UNANNOUNCED ; service . currentAnnouncer = undefined ; } , reason => { service . serviceState = ServiceState . UNANNOUNCED ; service . currentAnnouncer = undefined ; return Promise . reject ( reason ) ; } ) ; } private handleNetworkUpdate ( change ) { for ( const service of this . announcedServices . values ( ) ) { service . handleNetworkInterfaceUpdate ( change ) ; } } private handleQuery ( packet , endpoint ) { const start = new Date ( ) . getTime ( ) ; const endpointId = endpoint . address + ":" + endpoint . port + ":" + endpoint . interface ; const previousQuery = this . truncatedQueries [ endpointId ] ; if ( previousQuery ) { const truncatedQueryResult = previousQuery . appendDNSPacket ( packet ) ; switch ( truncatedQueryResult ) { case TruncatedQueryResult . ABORT : delete this . truncatedQueries [ endpointId ] ; debug ( "[%s] Aborting to wait for more truncated queries. Waited a total of %d ms receiving %d queries" , endpointId , previousQuery . getTotalWaitTime ( ) , previousQuery . getArrivedPacketCount ( ) ) ; return ; case TruncatedQueryResult . AGAIN_TRUNCATED : debug ( "[%s] Received a query marked as truncated, waiting for more to arrive" , endpointId ) ; return ; case TruncatedQueryResult . FINISHED : delete this . truncatedQueries [ endpointId ] ; packet = previousQuery . getPacket ( ) ; debug ( "[%s] Last part of the truncated query arrived. Received %d packets taking a total of %d ms" , endpointId , previousQuery . getArrivedPacketCount ( ) , previousQuery . getTotalWaitTime ( ) ) ; break ; } } else if ( packet . flags . truncation ) { debug ( "Received truncated query from " + JSON . stringify ( endpoint ) + " waiting for more to come!" ) ; const truncatedQuery = new TruncatedQuery ( packet ) ; this . truncatedQueries [ endpointId ] = truncatedQuery ; truncatedQuery . on ( TruncatedQueryEvent . TIMEOUT , ( ) => { debug ( "[%s] Timeout passed since the last truncated query was received. Discarding %d packets received in %d ms." , endpointId , truncatedQuery . getArrivedPacketCount ( ) , truncatedQuery . getTotalWaitTime ( ) ) ; delete this . truncatedQueries [ endpointId ] ; } ) ; return ; } const isUnicastQuerier = endpoint . port !== MDNSServer . MDNS_PORT ; const isProbeQuery = packet . authorities . size > 0 ; let udpPayloadSize = undefined ; for ( const record of packet . additionals . values ( ) ) { if ( record . type === RType . OPT ) { udpPayloadSize = ( record as OPTRecord ) . udpPayloadSize ; break ; } } const multicastResponses = [ ] ; const unicastResponses = [ ] ; packet . questions . forEach ( question => { const responses = this . answerQuestion ( question , endpoint , packet . answers ) ; if ( isUnicastQuerier || question . unicastResponseFlag && ! this . ignoreUnicastResponseFlag ) { unicastResponses . push ( ... responses ) ; } else { multicastResponses . push ( ... responses ) ; } } ) ; if ( this . currentProber ) { this . currentProber . handleQuery ( packet , endpoint ) ; } if ( isUnicastQuerier ) { for ( let i = 0 ; i < unicastResponses . length ; i ++ ) { const response = unicastResponses [ i ] ; response . markLegacyUnicastResponse ( packet . id , i === 0 ? Array . from ( packet . questions . values ( ) ) : undefined ) ; } } QueryResponse . combineResponses ( multicastResponses , udpPayloadSize ) ; QueryResponse . combineResponses ( unicastResponses , udpPayloadSize ) ; if ( isUnicastQuerier && unicastResponses . length > 1 ) { unicastResponses . splice ( 1 , unicastResponses . length - 1 ) ; unicastResponses [ 0 ] . markTruncated ( ) ; } for ( const unicastResponse of unicastResponses ) { if ( ! unicastResponse . hasAnswers ( ) ) { continue ; } this . server . sendResponse ( unicastResponse . asPacket ( ) , endpoint ) ; const time = new Date ( ) . getTime ( ) - start ; debug ( "Sending response via unicast to %s (took %d ms): %s" , JSON . stringify ( endpoint ) , time , unicastResponse . asString ( udpPayloadSize ) ) ; } for ( const multicastResponse of multicastResponses ) { if ( ! multicastResponse . hasAnswers ( ) ) { continue ; } if ( ( multicastResponse . containsSharedAnswer ( ) || packet . questions . size > 1 ) && ! isProbeQuery ) { const time = new Date ( ) . getTime ( ) - start ; this . enqueueDelayedMulticastResponse ( multicastResponse . asPacket ( ) , endpoint . interface , time ) ; } else { let sentWithLaterPacket = false ; for ( let i = 0 ; i < this . delayedMulticastResponses . length ; i ++ ) { const delayedResponse = this . delayedMulticastResponses [ i ] ; if ( delayedResponse . getTimeTillSent ( ) > QueuedResponse . MAX_DELAY ) { break ; } if ( delayedResponse . combineWithUniqueResponseIfPossible ( multicastResponse , endpoint . interface ) ) { const time = new Date ( ) . getTime ( ) - start ; sentWithLaterPacket = true ; debug ( "Multicast response on interface %s containing unique records (took %d ms) was combined with response which is sent out later" , endpoint . interface , time ) ; break ; } } if ( ! sentWithLaterPacket ) { this . server . sendResponse ( multicastResponse . asPacket ( ) , endpoint . interface ) ; const time = new Date ( ) . getTime ( ) - start ; debug ( "Sending response via multicast on network %s (took %d ms): %s" , endpoint . interface , time , multicastResponse . asString ( udpPayloadSize ) ) ; } } } } private handleResponse ( packet , endpoint ) { if ( this . currentProber ) { this . currentProber . handleResponse ( packet , endpoint ) ; } for ( const service of this . announcedServices . values ( ) ) { let conflictingRData = false ; let ttlConflicts = 0 ; for ( const record of packet . answers . values ( ) ) { const type = Responder . checkRecordConflictType ( service , record , endpoint ) ; if ( type === ConflictType . CONFLICTING_RDATA ) { conflictingRData = true ; break ; } else if ( type === ConflictType . CONFLICTING_TTL ) { ttlConflicts ++ ; } } if ( ! conflictingRData ) { for ( const record of packet . additionals . values ( ) ) { const type = Responder . checkRecordConflictType ( service , record , endpoint ) ; if ( type === ConflictType . CONFLICTING_RDATA ) { conflictingRData = true ; break ; } else if ( type === ConflictType . CONFLICTING_TTL ) { ttlConflicts ++ ; } } } if ( conflictingRData ) { this . republishService ( service , error => { if ( error ) { console . log ( `FATAL Error occurred trying to resolve conflict for service ${ service . getFQDN ( ) }! We can't recover from this!` ) ; console . log ( error . stack ) ; process . exit ( 1 ) ; } } , true ) ; } else if ( ttlConflicts && ! service . currentAnnouncer ) { service . serviceState = ServiceState . ANNOUNCING ; const announcer = new Announcer ( this . server , service , { repetitions : 1 , } ) ; service . currentAnnouncer = announcer ; announcer . announce ( ) . then ( ( ) => { service . currentAnnouncer = undefined ; service . serviceState = ServiceState . ANNOUNCED ; } , reason => { service . currentAnnouncer = undefined ; service . serviceState = ServiceState . ANNOUNCED ; if ( reason === Announcer . CANCEL_REASON ) { return ; } console . warn ( "When trying to resolve a ttl conflict on the network, we were unable to send our response packet: " + reason . message ) ; } ) ; } } } private static checkRecordConflictType ( service , record , endpoint ) { if ( ! service . advertisesOnInterface ( endpoint . interface ) ) { return ConflictType . NO_CONFLICT ; } const recordName = record . getLowerCasedName ( ) ; if ( recordName === service . getLowerCasedFQDN ( ) ) { if ( record . type === RType . SRV ) { const srvRecord = record as SRVRecord ; if ( srvRecord . getLowerCasedHostname ( ) !== service . getLowerCasedHostname ( ) ) { debug ( "[%s] Noticed conflicting record on the network. SRV with hostname: %s" , service . getFQDN ( ) , srvRecord . hostname ) ; return ConflictType . CONFLICTING_RDATA ; } else if ( srvRecord . port !== service . getPort ( ) ) { debug ( "[%s] Noticed conflicting record on the network. SRV with port: %s" , service . getFQDN ( ) , srvRecord . port ) ; return ConflictType . CONFLICTING_RDATA ; } if ( srvRecord . ttl < SRVRecord . DEFAULT_TTL / 2 ) { return ConflictType . CONFLICTING_TTL ; } } else if ( record . type === RType . TXT ) { const txtRecord = record as TXTRecord ; const txt = service . getTXT ( ) ; if ( txt . length !== txtRecord . txt . length ) { debug ( "[%s] Noticed conflicting record on the network. TXT with differing data length" , service . getFQDN ( ) ) ; return ConflictType . CONFLICTING_RDATA ; } for ( let i = 0 ; i < txt . length ; i ++ ) { const buffer0 = txt [ i ] ; const buffer1 = txtRecord . txt [ i ] ; if ( buffer0 . length !== buffer1 . length || buffer0 . toString ( "hex" ) !== buffer1 . toString ( "hex" ) ) { debug ( "[%s] Noticed conflicting record on the network. TXT with differing data." , service . getFQDN ( ) ) ; return ConflictType . CONFLICTING_RDATA ; } } if ( txtRecord . ttl < TXTRecord . DEFAULT_TTL / 2 ) { return ConflictType . CONFLICTING_TTL ; } } } else if ( recordName === service . getLowerCasedHostname ( ) ) { if ( record . type === RType . A ) { const aRecord = record as ARecord ; if ( ! service . hasAddress ( aRecord . ipAddress ) ) { debug ( "[%s] Noticed conflicting record on the network. A with ip address: %s" , service . getFQDN ( ) , aRecord . ipAddress ) ; return ConflictType . CONFLICTING_RDATA ; } if ( aRecord . ttl < ARecord . DEFAULT_TTL / 2 ) { return ConflictType . CONFLICTING_TTL ; } } else if ( record . type === RType . AAAA ) { const aaaaRecord = record as AAAARecord ; if ( ! service . hasAddress ( aaaaRecord . ipAddress ) ) { debug ( "[%s] Noticed conflicting record on the network. AAAA with ip address: %s" , service . getFQDN ( ) , aaaaRecord . ipAddress ) ; return ConflictType . CONFLICTING_RDATA ; } if ( aaaaRecord . ttl < AAAARecord . DEFAULT_TTL / 2 ) { return ConflictType . CONFLICTING_TTL ; } } } else if ( record . type === RType . PTR ) { const ptrRecord = record as PTRRecord ; if ( recordName === service . getLowerCasedTypePTR ( ) ) { if ( ptrRecord . getLowerCasedPTRName ( ) === service . getLowerCasedFQDN ( ) && ptrRecord . ttl < PTRRecord . DEFAULT_TTL / 2 ) { return ConflictType . CONFLICTING_TTL ; } } else if ( recordName === Responder . SERVICE_TYPE_ENUMERATION_NAME ) { } else { const subTypes = service . getLowerCasedSubtypePTRs ( ) ; if ( subTypes && subTypes . includes ( recordName ) && ptrRecord . getLowerCasedPTRName ( ) === service . getLowerCasedFQDN ( ) && ptrRecord . ttl < PTRRecord . DEFAULT_TTL / 2 ) { return ConflictType . CONFLICTING_TTL ; } } } return ConflictType . NO_CONFLICT ; } private enqueueDelayedMulticastResponse ( packet , interfaceName , time ) { const response = new QueuedResponse ( packet , interfaceName ) ; response . calculateRandomDelay ( ) ; sortedInsert ( this . delayedMulticastResponses , response , queuedResponseComparator ) ; for ( let i = 0 ; i < this . delayedMulticastResponses . length ; i ++ ) { const response0 = this . delayedMulticastResponses [ i ] ; for ( let j = i + 1 ; j < this . delayedMulticastResponses . length ; j ++ ) { const response1 = this . delayedMulticastResponses [ j ] ; if ( ! response0 . delayWouldBeInTimelyManner ( response1 ) ) { break ; } if ( response0 . combineWithNextPacketIfPossible ( response1 ) ) { const index = this . delayedMulticastResponses . indexOf ( response0 ) ; if ( index !== - 1 ) { this . delayedMulticastResponses . splice ( index , 1 ) ; } i -- ; break ; } } } if ( ! response . delayed ) { response . scheduleResponse ( ( ) => { const index = this . delayedMulticastResponses . indexOf ( response ) ; if ( index !== - 1 ) { this . delayedMulticastResponses . splice ( index , 1 ) ; } try { this . server . sendResponse ( response . getPacket ( ) , interfaceName ) ; debug ( "Sending (delayed %dms) response via multicast on network interface %s (took %d ms): %s" , Math . round ( response . getTimeSinceCreation ( ) ) , interfaceName , time , response . getPacket ( ) . asLoggingString ( ) ) ; } catch ( error ) { if ( error . name === ERR_INTERFACE_NOT_FOUND ) { debug ( "Multicast response (delayed %dms) was cancelled as the network interface %s is no longer available!" , Math . round ( response . getTimeSinceCreation ( ) ) , interfaceName ) ; } else if ( error . name === ERR_SERVER_CLOSED ) { debug ( "Multicast response (delayed %dms) was cancelled as the server is about to be shutdown!" , Math . round ( response . getTimeSinceCreation ( ) ) ) ; } else { throw error ; } } } ) ; } } private answerQuestion ( question , endpoint , knownAnswers ? ) { if ( question . class !== QClass . IN && question . class !== QClass . ANY ) { return [ ] ; } const serviceResponses = [ ] ; let metaQueryResponse = undefined ; if ( question . type === QType . PTR || question . type === QType . ANY || question . type === QType . CNAME ) { const destinations = this . servicePointer . get ( question . getLowerCasedName ( ) ) ; if ( destinations ) { for ( const data of destinations ) { const service = this . announcedServices . get ( data ) ; if ( service ) { if ( service . advertisesOnInterface ( endpoint . interface ) ) { const response = Responder . answerServiceQuestion ( service , question , endpoint , knownAnswers ) ; if ( response . hasAnswers ( ) ) { serviceResponses . push ( response ) ; } } } else { if ( ! metaQueryResponse ) { metaQueryResponse = new QueryResponse ( knownAnswers ) ; serviceResponses . unshift ( metaQueryResponse ) ; } metaQueryResponse . addAnswer ( new PTRRecord ( question . name , data ) ) ; } } return serviceResponses ; } } for ( const service of this . announcedServices . values ( ) ) { if ( ! service . advertisesOnInterface ( endpoint . interface ) ) { continue ; } const response = Responder . answerServiceQuestion ( service , question , endpoint , knownAnswers ) ; if ( response . hasAnswers ( ) ) { serviceResponses . push ( response ) ; } } return serviceResponses ; } private static answerServiceQuestion ( service , question , endpoint , knownAnswers ? ) { const response = new QueryResponse ( knownAnswers ) ; const loweredQuestionName = question . getLowerCasedName ( ) ; const askingAny = question . type === QType . ANY || question . type === QType . CNAME ; const addAnswer = response . addAnswer . bind ( response ) ; const addAdditional = response . addAdditional . bind ( response ) ; if ( loweredQuestionName === service . getLowerCasedTypePTR ( ) ) { if ( askingAny || question . type === QType . PTR ) { const added = response . addAnswer ( service . ptrRecord ( ) ) ; if ( added ) { response . addAdditional ( service . txtRecord ( ) , service . srvRecord ( ) ) ; this . addAddressRecords ( service , endpoint , RType . A , addAdditional ) ; this . addAddressRecords ( service , endpoint , RType . AAAA , addAdditional ) ; response . addAdditional ( service . serviceNSECRecord ( ) , service . addressNSECRecord ( ) ) ; } } } else if ( loweredQuestionName === service . getLowerCasedFQDN ( ) ) { if ( askingAny ) { response . addAnswer ( service . txtRecord ( ) ) ; const addedSrv = response . addAnswer ( service . srvRecord ( ) ) ; if ( addedSrv ) { this . addAddressRecords ( service , endpoint , RType . A , addAdditional ) ; this . addAddressRecords ( service , endpoint , RType . AAAA , addAdditional ) ; response . addAdditional ( service . serviceNSECRecord ( ) , service . addressNSECRecord ( ) ) ; } } else if ( question . type === QType . SRV ) { const added = response . addAnswer ( service . srvRecord ( ) ) ; if ( added ) { this . addAddressRecords ( service , endpoint , RType . A , addAdditional ) ; this . addAddressRecords ( service , endpoint , RType . AAAA , addAdditional ) ; response . addAdditional ( service . serviceNSECRecord ( true ) , service . addressNSECRecord ( ) ) ; } } else if ( question . type === QType . TXT ) { response . addAnswer ( service . txtRecord ( ) ) ; response . addAdditional ( service . serviceNSECRecord ( ) ) ; } } else if ( loweredQuestionName === service . getLowerCasedHostname ( ) || loweredQuestionName + "local." === service . getLowerCasedHostname ( ) ) { if ( askingAny ) { this . addAddressRecords ( service , endpoint , RType . A , addAnswer ) ; this . addAddressRecords ( service , endpoint , RType . AAAA , addAnswer ) ; response . addAdditional ( service . addressNSECRecord ( ) ) ; } else if ( question . type === QType . A ) { const added = this . addAddressRecords ( service , endpoint , RType . A , addAnswer ) ; if ( added ) { this . addAddressRecords ( service , endpoint , RType . AAAA , addAdditional ) ; } response . addAdditional ( service . addressNSECRecord ( ) ) ; } else if ( question . type === QType . AAAA ) { const added = this . addAddressRecords ( service , endpoint , RType . AAAA , addAnswer ) ; if ( added ) { this . addAddressRecords ( service , endpoint , RType . A , addAdditional ) ; } response . addAdditional ( service . addressNSECRecord ( ) ) ; } } else if ( service . getLowerCasedSubtypePTRs ( ) ) { if ( askingAny || question . type === QType . PTR ) { const dnsLowerSubTypes = service . getLowerCasedSubtypePTRs ( ) ! ; const index = dnsLowerSubTypes . indexOf ( loweredQuestionName ) ; if ( index !== - 1 ) { const records = service . subtypePtrRecords ( ) ; const record = records ! [ index ] ; assert ( loweredQuestionName === record . name , "Question Name didn't match selected sub type ptr record!" ) ; const added = response . addAnswer ( record ) ; if ( added ) { response . addAdditional ( service . txtRecord ( ) , service . srvRecord ( ) ) ; this . addAddressRecords ( service , endpoint , RType . A , addAdditional ) ; this . addAddressRecords ( service , endpoint , RType . AAAA , addAdditional ) ; response . addAdditional ( service . serviceNSECRecord ( ) , service . addressNSECRecord ( ) ) ; } } } } return response ; } /**
     * This method is a helper method to reduce the complexity inside  answerServiceQuestion .
     * The method calculates which A and AAAA records to be added for a given {@code endpoint} using
     * the records from the provided {@code service}.
     * It will add the records by calling the provided {@code dest} method.
     *
     *  param { CiaoService } service param { EndpointInfo } endpoint param { RType . A | RType . AAAA } type param { RecordAddMethod } dest returns private static addAddressRecords ( service , endpoint , type , dest ) { if ( type === RType . A ) { const record = service . aRecord ( endpoint . interface ) ; return record ? dest ( record ) : false ; } else if ( type === RType . AAAA ) { const record = service . aaaaRecord ( endpoint . interface ) ; const routableRecord = service . aaaaRoutableRecord ( endpoint . interface ) ; const ulaRecord = service . aaaaUniqueLocalRecord ( endpoint . interface ) ; let addedAny = false ; if ( record ) { addedAny = dest ( record ) ; } if ( routableRecord ) { const added = dest ( routableRecord ) ; addedAny = addedAny || added ; } if ( ulaRecord ) { const added = dest ( ulaRecord ) ; addedAny = addedAny || added ; } return addedAny ; } else { assert . fail ( "Illegal argument!" ) ; } } }