2014-06-15 01:47:15 +02:00
|
|
|
<?php
|
|
|
|
|
|
|
|
namespace Addr;
|
|
|
|
|
|
|
|
use Alert\Reactor;
|
|
|
|
|
|
|
|
class Client
|
|
|
|
{
|
|
|
|
/**
|
|
|
|
* @var Reactor
|
|
|
|
*/
|
|
|
|
private $reactor;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @var RequestBuilder
|
|
|
|
*/
|
|
|
|
private $requestBuilder;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @var ResponseInterpreter
|
|
|
|
*/
|
|
|
|
private $responseInterpreter;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @var resource
|
|
|
|
*/
|
|
|
|
private $socket;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @var int
|
|
|
|
*/
|
|
|
|
private $requestTimeout;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @var int
|
|
|
|
*/
|
|
|
|
private $readWatcherId;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @var array
|
|
|
|
*/
|
2014-06-16 04:28:59 +02:00
|
|
|
private $pendingLookups = [];
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @var array
|
|
|
|
*/
|
|
|
|
private $pendingRequestsByNameAndType = [];
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @var array
|
|
|
|
*/
|
|
|
|
private $pendingRequestsById = [];
|
2014-06-15 01:47:15 +02:00
|
|
|
|
|
|
|
/**
|
|
|
|
* @var int
|
|
|
|
*/
|
|
|
|
private $requestIdCounter = 0;
|
|
|
|
|
2014-06-16 04:28:59 +02:00
|
|
|
/**
|
|
|
|
* @var int
|
|
|
|
*/
|
|
|
|
private $lookupIdCounter = 0;
|
|
|
|
|
2014-06-15 01:47:15 +02:00
|
|
|
/**
|
|
|
|
* Constructor
|
|
|
|
*
|
|
|
|
* @param Reactor $reactor
|
|
|
|
* @param RequestBuilder $requestBuilder
|
|
|
|
* @param ResponseInterpreter $responseInterpreter
|
|
|
|
* @param string $serverAddress
|
|
|
|
* @param int $serverPort
|
|
|
|
* @param int $requestTimeout
|
|
|
|
* @throws \RuntimeException
|
|
|
|
*/
|
|
|
|
public function __construct(
|
|
|
|
Reactor $reactor,
|
|
|
|
RequestBuilder $requestBuilder,
|
|
|
|
ResponseInterpreter $responseInterpreter,
|
2014-06-15 23:45:33 +02:00
|
|
|
$serverAddress = null,
|
|
|
|
$serverPort = null,
|
|
|
|
$requestTimeout = null
|
2014-06-15 01:47:15 +02:00
|
|
|
) {
|
|
|
|
$this->reactor = $reactor;
|
|
|
|
$this->requestBuilder = $requestBuilder;
|
|
|
|
$this->responseInterpreter = $responseInterpreter;
|
|
|
|
|
2014-06-16 04:28:59 +02:00
|
|
|
$serverAddress = $serverAddress !== null ? (string)$serverAddress : '8.8.8.8';
|
|
|
|
$serverPort = $serverPort !== null ? (int)$serverPort : 53;
|
|
|
|
$requestTimeout = $requestTimeout !== null ? (int)$requestTimeout : 2000;
|
2014-06-15 23:45:33 +02:00
|
|
|
|
2014-06-15 01:47:15 +02:00
|
|
|
$address = sprintf('udp://%s:%d', $serverAddress, $serverPort);
|
|
|
|
$this->socket = stream_socket_client($address, $errNo, $errStr);
|
|
|
|
if (!$this->socket) {
|
|
|
|
throw new \RuntimeException("Creating socket {$address} failed: {$errNo}: {$errStr}");
|
|
|
|
}
|
|
|
|
|
|
|
|
stream_set_blocking($this->socket, 0);
|
|
|
|
$this->requestTimeout = $requestTimeout;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get the next available request ID
|
|
|
|
*
|
|
|
|
* @return int
|
|
|
|
*/
|
|
|
|
private function getNextFreeRequestId()
|
|
|
|
{
|
|
|
|
do {
|
|
|
|
$result = $this->requestIdCounter++;
|
|
|
|
|
|
|
|
if ($this->requestIdCounter >= 65536) {
|
|
|
|
$this->requestIdCounter = 0;
|
|
|
|
}
|
2014-06-16 04:28:59 +02:00
|
|
|
} while(isset($this->pendingRequestsById[$result]));
|
|
|
|
|
|
|
|
return $result;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get the next available lookup ID
|
|
|
|
*
|
|
|
|
* @return int
|
|
|
|
*/
|
|
|
|
private function getNextFreeLookupId()
|
|
|
|
{
|
|
|
|
do {
|
|
|
|
$result = $this->lookupIdCounter++;
|
|
|
|
|
|
|
|
if ($this->lookupIdCounter >= PHP_INT_MAX) {
|
|
|
|
$this->lookupIdCounter = 0;
|
|
|
|
}
|
|
|
|
} while(isset($this->pendingLookups[$result]));
|
2014-06-15 01:47:15 +02:00
|
|
|
|
|
|
|
return $result;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get a list of requests to execute for a given mode mask
|
|
|
|
*
|
|
|
|
* @param int $mode
|
|
|
|
* @return array
|
|
|
|
*/
|
|
|
|
private function getRequestList($mode)
|
|
|
|
{
|
|
|
|
$result = [];
|
|
|
|
|
|
|
|
if ($mode & AddressModes::PREFER_INET6) {
|
|
|
|
if ($mode & AddressModes::INET6_ADDR) {
|
|
|
|
$result[] = AddressModes::INET6_ADDR;
|
|
|
|
}
|
|
|
|
if ($mode & AddressModes::INET4_ADDR) {
|
|
|
|
$result[] = AddressModes::INET4_ADDR;
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
if ($mode & AddressModes::INET4_ADDR) {
|
|
|
|
$result[] = AddressModes::INET4_ADDR;
|
|
|
|
}
|
|
|
|
if ($mode & AddressModes::INET6_ADDR) {
|
|
|
|
$result[] = AddressModes::INET6_ADDR;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return $result;
|
|
|
|
}
|
|
|
|
|
2014-06-16 04:28:59 +02:00
|
|
|
/**
|
|
|
|
* Send a request to the server
|
|
|
|
*
|
|
|
|
* @param array $request
|
|
|
|
*/
|
|
|
|
private function sendRequest($request)
|
|
|
|
{
|
|
|
|
$packet = $this->requestBuilder->buildRequest($request['id'], $request['name'], $request['type']);
|
|
|
|
fwrite($this->socket, $packet);
|
|
|
|
|
|
|
|
$request['timeout_id'] = $this->reactor->once(function() use($request) {
|
|
|
|
unset($this->pendingRequestsByNameAndType[$request['name']][$request['type']]);
|
|
|
|
|
|
|
|
foreach ($request['lookups'] as $id => $lookup) {
|
|
|
|
$this->completePendingLookup($id, null, ResolutionErrors::ERR_SERVER_TIMEOUT);
|
|
|
|
}
|
|
|
|
}, $this->requestTimeout);
|
|
|
|
|
|
|
|
if ($this->readWatcherId === null) {
|
|
|
|
$this->readWatcherId = $this->reactor->onReadable($this->socket, function() {
|
|
|
|
$this->onSocketReadable();
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
$this->pendingRequestsById[$request['id']] = $request;
|
|
|
|
$this->pendingRequestsByNameAndType[$request['name']][$request['type']] = &$this->pendingRequestsById[$request['id']];
|
|
|
|
}
|
|
|
|
|
2014-06-15 01:47:15 +02:00
|
|
|
/**
|
|
|
|
* Handle data waiting to be read from the socket
|
|
|
|
*/
|
|
|
|
private function onSocketReadable()
|
|
|
|
{
|
|
|
|
$packet = fread($this->socket, 512);
|
|
|
|
|
2014-06-17 19:01:10 +02:00
|
|
|
// Decode the response and clean up the pending requests list
|
|
|
|
$decoded = $this->responseInterpreter->decode($packet);
|
|
|
|
if ($decoded === null) {
|
2014-06-15 01:47:15 +02:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2014-06-17 19:01:10 +02:00
|
|
|
list($id, $response) = $decoded;
|
2014-06-16 04:28:59 +02:00
|
|
|
$request = $this->pendingRequestsById[$id];
|
|
|
|
$name = $request['name'];
|
|
|
|
|
|
|
|
$this->reactor->cancel($request['timeout_id']);
|
2014-06-17 19:01:10 +02:00
|
|
|
unset($this->pendingRequestsById[$id], $this->pendingRequestsByNameAndType[$name][$request['type']]);
|
2014-06-16 04:28:59 +02:00
|
|
|
if (!$this->pendingRequestsById) {
|
|
|
|
$this->reactor->cancel($this->readWatcherId);
|
|
|
|
$this->readWatcherId = null;
|
|
|
|
}
|
|
|
|
|
2014-06-17 19:01:10 +02:00
|
|
|
// Interpret the response and make sure we have at least one resource record
|
|
|
|
$interpreted = $this->responseInterpreter->interpret($response, $request['type']);
|
|
|
|
if ($interpreted === null) {
|
|
|
|
foreach ($request['lookups'] as $id => $lookup) {
|
|
|
|
$this->processPendingLookup($id);
|
|
|
|
}
|
|
|
|
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Distribute the result to the appropriate lookup routine
|
|
|
|
list($type, $addr, $ttl) = $interpreted;
|
|
|
|
if ($type === AddressModes::CNAME) {
|
|
|
|
foreach ($request['lookups'] as $id => $lookup) {
|
|
|
|
$this->redirectPendingLookup($id, $addr);
|
|
|
|
}
|
|
|
|
} else if ($addr !== null) {
|
2014-06-16 04:28:59 +02:00
|
|
|
if ($request['cache_store']) {
|
|
|
|
call_user_func($request['cache_store'], $name, $addr, $type, $ttl);
|
|
|
|
}
|
2014-06-16 04:53:40 +02:00
|
|
|
|
|
|
|
foreach ($request['lookups'] as $id => $lookup) {
|
|
|
|
$this->completePendingLookup($id, $addr, $type);
|
|
|
|
}
|
2014-06-15 01:47:15 +02:00
|
|
|
} else {
|
2014-06-16 04:28:59 +02:00
|
|
|
foreach ($request['lookups'] as $id => $lookup) {
|
|
|
|
$this->processPendingLookup($id);
|
|
|
|
}
|
2014-06-15 01:47:15 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Call a response callback with the result
|
|
|
|
*
|
|
|
|
* @param int $id
|
|
|
|
* @param string $addr
|
|
|
|
* @param int $type
|
|
|
|
*/
|
2014-06-16 04:28:59 +02:00
|
|
|
private function completePendingLookup($id, $addr, $type)
|
2014-06-15 01:47:15 +02:00
|
|
|
{
|
2014-06-16 04:28:59 +02:00
|
|
|
call_user_func($this->pendingLookups[$id]['callback'], $addr, $type);
|
|
|
|
unset($this->pendingLookups[$id]);
|
2014-06-15 01:47:15 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Send a request to the server
|
|
|
|
*
|
|
|
|
* @param int $id
|
|
|
|
*/
|
2014-06-16 04:28:59 +02:00
|
|
|
private function processPendingLookup($id)
|
2014-06-15 01:47:15 +02:00
|
|
|
{
|
2014-06-16 04:28:59 +02:00
|
|
|
$lookup = &$this->pendingLookups[$id];
|
|
|
|
|
|
|
|
if (!$lookup['requests']) {
|
|
|
|
$this->completePendingLookup($id, null, ResolutionErrors::ERR_NO_RECORD);
|
2014-06-15 01:47:15 +02:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2014-06-16 04:28:59 +02:00
|
|
|
$name = $lookup['name'];
|
|
|
|
$type = array_shift($lookup['requests']);
|
|
|
|
$lookup['last_type'] = $type;
|
2014-06-15 01:47:15 +02:00
|
|
|
|
2014-06-16 04:28:59 +02:00
|
|
|
$this->pendingRequestsByNameAndType[$name][$type]['lookups'][$id] = $lookup;
|
2014-06-15 01:47:15 +02:00
|
|
|
|
2014-06-16 04:28:59 +02:00
|
|
|
if (count($this->pendingRequestsByNameAndType[$name][$type]) === 1) {
|
|
|
|
$request = [
|
|
|
|
'id' => $this->getNextFreeRequestId(),
|
|
|
|
'name' => $name,
|
|
|
|
'type' => $type,
|
|
|
|
'lookups' => [$id => $lookup],
|
|
|
|
'timeout_id' => null,
|
|
|
|
'cache_store' => $lookup['cache_store'],
|
|
|
|
];
|
2014-06-15 01:47:15 +02:00
|
|
|
|
2014-06-16 04:28:59 +02:00
|
|
|
$this->sendRequest($request);
|
2014-06-15 01:47:15 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2014-06-17 19:01:10 +02:00
|
|
|
/**
|
|
|
|
* Redirect a lookup to search for another name
|
|
|
|
*
|
|
|
|
* @param int $id
|
|
|
|
* @param string $name
|
|
|
|
*/
|
|
|
|
private function redirectPendingLookup($id, $name)
|
|
|
|
{
|
|
|
|
array_unshift($this->pendingLookups[$id]['requests'], $this->pendingLookups[$id]['last_type']);
|
|
|
|
$this->pendingLookups[$id]['last_type'] = null;
|
|
|
|
$this->pendingLookups[$id]['name'] = $name;
|
|
|
|
|
|
|
|
$this->processPendingLookup($id);
|
|
|
|
}
|
|
|
|
|
2014-06-15 01:47:15 +02:00
|
|
|
/**
|
|
|
|
* Resolve a name from a server
|
|
|
|
*
|
|
|
|
* @param string $name
|
|
|
|
* @param int $mode
|
|
|
|
* @param callable $callback
|
2014-06-16 04:28:59 +02:00
|
|
|
* @param callable $cacheStore
|
2014-06-15 01:47:15 +02:00
|
|
|
*/
|
2014-06-16 04:28:59 +02:00
|
|
|
public function resolve($name, $mode, callable $callback, callable $cacheStore = null)
|
2014-06-15 01:47:15 +02:00
|
|
|
{
|
2014-06-16 04:28:59 +02:00
|
|
|
$id = $this->getNextFreeLookupId();
|
|
|
|
|
|
|
|
$this->pendingLookups[$id] = [
|
|
|
|
'name' => $name,
|
|
|
|
'requests' => $this->getRequestList($mode),
|
|
|
|
'last_type' => null,
|
|
|
|
'callback' => $callback,
|
|
|
|
'cache_store' => $cacheStore,
|
2014-06-16 02:20:38 +02:00
|
|
|
];
|
|
|
|
|
2014-06-16 04:28:59 +02:00
|
|
|
$this->processPendingLookup($id);
|
2014-06-15 01:47:15 +02:00
|
|
|
}
|
|
|
|
}
|