2015-07-28 03:53:44 +02:00
< ? php
namespace Amp\Dns ;
2015-09-10 17:11:48 +02:00
use Amp\Cache\ArrayCache ;
use Amp\CoroutineResult ;
use Amp\Deferred ;
use Amp\Failure ;
use Amp\Success ;
2015-09-18 02:49:06 +02:00
use LibDNS\Decoder\DecoderFactory ;
use LibDNS\Encoder\EncoderFactory ;
use LibDNS\Messages\MessageFactory ;
use LibDNS\Messages\MessageTypes ;
use LibDNS\Records\QuestionFactory ;
2015-08-02 04:18:44 +02:00
/**
2016-02-25 12:36:51 +01:00
* Resolve a hostname name to an IP address
* [ hostname as defined by RFC 3986 ]
2015-08-02 04:18:44 +02:00
*
* Upon success the returned promise resolves to an indexed array of the form :
*
2015-09-08 23:03:25 +02:00
* [ string $recordValue , int $type , int $ttl ]
2015-08-02 04:18:44 +02:00
*
* A null $ttl value indicates the DNS name was resolved from the cache or the
* local hosts file .
2015-08-30 16:39:04 +02:00
* $type being one constant from Amp\Dns\Record
2015-08-02 04:18:44 +02:00
*
* Options :
*
2015-09-08 23:03:25 +02:00
* - " server " | string Custom DNS server address in ip or ip : port format ( Default : 8.8 . 8.8 : 53 )
* - " timeout " | int DNS server query timeout ( Default : 3000 ms )
* - " hosts " | bool Use the hosts file ( Default : true )
2015-08-30 16:39:04 +02:00
* - " reload_hosts " | bool Reload the hosts file ( Default : false ), only active when no_hosts not true
2015-09-08 23:03:25 +02:00
* - " cache " | bool Use local DNS cache when querying ( Default : true )
2015-08-30 16:39:04 +02:00
* - " types " | array Default : [ Record :: A , Record :: AAAA ] ( only for resolve ())
2015-09-18 03:37:10 +02:00
* - " recurse " | bool Check for DNAME and CNAME records ( always active for resolve (), Default : false for query ())
2015-08-02 04:18:44 +02:00
*
* If the custom per - request " server " option is not present the resolver will
2015-09-18 15:09:28 +02:00
* use the first nameserver in / etc / resolv . conf or default to Google ' s public
* DNS servers on Windows or if / etc / resolv . conf is not found .
2015-08-02 04:18:44 +02:00
*
* @ param string $name The hostname to resolve
* @ param array $options
* @ return \Amp\Promise
* @ TODO add boolean " clear_cache " option flag
*/
function resolve ( $name , array $options = []) {
2015-08-30 16:39:04 +02:00
if ( ! $inAddr = @ \inet_pton ( $name )) {
if ( __isValidHostName ( $name )) {
$types = empty ( $options [ " types " ]) ? [ Record :: A , Record :: AAAA ] : $options [ " types " ];
return __pipeResult ( __recurseWithHosts ( $name , $types , $options ), $types );
} else {
2015-09-10 17:11:48 +02:00
return new Failure ( new ResolutionException ( " Cannot resolve; invalid host name " ));
2015-08-30 16:39:04 +02:00
}
2015-08-02 04:18:44 +02:00
} else {
2015-09-10 17:11:48 +02:00
return new Success ([[ $name , isset ( $inAddr [ 4 ]) ? Record :: AAAA : Record :: A , $ttl = null ]]);
2015-08-30 16:39:04 +02:00
}
}
2015-09-18 15:09:28 +02:00
/**
* Query specific DNS records .
*
2016-02-25 12:36:51 +01:00
* @ param string $name Unlike resolve (), query () allows for requesting _any_ name ( as DNS RFC allows for arbitrary strings )
2015-09-18 15:09:28 +02:00
* @ param int | int [] $type Use constants of Amp\Dns\Record
2016-02-25 12:36:51 +01:00
* @ param array $options @ see resolve documentation
2015-09-18 15:09:28 +02:00
* @ return \Amp\Promise
*/
2015-08-30 16:39:04 +02:00
function query ( $name , $type , array $options = []) {
2016-02-25 12:36:51 +01:00
$handler = __NAMESPACE__ . " \\ " . ( empty ( $options [ " recurse " ]) ? " __doResolve " : " __doRecurse " );
$types = ( array ) $type ;
return __pipeResult ( \Amp\resolve ( $handler ( $name , $types , $options )), $types );
2015-08-02 04:18:44 +02:00
}
2015-07-28 03:53:44 +02:00
2015-08-02 04:18:44 +02:00
function __isValidHostName ( $name ) {
2015-09-10 17:11:48 +02:00
$pattern = " /^(?:[a-z0-9](?:[a-z0-9-] { 0,61}[a-z0-9]) { 0,1})(?: \\ .[a-z0-9][a-z0-9-] { 0,61}[a-z0-9])* $ /i " ;
2015-07-28 03:53:44 +02:00
2015-09-08 23:03:25 +02:00
return ! isset ( $name [ 253 ]) && \preg_match ( $pattern , $name );
2015-07-28 03:53:44 +02:00
}
2015-09-08 23:03:25 +02:00
// flatten $result while preserving order according to $types (append unspecified types for e.g. Record::ALL queries)
2015-08-30 16:39:04 +02:00
function __pipeResult ( $promise , array $types ) {
return \Amp\pipe ( $promise , function ( array $result ) use ( $types ) {
$retval = [];
foreach ( $types as $type ) {
if ( isset ( $result [ $type ])) {
$retval = \array_merge ( $retval , $result [ $type ]);
unset ( $result [ $type ]);
}
}
2015-09-08 23:03:25 +02:00
return $result ? \array_merge ( $retval , \call_user_func_array ( " array_merge " , $result )) : $retval ;
2015-08-30 16:39:04 +02:00
});
}
function __recurseWithHosts ( $name , array $types , $options ) {
// Check for hosts file matches
2015-09-08 23:03:25 +02:00
if ( ! isset ( $options [ " hosts " ]) || $options [ " hosts " ]) {
2015-08-30 16:39:04 +02:00
static $hosts = null ;
if ( $hosts === null || ! empty ( $options [ " reload_hosts " ])) {
2015-09-18 02:49:06 +02:00
return \Amp\pipe ( \Amp\resolve ( __loadHostsFile ()), function ( $value ) use ( & $hosts , $name , $types , $options ) {
2015-09-08 23:03:25 +02:00
unset ( $options [ " reload_hosts " ]); // avoid recursion
2015-08-30 16:39:04 +02:00
$hosts = $value ;
return __recurseWithHosts ( $name , $types , $options );
});
}
$result = [];
if ( in_array ( Record :: A , $types ) && isset ( $hosts [ Record :: A ][ $name ])) {
$result [ Record :: A ] = [[ $hosts [ Record :: A ][ $name ], Record :: A , $ttl = null ]];
}
if ( in_array ( Record :: AAAA , $types ) && isset ( $hosts [ Record :: AAAA ][ $name ])) {
$result [ Record :: AAAA ] = [[ $hosts [ Record :: AAAA ][ $name ], Record :: AAAA , $ttl = null ]];
}
if ( $result ) {
2015-09-10 17:11:48 +02:00
return new Success ( $result );
2015-08-30 16:39:04 +02:00
}
}
return \Amp\resolve ( __doRecurse ( $name , $types , $options ));
}
function __doRecurse ( $name , array $types , $options ) {
if ( array_intersect ( $types , [ Record :: CNAME , Record :: DNAME ])) {
throw new ResolutionException ( " Cannot use recursion for CNAME and DNAME records " );
}
$types = array_merge ( $types , [ Record :: CNAME , Record :: DNAME ]);
$lookupName = $name ;
for ( $i = 0 ; $i < 30 ; $i ++ ) {
$result = ( yield \Amp\resolve ( __doResolve ( $lookupName , $types , $options )));
if ( count ( $result ) > isset ( $result [ Record :: CNAME ]) + isset ( $result [ Record :: DNAME ])) {
unset ( $result [ Record :: CNAME ], $result [ Record :: DNAME ]);
2015-09-10 17:11:48 +02:00
yield new CoroutineResult ( $result );
2015-08-30 16:39:04 +02:00
return ;
}
// @TODO check for potentially using recursion and iterate over *all* CNAME/DNAME
// @FIXME check higher level for CNAME?
foreach ([ Record :: CNAME , Record :: DNAME ] as $type ) {
if ( isset ( $result [ $type ])) {
list ( $lookupName ) = $result [ $type ][ 0 ];
}
}
}
throw new ResolutionException ( " CNAME or DNAME chain too long (possible recursion?) " );
}
2015-08-30 19:59:49 +02:00
function __doRequest ( $state , $uri , $name , $type ) {
$server = __loadExistingServer ( $state , $uri ) ? : __loadNewServer ( $state , $uri );
// Get the next available request ID
do {
$requestId = $state -> requestIdCounter ++ ;
if ( $state -> requestIdCounter >= MAX_REQUEST_ID ) {
$state -> requestIdCounter = 1 ;
}
} while ( isset ( $state -> pendingRequests [ $requestId ]));
// Create question record
$question = $state -> questionFactory -> create ( $type );
$question -> setName ( $name );
// Create request message
$request = $state -> messageFactory -> create ( MessageTypes :: QUERY );
$request -> getQuestionRecords () -> add ( $question );
$request -> isRecursionDesired ( true );
$request -> setID ( $requestId );
// Encode request message
$requestPacket = $state -> encoder -> encode ( $request );
if ( substr ( $uri , 0 , 6 ) == " tcp:// " ) {
$requestPacket = pack ( " n " , strlen ( $requestPacket )) . $requestPacket ;
}
// Send request
$bytesWritten = \fwrite ( $server -> socket , $requestPacket );
if ( $bytesWritten === false || isset ( $packet [ $bytesWritten ])) {
throw new ResolutionException (
" Request send failed "
);
}
2015-09-10 17:11:48 +02:00
$promisor = new Deferred ;
2015-08-30 19:59:49 +02:00
$server -> pendingRequests [ $requestId ] = true ;
$state -> pendingRequests [ $requestId ] = [ $promisor , $name , $type , $uri ];
return $promisor -> promise ();
}
2015-08-30 16:39:04 +02:00
function __doResolve ( $name , array $types , $options ) {
2015-08-02 04:18:44 +02:00
static $state ;
$state = $state ? : ( yield \Amp\resolve ( __init ()));
2015-07-28 03:53:44 +02:00
2015-08-30 16:39:04 +02:00
if ( empty ( $types )) {
2015-09-10 17:11:48 +02:00
yield new CoroutineResult ([]);
2015-08-30 16:39:04 +02:00
return ;
}
2016-02-21 21:51:06 +01:00
assert ( array_reduce ( $types , function ( $result , $val ) { return $result && \is_int ( $val ); }, true ), 'The $types passed to DNS functions must all be integers (from \Amp\Dns\Record class)' );
2015-09-08 23:03:25 +02:00
2015-08-02 04:18:44 +02:00
$name = \strtolower ( $name );
2015-08-30 16:39:04 +02:00
$result = [];
2015-07-28 03:53:44 +02:00
2015-08-02 04:18:44 +02:00
// Check for cache hits
2015-09-08 23:03:25 +02:00
if ( ! isset ( $options [ " cache " ]) || $options [ " cache " ]) {
2015-08-30 16:39:04 +02:00
foreach ( $types as $k => $type ) {
$cacheKey = " $name # $type " ;
2015-09-10 17:11:48 +02:00
$cacheValue = ( yield $state -> arrayCache -> get ( $cacheKey ));
if ( $cacheValue !== null ) {
$result [ $type ] = $cacheValue ;
2015-08-30 16:39:04 +02:00
unset ( $types [ $k ]);
2015-09-10 17:11:48 +02:00
}
2015-07-28 03:53:44 +02:00
}
2015-08-30 16:39:04 +02:00
if ( empty ( $types )) {
2016-03-14 22:50:59 +01:00
if ( empty ( array_filter ( $result ))) {
throw new NoRecordException ( " No records returned for { $name } (cached result) " );
} else {
yield new CoroutineResult ( $result );
return ;
}
2015-07-28 03:53:44 +02:00
}
}
2015-09-18 02:49:06 +02:00
$timeout = empty ( $options [ " timeout " ]) ? $state -> config [ " timeout " ] : ( int ) $options [ " timeout " ];
if ( empty ( $options [ " server " ])) {
if ( empty ( $state -> config [ " nameservers " ])) {
throw new ResolutionException ( " No nameserver specified in system config " );
}
2015-07-28 03:53:44 +02:00
2015-09-18 02:49:06 +02:00
$uri = " udp:// " . $state -> config [ " nameservers " ][ 0 ];
} else {
$uri = __parseCustomServerUri ( $options [ " server " ]);
}
2015-07-28 03:53:44 +02:00
2015-08-30 16:39:04 +02:00
foreach ( $types as $type ) {
2015-08-30 19:59:49 +02:00
$promises [] = __doRequest ( $state , $uri , $name , $type );
2015-08-02 04:18:44 +02:00
}
2015-07-28 03:53:44 +02:00
2015-08-02 04:18:44 +02:00
try {
2015-09-18 03:37:10 +02:00
list ( , $resultArr ) = ( yield \Amp\timeout ( \Amp\some ( $promises ), $timeout ));
2015-08-30 16:39:04 +02:00
foreach ( $resultArr as $value ) {
$result += $value ;
}
2015-08-02 04:18:44 +02:00
} catch ( \Amp\TimeoutException $e ) {
2015-09-16 16:31:31 +02:00
if ( substr ( $uri , 0 , 6 ) == " tcp:// " ) {
throw new TimeoutException (
" Name resolution timed out for { $name } "
);
} else {
$options [ " server " ] = \preg_replace ( " #[a-z.]+://# " , " tcp:// " , $uri );
yield new CoroutineResult ( \Amp\resolve ( __doResolve ( $name , $types , $options )));
2015-12-23 16:47:49 +01:00
return ;
2015-09-16 16:31:31 +02:00
}
2015-09-08 23:03:25 +02:00
} catch ( ResolutionException $e ) {
2015-09-23 22:22:33 +02:00
if ( empty ( $result )) { // if we have no cached results
2015-09-08 23:03:25 +02:00
throw $e ;
}
2016-02-21 21:51:06 +01:00
} catch ( \Amp\CombinatorException $e ) { // if all promises in Amp\some fail
2015-09-23 22:22:33 +02:00
if ( empty ( $result )) { // if we have no cached results
2016-03-14 22:50:59 +01:00
foreach ( $e -> getExceptions () as $ex ) {
if ( $ex instanceof NoRecordException ) {
throw new NoRecordException ( " No records returned for { $name } " , 0 , $e );
}
}
2015-09-23 22:22:33 +02:00
throw new ResolutionException ( " All name resolution requests failed " , 0 , $e );
}
2015-08-02 04:18:44 +02:00
}
2015-09-08 23:03:25 +02:00
2015-09-10 17:11:48 +02:00
yield new CoroutineResult ( $result );
2015-08-02 04:18:44 +02:00
}
function __init () {
$state = new \StdClass ;
$state -> messageFactory = new MessageFactory ;
$state -> questionFactory = new QuestionFactory ;
$state -> encoder = ( new EncoderFactory ) -> create ();
$state -> decoder = ( new DecoderFactory ) -> create ();
2015-09-10 17:11:48 +02:00
$state -> arrayCache = new ArrayCache ;
2015-08-02 04:18:44 +02:00
$state -> requestIdCounter = 1 ;
$state -> pendingRequests = [];
$state -> serverIdMap = [];
$state -> serverUriMap = [];
$state -> serverIdTimeoutMap = [];
$state -> now = \time ();
$state -> serverTimeoutWatcher = \Amp\repeat ( function ( $watcherId ) use ( $state ) {
$state -> now = $now = \time ();
foreach ( $state -> serverIdTimeoutMap as $id => $expiry ) {
if ( $now > $expiry ) {
__unloadServer ( $state , $id );
}
2015-07-28 03:53:44 +02:00
}
2015-08-02 04:18:44 +02:00
if ( empty ( $state -> serverIdMap )) {
\Amp\disable ( $watcherId );
2015-07-28 03:53:44 +02:00
}
2015-08-02 04:18:44 +02:00
}, 1000 , $options = [
" enable " => true ,
" keep_alive " => false ,
]);
2015-09-18 02:49:06 +02:00
$state -> config = ( yield \Amp\resolve ( __loadResolvConf ()));
2015-09-10 17:11:48 +02:00
yield new CoroutineResult ( $state );
2015-08-02 04:18:44 +02:00
}
2015-09-18 03:37:10 +02:00
/** @link http://man7.org/linux/man-pages/man5/resolv.conf.5.html */
2015-09-18 02:49:06 +02:00
function __loadResolvConf ( $path = null ) {
2015-09-18 03:37:10 +02:00
$result = [
2015-09-18 02:49:06 +02:00
" nameservers " => [
2015-12-08 10:41:38 +01:00
" 8.8.8.8:53 " ,
" 8.8.4.4:53 " ,
2015-09-18 02:49:06 +02:00
],
" timeout " => 3000 ,
" attempts " => 2 ,
];
2015-09-18 03:37:10 +02:00
if ( \stripos ( PHP_OS , " win " ) !== 0 ) {
$path = $path ? : " /etc/resolv.conf " ;
2015-09-18 02:49:06 +02:00
2015-09-18 03:37:10 +02:00
try {
$lines = explode ( " \n " , ( yield \Amp\File\get ( $path )));
2015-09-18 02:49:06 +02:00
$result [ " nameservers " ] = [];
foreach ( $lines as $line ) {
2015-09-18 03:37:10 +02:00
$line = \preg_split ( '#\s+#' , $line , 2 );
2015-09-18 02:49:06 +02:00
if ( \count ( $line ) !== 2 ) {
continue ;
}
2015-09-18 03:37:10 +02:00
list ( $type , $value ) = $line ;
if ( $type === " nameserver " ) {
2015-09-18 02:49:06 +02:00
$line [ 1 ] = trim ( $line [ 1 ]);
$ip = @ \inet_pton ( $line [ 1 ]);
if ( $ip === false ) {
continue ;
}
2015-12-23 14:56:14 +01:00
if ( isset ( $ip [ 15 ])) {
$result [ " nameservers " ][] = " [ " . $line [ 1 ] . " ]:53 " ;
} else {
$result [ " nameservers " ][] = $line [ 1 ] . " :53 " ;
}
2015-09-18 03:37:10 +02:00
} elseif ( $type === " options " ) {
$optline = preg_split ( '#\s+#' , $value , 2 );
if ( \count ( $optline ) !== 2 ) {
2015-09-18 02:49:06 +02:00
continue ;
}
2015-09-18 15:09:28 +02:00
// TODO: Respect the contents of the attempts setting during resolution
2015-09-18 03:37:10 +02:00
list ( $option , $value ) = $optline ;
if ( in_array ( $option , [ " timeout " , " attempts " ])) {
$result [ $option ] = ( int ) $value ;
2015-09-18 02:49:06 +02:00
}
}
}
2016-02-21 21:51:06 +01:00
} catch ( \Amp\File\FilesystemException $e ) {}
2015-09-18 02:49:06 +02:00
}
2015-09-18 03:37:10 +02:00
yield new CoroutineResult ( $result );
2015-09-18 02:49:06 +02:00
}
2015-08-02 04:18:44 +02:00
function __loadHostsFile ( $path = null ) {
2015-08-30 16:39:04 +02:00
$data = [];
2015-08-02 04:18:44 +02:00
if ( empty ( $path )) {
2015-09-18 02:49:06 +02:00
$path = \stripos ( PHP_OS , " win " ) === 0
2015-09-18 03:37:10 +02:00
? 'C:\Windows\system32\drivers\etc\hosts'
: '/etc/hosts' ;
2015-08-02 04:18:44 +02:00
}
try {
2015-09-08 23:03:25 +02:00
$contents = ( yield \Amp\File\get ( $path ));
2015-08-02 15:19:32 +02:00
} catch ( \Exception $e ) {
2015-09-10 17:11:48 +02:00
yield new CoroutineResult ( $data );
2015-09-18 02:49:06 +02:00
2015-08-02 15:19:32 +02:00
return ;
}
$lines = \array_filter ( \array_map ( " trim " , \explode ( " \n " , $contents )));
foreach ( $lines as $line ) {
if ( $line [ 0 ] === " # " ) {
continue ;
}
$parts = \preg_split ( '/\s+/' , $line );
if ( ! ( $ip = @ \inet_pton ( $parts [ 0 ]))) {
continue ;
} elseif ( isset ( $ip [ 4 ])) {
2015-08-30 16:39:04 +02:00
$key = Record :: AAAA ;
2015-08-02 15:19:32 +02:00
} else {
2015-08-30 16:39:04 +02:00
$key = Record :: A ;
2015-08-02 15:19:32 +02:00
}
for ( $i = 1 , $l = \count ( $parts ); $i < $l ; $i ++ ) {
if ( __isValidHostName ( $parts [ $i ])) {
$data [ $key ][ strtolower ( $parts [ $i ])] = $parts [ 0 ];
2015-08-02 04:18:44 +02:00
}
2015-07-28 03:53:44 +02:00
}
}
2015-09-10 17:11:48 +02:00
yield new CoroutineResult ( $data );
2015-08-02 04:18:44 +02:00
}
2015-07-28 03:53:44 +02:00
2015-08-02 04:18:44 +02:00
function __parseCustomServerUri ( $uri ) {
if ( ! \is_string ( $uri )) {
throw new ResolutionException (
2015-09-18 02:49:06 +02:00
'Invalid server address ($uri must be a string IP address, ' . gettype ( $uri ) . " given) "
2015-08-02 04:18:44 +02:00
);
}
2016-02-21 21:51:06 +01:00
if ( strpos ( $uri , " :// " ) !== false ) {
2015-09-16 16:31:31 +02:00
return $uri ;
}
2015-08-02 04:18:44 +02:00
if (( $colonPos = strrpos ( " : " , $uri )) !== false ) {
$addr = \substr ( $uri , 0 , $colonPos );
$port = \substr ( $uri , $colonPos );
} else {
$addr = $uri ;
2015-09-18 02:49:06 +02:00
$port = 53 ;
2015-08-02 04:18:44 +02:00
}
$addr = trim ( $addr , " [] " );
if ( ! $inAddr = @ \inet_pton ( $addr )) {
throw new ResolutionException (
2015-09-08 23:03:25 +02:00
'Invalid server $uri; string IP address required'
2015-08-02 04:18:44 +02:00
);
2015-07-28 03:53:44 +02:00
}
2015-08-02 15:19:32 +02:00
return isset ( $inAddr [ 4 ]) ? " udp://[ { $addr } ]: { $port } " : " udp:// { $addr } : { $port } " ;
2015-08-02 04:18:44 +02:00
}
2015-07-28 03:53:44 +02:00
2015-08-02 04:18:44 +02:00
function __loadExistingServer ( $state , $uri ) {
if ( empty ( $state -> serverUriMap [ $uri ])) {
2015-09-10 17:11:48 +02:00
return null ;
2015-08-02 04:18:44 +02:00
}
2015-08-30 16:39:04 +02:00
2015-08-02 04:18:44 +02:00
$server = $state -> serverUriMap [ $uri ];
2015-09-10 17:11:48 +02:00
2015-08-02 15:19:32 +02:00
if ( \is_resource ( $server -> socket )) {
2015-08-02 04:18:44 +02:00
unset ( $state -> serverIdTimeoutMap [ $server -> id ]);
\Amp\enable ( $server -> watcherId );
return $server ;
}
2015-09-10 17:11:48 +02:00
2015-08-02 04:18:44 +02:00
__unloadServer ( $state , $server -> id );
2015-09-10 17:11:48 +02:00
return null ;
2015-08-02 04:18:44 +02:00
}
2015-07-28 03:53:44 +02:00
2015-08-02 04:18:44 +02:00
function __loadNewServer ( $state , $uri ) {
if ( ! $socket = @ \stream_socket_client ( $uri , $errno , $errstr )) {
throw new ResolutionException ( sprintf (
2015-07-28 03:53:44 +02:00
" Connection to %s failed: [Error #%d] %s " ,
$uri ,
$errno ,
$errstr
));
}
2015-08-02 04:18:44 +02:00
\stream_set_blocking ( $socket , false );
$id = ( int ) $socket ;
$server = new \StdClass ;
$server -> id = $id ;
$server -> uri = $uri ;
$server -> socket = $socket ;
2015-08-30 19:59:49 +02:00
$server -> buffer = " " ;
$server -> length = INF ;
2015-08-02 04:18:44 +02:00
$server -> pendingRequests = [];
2015-09-18 03:37:10 +02:00
$server -> watcherId = \Amp\onReadable ( $socket , 'Amp\Dns\__onReadable' , [
2015-08-02 04:18:44 +02:00
" enable " => true ,
" keep_alive " => true ,
" cb_data " => $state ,
]);
$state -> serverIdMap [ $id ] = $server ;
$state -> serverUriMap [ $uri ] = $server ;
return $server ;
}
2015-07-28 03:53:44 +02:00
2015-08-02 04:18:44 +02:00
function __unloadServer ( $state , $serverId , $error = null ) {
2016-02-29 19:58:53 +01:00
// Might already have been unloaded (especially if multiple requests happen)
if ( ! isset ( $state -> serverIdMap [ $serverId ])) {
return ;
}
2015-08-02 04:18:44 +02:00
$server = $state -> serverIdMap [ $serverId ];
\Amp\cancel ( $server -> watcherId );
unset (
$state -> serverIdMap [ $serverId ],
$state -> serverUriMap [ $server -> uri ]
);
if ( \is_resource ( $server -> socket )) {
@ \fclose ( $server -> socket );
}
if ( $error && $server -> pendingRequests ) {
foreach ( array_keys ( $server -> pendingRequests ) as $requestId ) {
list ( $promisor ) = $state -> pendingRequests [ $requestId ];
$promisor -> fail ( $error );
}
}
}
2015-07-28 03:53:44 +02:00
2015-08-02 04:18:44 +02:00
function __onReadable ( $watcherId , $socket , $state ) {
$serverId = ( int ) $socket ;
$packet = @ \fread ( $socket , 512 );
if ( $packet != " " ) {
2015-08-30 19:59:49 +02:00
$server = $state -> serverIdMap [ $serverId ];
if ( \substr ( $server -> uri , 0 , 6 ) == " tcp:// " ) {
if ( $server -> length == INF ) {
$server -> length = unpack ( " n " , $packet )[ 1 ];
$packet = substr ( $packet , 2 );
}
$server -> buffer .= $packet ;
while ( $server -> length <= \strlen ( $server -> buffer )) {
__decodeResponsePacket ( $state , $serverId , substr ( $server -> buffer , 0 , $server -> length ));
$server -> buffer = substr ( $server -> buffer , $server -> length );
if ( \strlen ( $server -> buffer ) >= 2 + $server -> length ) {
$server -> length = unpack ( " n " , $server -> buffer )[ 1 ];
$server -> buffer = substr ( $server -> buffer , 2 );
} else {
$server -> length = INF ;
}
}
} else {
__decodeResponsePacket ( $state , $serverId , $packet );
}
2015-07-28 03:53:44 +02:00
} else {
2015-08-02 04:18:44 +02:00
__unloadServer ( $state , $serverId , new ResolutionException (
" Server connection failed "
));
2015-07-28 03:53:44 +02:00
}
2015-08-02 04:18:44 +02:00
}
2015-07-28 03:53:44 +02:00
2015-08-02 04:18:44 +02:00
function __decodeResponsePacket ( $state , $serverId , $packet ) {
try {
$response = $state -> decoder -> decode ( $packet );
$requestId = $response -> getID ();
$responseCode = $response -> getResponseCode ();
$responseType = $response -> getType ();
if ( $responseCode !== 0 ) {
__finalizeResult ( $state , $serverId , $requestId , new ResolutionException (
" Server returned error code: { $responseCode } "
));
} elseif ( $responseType !== MessageTypes :: RESPONSE ) {
__unloadServer ( $state , $serverId , new ResolutionException (
" Invalid server reply; expected RESPONSE but received QUERY "
));
} else {
__processDecodedResponse ( $state , $serverId , $requestId , $response );
}
} catch ( \Exception $e ) {
__unloadServer ( $state , $serverId , new ResolutionException (
2015-08-30 16:39:04 +02:00
" Response decode error " , 0 , $e
2015-08-02 04:18:44 +02:00
));
}
}
2015-07-28 03:53:44 +02:00
2015-08-02 04:18:44 +02:00
function __processDecodedResponse ( $state , $serverId , $requestId , $response ) {
2015-08-30 19:59:49 +02:00
list ( $promisor , $name , $type , $uri ) = $state -> pendingRequests [ $requestId ];
// Retry via tcp if message has been truncated
if ( $response -> isTruncated ()) {
if ( \substr ( $uri , 0 , 6 ) != " tcp:// " ) {
$uri = \preg_replace ( " #[a-z.]+://# " , " tcp:// " , $uri );
$promisor -> succeed ( __doRequest ( $state , $uri , $name , $type ));
} else {
__finalizeResult ( $state , $serverId , $requestId , new ResolutionException (
" Server returned truncated response "
));
}
return ;
}
2015-08-02 04:18:44 +02:00
$answers = $response -> getAnswerRecords ();
foreach ( $answers as $record ) {
2015-08-30 16:39:04 +02:00
$result [ $record -> getType ()][] = [( string ) $record -> getData (), $record -> getType (), $record -> getTTL ()];
2015-08-02 04:18:44 +02:00
}
if ( empty ( $result )) {
2015-09-08 23:03:25 +02:00
$state -> arrayCache -> set ( " $name # $type " , [], 300 ); // "it MUST NOT cache it for longer than five (5) minutes" per RFC 2308 section 7.1
2015-08-02 04:18:44 +02:00
__finalizeResult ( $state , $serverId , $requestId , new NoRecordException (
2015-08-30 16:39:04 +02:00
" No records returned for { $name } "
2015-08-02 04:18:44 +02:00
));
} else {
__finalizeResult ( $state , $serverId , $requestId , $error = null , $result );
}
}
2015-07-28 03:53:44 +02:00
2015-08-02 04:18:44 +02:00
function __finalizeResult ( $state , $serverId , $requestId , $error = null , $result = null ) {
if ( empty ( $state -> pendingRequests [ $requestId ])) {
return ;
}
2015-07-28 03:53:44 +02:00
2015-08-30 16:39:04 +02:00
list ( $promisor , $name ) = $state -> pendingRequests [ $requestId ];
2015-08-02 04:18:44 +02:00
$server = $state -> serverIdMap [ $serverId ];
unset (
$state -> pendingRequests [ $requestId ],
$server -> pendingRequests [ $requestId ]
);
if ( empty ( $server -> pendingRequests )) {
$state -> serverIdTimeoutMap [ $server -> id ] = $state -> now + IDLE_TIMEOUT ;
\Amp\disable ( $server -> watcherId );
\Amp\enable ( $state -> serverTimeoutWatcher );
}
if ( $error ) {
$promisor -> fail ( $error );
} else {
2015-08-30 16:39:04 +02:00
foreach ( $result as $type => $records ) {
$minttl = INF ;
2015-09-18 03:37:10 +02:00
foreach ( $records as list ( , $ttl )) {
2015-08-30 16:39:04 +02:00
if ( $ttl && $minttl > $ttl ) {
$minttl = $ttl ;
}
}
$state -> arrayCache -> set ( " $name # $type " , $records , $minttl );
}
2015-08-02 04:18:44 +02:00
$promisor -> succeed ( $result );
}
2015-07-28 03:53:44 +02:00
}