import assert from 'assert' ; import { PeerInfo } from '../dpt' ; import { ENR } from './enr' ; import { debug as createDebugLogger } from 'debug' ; let dns ; try { dns = require ( 'dns' ) ; } catch ( e ) { dns = require ( '../browser/dns' ) ; } const debug = createDebugLogger ( 'devp2p:dns:dns' ) ; type SearchContext = { domain : string ; publicKey : string ; visits : { [ key ] : boolean ; } ; } ; export type DNSOptions = { type { string } dnsServerAddress ? : string ; } ; export class DNS { private _DNSTreeCache : { [ key ] : string ; } ; private readonly _errorTolerance : number = 10 ; constructor ( options = { } ) { this . _DNSTreeCache = { } ; if ( options . dnsServerAddress ) { dns . setServers ( [ options . dnsServerAddress ] ) ; } } param { number } maxQuantity param { string } treeEntry return { PeerInfo } async getPeers ( maxQuantity , dnsNetworks ) { let totalSearches = 0 ; const peers = [ ] ; const networkIndex = Math . floor ( Math . random ( ) * dnsNetworks . length ) ; const { publicKey , domain } = ENR . parseTree ( dnsNetworks [ networkIndex ] ) ; while ( peers . length < maxQuantity && totalSearches < maxQuantity + this . _errorTolerance ) { const context = { domain , publicKey , visits : { } , } ; const peer = await this . _search ( domain , context ) ; if ( this . _isNewPeer ( peer , peers ) ) { peers . push ( peer as PeerInfo ) ; debug ( `got new peer candidate from DNS address=${ peer ! . address }` ) ; } totalSearches ++ ; } return peers ; } param { string } subdomain param { SearchContext } context return { PeerInfo | null } private async _search ( subdomain , context ) { const entry = await this . _getTXTRecord ( subdomain , context ) ; context . visits [ subdomain ] = true ; let next ; let branches ; try { switch ( this . _getEntryType ( entry ) ) { case ENR . ROOT_PREFIX : next = ENR . parseAndVerifyRoot ( entry , context . publicKey ) ; return await this . _search ( next , context ) ; case ENR . BRANCH_PREFIX : branches = ENR . parseBranch ( entry ) ; next = this . _selectRandomPath ( branches , context ) ; return await this . _search ( next , context ) ; case ENR . RECORD_PREFIX : return ENR . parseAndVerifyRecord ( entry ) ; default : return null ; } } catch ( error ) { debug ( `Errored searching DNS tree at subdomain ${ subdomain }: ${ error }` ) ; return null ; } } private _getEntryType ( entry ) { if ( entry . startsWith ( ENR . ROOT_PREFIX ) ) return ENR . ROOT_PREFIX ; if ( entry . startsWith ( ENR . BRANCH_PREFIX ) ) return ENR . BRANCH_PREFIX ; if ( entry . startsWith ( ENR . RECORD_PREFIX ) ) return ENR . RECORD_PREFIX ; return '' ; } param { string [ ] } branches param { SearchContext } context return { String } private _selectRandomPath ( branches , context ) { const circularRefs = { } ; for ( const [ idx , subdomain ] of branches . entries ( ) ) { if ( context . visits [ subdomain ] ) { circularRefs [ idx ] = true ; } } if ( Object . keys ( circularRefs ) . length === branches . length ) { throw new Error ( 'Unresolvable circular path detected' ) ; } let index ; do { index = Math . floor ( Math . random ( ) * branches . length ) ; } while ( circularRefs [ index ] ) ; return branches [ index ] ; } param { string } subdomain param { SearchContext = return { string } private async _getTXTRecord ( subdomain , context ) { if ( this . _DNSTreeCache [ subdomain ] ) { return this . _DNSTreeCache [ subdomain ] ; } const location = subdomain !== context . domain ? `${ subdomain }.${ context . domain }` : context . domain ; const response = await dns . promises . resolve ( location , 'TXT' ) ; assert ( response . length , 'Received empty result array while fetching TXT record' ) ; assert ( response [ 0 ] . length , 'Received empty TXT record' ) ; const result = response [ 0 ] . length > 1 ? response [ 0 ] . join ( '' ) : response [ 0 ] [ 0 ] ; this . _DNSTreeCache [ subdomain ] = result ; return result ; } param { PeerInfo } peer param { PeerInfo [ ] } peers return { boolean } private _isNewPeer ( peer , peers ) { if ( ! peer || ! peer ! . address ) return false ; for ( const existingPeer of peers ) { if ( peer . address === existingPeer . address ) { return false ; } } return true ; } param { any } mock __setNativeDNSModuleResolve ( mock ) { dns . promises . resolve = mock . resolve ; } }