import assert from "assert" ; import createDebug from "debug" ; import { CiaoService , ServiceState } from "../CiaoService" ; import { DNSPacket , QType } from "../coder/DNSPacket" ; import { Question } from "../coder/Question" ; import { ResourceRecord } from "../coder/ResourceRecord" ; import { EndpointInfo , MDNSServer , SendResultFailedRatio , SendResultFormatError } from "../MDNSServer" ; import { Responder } from "../Responder" ; import * as tiebreaking from "../util/tiebreaking" ; import { rrComparator , TiebreakingResult } from "../util/tiebreaking" ; import Timeout = NodeJS . Timeout ; const PROBE_INTERVAL = 250 ; const LIMITED_PROBE_INTERVAL = 1000 ; const debug = createDebug ( "ciao:Prober" ) ; /**
 * This class is used to execute the probing process for a given service as defined
 * in RFC 6762 8.1.
 * This ensure that the 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.
 */ export class Prober { public static readonly CANCEL_REASON = "CIAO PROBING CANCELLED" ; private readonly responder : Responder ; private readonly server : MDNSServer ; private readonly service : CiaoService ; private records : ResourceRecord [ ] = [ ] ; private timer ? : Timeout ; private currentInterval : number = PROBE_INTERVAL ; private promiseResolve ? : ( value ? : void | PromiseLike < void > ) => void ; private promiseReject ? : ( reason ? : any ) => void ; private serviceEncounteredNameChange = false ; private sentFirstProbeQuery = false ; private sentQueriesForCurrentTry = 0 ; private sentQueries = 0 ; constructor ( responder , server , service ) { assert ( responder , "responder must be defined" ) ; assert ( server , "server must be defined" ) ; assert ( service , "service must be defined" ) ; this . responder = responder ; this . server = server ; this . service = service ; } public getService ( ) { return this . service ; } returns public probe ( ) { 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 ( ) ; } ) ; } public cancel ( ) { this . clear ( ) ; this . promiseReject ! ( Prober . CANCEL_REASON ) ; } private clear ( ) { if ( this . timer ) { clearTimeout ( this . timer ) ; this . timer = undefined ; } this . sentFirstProbeQuery = false ; this . sentQueriesForCurrentTry = 0 ; } param success private endProbing ( success ) { this . clear ( ) ; if ( success ) { debug ( "Probing for '%s' finished successfully" , this . service . getFQDN ( ) ) ; this . promiseResolve ! ( ) ; if ( this . serviceEncounteredNameChange ) { this . service . informAboutNameUpdates ( ) ; } } } private sendProbeRequest ( ) { if ( this . sentQueriesForCurrentTry === 0 ) { this . records = [ this . service . srvRecord ( ) , this . service . txtRecord ( ) , this . service . ptrRecord ( ) , ... this . service . subtypePtrRecords ( ) , ... this . service . allAddressRecords ( ) , ] . sort ( rrComparator ) ; this . records . forEach ( record => record . flushFlag = false ) ; } if ( this . sentQueriesForCurrentTry >= 3 ) { 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 ( ) ) ; assert ( this . records . length > 0 , "Tried sending probing request for zero record length!" ) ; const questions = [ new Question ( this . service . getFQDN ( ) , QType . ANY , true ) , new Question ( this . service . getHostname ( ) , QType . ANY , true ) , ] ; this . server . sendQueryBroadcast ( { questions : questions , authorities : this . records , } , this . service ) . then ( results => { const failRatio = SendResultFailedRatio ( results ) ; if ( failRatio === 1 ) { console . error ( SendResultFormatError ( results , `Failed to send probe queries for '${ this . service . getFQDN ( ) }'` ) , true ) ; this . endProbing ( false ) ; this . promiseReject ! ( new Error ( "Probing failed as of socket errors!" ) ) ; return ; } if ( failRatio > 0 ) { debug ( SendResultFormatError ( results , `Some of the probe queries for '${ this . service . getFQDN ( ) }' encountered an error` ) ) ; } if ( this . service . serviceState !== 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 ( ) ; } ) ; } private 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 ; 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 ) { debug ( "Probing for '%s' failed. Doing a name change" , this . service . getFQDN ( ) ) ; this . handleNameChange ( ) ; } } private handleNameChange ( ) { this . endProbing ( false ) ; this . service . serviceState = ServiceState . UNANNOUNCED ; this . service . incrementName ( ) ; this . service . serviceState = 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 ; } 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 ) ; } } private doTiebreaking ( packet ) { if ( ! this . sentFirstProbeQuery ) { return ; } 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 ; } const answers = this . records ; const opponent = Array . from ( packet . authorities . values ( ) ) . sort ( tiebreaking . rrComparator ) ; const result = tiebreaking . runTiebreaking ( answers , opponent ) ; if ( result === TiebreakingResult . HOST ) { debug ( "'%s' won the tiebreak. We gonna ignore the other probing request!" , this . service . getFQDN ( ) ) ; } else if ( result === TiebreakingResult . OPPONENT ) { debug ( "'%s' lost the tiebreak. We are waiting a second and try to probe again..." , this . service . getFQDN ( ) ) ; this . endProbing ( false ) ; this . timer = setTimeout ( this . sendProbeRequest . bind ( this ) , 1000 ) ; this . timer . unref ( ) ; } else { } } }