diff --git a/src/Model/Message.php b/src/Model/Message.php index a3ced691..fccf00be 100644 --- a/src/Model/Message.php +++ b/src/Model/Message.php @@ -2,6 +2,9 @@ namespace React\Dns\Model; +use React\Dns\Query\Query; +use React\Dns\Model\Record; + class Message { const TYPE_A = 1; @@ -25,6 +28,55 @@ class Message const RCODE_NOT_IMPLEMENTED = 4; const RCODE_REFUSED = 5; + /** + * Creates a new request message for the given query + * + * @param Query $query + * @return self + */ + public static function createRequestForQuery(Query $query) + { + $request = new Message(); + $request->header->set('id', self::generateId()); + $request->header->set('rd', 1); + $request->questions[] = (array) $query; + $request->prepare(); + + return $request; + } + + /** + * Creates a new response message for the given query with the given answer records + * + * @param Query $query + * @param Record[] $answers + * @return self + */ + public static function createResponseWithAnswersForQuery(Query $query, array $answers) + { + $response = new Message(); + $response->header->set('id', self::generateId()); + $response->header->set('qr', 1); + $response->header->set('opcode', Message::OPCODE_QUERY); + $response->header->set('rd', 1); + $response->header->set('rcode', Message::RCODE_OK); + + $response->questions[] = (array) $query; + + foreach ($answers as $record) { + $response->answers[] = $record; + } + + $response->prepare(); + + return $response; + } + + private static function generateId() + { + return mt_rand(0, 0xffff); + } + public $data = ''; public $header; diff --git a/src/Protocol/Parser.php b/src/Protocol/Parser.php index 1099328a..04ae18d6 100644 --- a/src/Protocol/Parser.php +++ b/src/Protocol/Parser.php @@ -4,6 +4,7 @@ use React\Dns\Model\Message; use React\Dns\Model\Record; +use InvalidArgumentException; /** * DNS protocol parser @@ -12,7 +13,32 @@ */ class Parser { + /** + * Parses the given raw binary message into a Message object + * + * @param string $data + * @throws InvalidArgumentException + * @return Message + */ + public function parseMessage($data) + { + $message = new Message(); + if ($this->parse($data, $message) !== $message) { + throw new InvalidArgumentException('Unable to parse binary message'); + } + + return $message; + } + + /** + * @deprecated unused, exists for BC only + */ public function parseChunk($data, Message $message) + { + return $this->parse($data, $message); + } + + private function parse($data, Message $message) { $message->data .= $data; diff --git a/src/Query/CachedExecutor.php b/src/Query/CachedExecutor.php index 9953bc53..285936db 100644 --- a/src/Query/CachedExecutor.php +++ b/src/Query/CachedExecutor.php @@ -3,7 +3,6 @@ namespace React\Dns\Query; use React\Dns\Model\Message; -use React\Dns\Model\Record; class CachedExecutor implements ExecutorInterface { @@ -18,15 +17,14 @@ public function __construct(ExecutorInterface $executor, RecordCache $cache) public function query($nameserver, Query $query) { - $that = $this; $executor = $this->executor; $cache = $this->cache; return $this->cache ->lookup($query) ->then( - function ($cachedRecords) use ($that, $query) { - return $that->buildResponse($query, $cachedRecords); + function ($cachedRecords) use ($query) { + return Message::createResponseWithAnswersForQuery($query, $cachedRecords); }, function () use ($executor, $cache, $nameserver, $query) { return $executor @@ -39,27 +37,17 @@ function () use ($executor, $cache, $nameserver, $query) { ); } + /** + * @deprecated unused, exists for BC only + */ public function buildResponse(Query $query, array $cachedRecords) { - $response = new Message(); - - $response->header->set('id', $this->generateId()); - $response->header->set('qr', 1); - $response->header->set('opcode', Message::OPCODE_QUERY); - $response->header->set('rd', 1); - $response->header->set('rcode', Message::RCODE_OK); - - $response->questions[] = new Record($query->name, $query->type, $query->class); - - foreach ($cachedRecords as $record) { - $response->answers[] = $record; - } - - $response->prepare(); - - return $response; + return Message::createResponseWithAnswersForQuery($query, $cachedRecords); } + /** + * @deprecated unused, exists for BC only + */ protected function generateId() { return mt_rand(0, 0xffff); diff --git a/src/Query/Executor.php b/src/Query/Executor.php index 3766866b..2e201173 100644 --- a/src/Query/Executor.php +++ b/src/Query/Executor.php @@ -38,7 +38,7 @@ public function __construct(LoopInterface $loop, Parser $parser, BinaryDumper $d public function query($nameserver, Query $query) { - $request = $this->prepareRequest($query); + $request = Message::createRequestForQuery($query); $queryData = $this->dumper->toBinary($request); $transport = strlen($queryData) > 512 ? 'tcp' : 'udp'; @@ -46,15 +46,12 @@ public function query($nameserver, Query $query) return $this->doQuery($nameserver, $transport, $queryData, $query->name); } + /** + * @deprecated unused, exists for BC only + */ public function prepareRequest(Query $query) { - $request = new Message(); - $request->header->set('id', $this->generateId()); - $request->header->set('rd', 1); - $request->questions[] = (array) $query; - $request->prepare(); - - return $request; + return Message::createRequestForQuery($query); } public function doQuery($nameserver, $transport, $queryData, $name) @@ -63,7 +60,6 @@ public function doQuery($nameserver, $transport, $queryData, $name) $parser = $this->parser; $loop = $this->loop; - $response = new Message(); $deferred = new Deferred(function ($resolve, $reject) use (&$timer, &$conn, $name) { $reject(new CancellationException(sprintf('DNS query for %s has been cancelled', $name))); @@ -108,17 +104,19 @@ public function doQuery($nameserver, $transport, $queryData, $name) return $deferred->promise(); } - $conn->on('data', function ($data) use ($retryWithTcp, $conn, $parser, $response, $transport, $deferred, $timer) { - $responseReady = $parser->parseChunk($data, $response); - - if (!$responseReady) { - return; - } - + $conn->on('data', function ($data) use ($retryWithTcp, $conn, $parser, $transport, $deferred, $timer) { if ($timer !== null) { $timer->cancel(); } + try { + $response = $parser->parseMessage($data); + } catch (\Exception $e) { + $conn->end(); + $deferred->reject($e); + return; + } + if ($response->header->isTruncated()) { if ('tcp' === $transport) { $deferred->reject(new BadServerException('The server set the truncated bit although we issued a TCP request')); @@ -138,6 +136,9 @@ public function doQuery($nameserver, $transport, $queryData, $name) return $deferred->promise(); } + /** + * @deprecated unused, exists for BC only + */ protected function generateId() { return mt_rand(0, 0xffff); diff --git a/tests/Model/MessageTest.php b/tests/Model/MessageTest.php new file mode 100644 index 00000000..46528273 --- /dev/null +++ b/tests/Model/MessageTest.php @@ -0,0 +1,29 @@ +assertTrue($request->header->isQuery()); + $this->assertSame(1, $request->header->get('rd')); + } + + public function testCreateResponseWithNoAnswers() + { + $query = new Query('igor.io', Message::TYPE_A, Message::CLASS_IN, 1345656451); + $answers = array(); + $request = Message::createResponseWithAnswersForQuery($query, $answers); + + $this->assertFalse($request->header->isQuery()); + $this->assertTrue($request->header->isResponse()); + $this->assertEquals(0, $request->header->get('anCount')); + } +} diff --git a/tests/Protocol/ParserTest.php b/tests/Protocol/ParserTest.php index e6a12cb8..aee590c1 100644 --- a/tests/Protocol/ParserTest.php +++ b/tests/Protocol/ParserTest.php @@ -7,6 +7,11 @@ class ParserTest extends \PHPUnit_Framework_TestCase { + public function setUp() + { + $this->parser = new Parser(); + } + /** * @dataProvider provideConvertTcpDumpToBinary */ @@ -34,10 +39,7 @@ public function testParseRequest() $data = $this->convertTcpDumpToBinary($data); - $request = new Message(); - - $parser = new Parser(); - $parser->parseChunk($data, $request); + $request = $this->parser->parseMessage($data); $header = $request->header; $this->assertSame(0x7262, $header->get('id')); @@ -74,10 +76,7 @@ public function testParseResponse() $data = $this->convertTcpDumpToBinary($data); - $response = new Message(); - - $parser = new Parser(); - $parser->parseChunk($data, $response); + $response = $this->parser->parseMessage($data); $header = $response->header; $this->assertSame(0x7262, $header->get('id')); @@ -121,8 +120,7 @@ public function testParseQuestionWithTwoQuestions() $request->header->set('qdCount', 2); $request->data = $data; - $parser = new Parser(); - $parser->parseQuestion($request); + $this->parser->parseQuestion($request); $this->assertCount(2, $request->questions); $this->assertSame('igor.io', $request->questions[0]['name']); @@ -148,8 +146,7 @@ public function testParseAnswerWithInlineData() $response->header->set('anCount', 1); $response->data = $data; - $parser = new Parser(); - $parser->parseAnswer($response); + $this->parser->parseAnswer($response); $this->assertCount(1, $response->answers); $this->assertSame('igor.io', $response->answers[0]->name); @@ -174,10 +171,7 @@ public function testParseResponseWithCnameAndOffsetPointers() $data = $this->convertTcpDumpToBinary($data); - $response = new Message(); - - $parser = new Parser(); - $parser->parseChunk($data, $response); + $response = $this->parser->parseMessage($data); $this->assertCount(1, $response->questions); $this->assertSame('mail.google.com', $response->questions[0]['name']); @@ -212,10 +206,7 @@ public function testParseResponseWithTwoAnswers() $data = $this->convertTcpDumpToBinary($data); - $response = new Message(); - - $parser = new Parser(); - $parser->parseChunk($data, $response); + $response = $this->parser->parseMessage($data); $this->assertCount(1, $response->questions); $this->assertSame('io.whois-servers.net', $response->questions[0]['name']); @@ -237,6 +228,21 @@ public function testParseResponseWithTwoAnswers() $this->assertSame('193.223.78.152', $response->answers[1]->data); } + /** + * @expectedException InvalidArgumentException + */ + public function testParseIncomplete() + { + $data = ""; + $data .= "72 62 01 00 00 01 00 00 00 00 00 00"; // header + $data .= "04 69 67 6f 72 02 69 6f 00"; // question: igor.io + //$data .= "00 01 00 01"; // question: type A, class IN + + $data = $this->convertTcpDumpToBinary($data); + + $this->parser->parseMessage($data); + } + private function convertTcpDumpToBinary($input) { // sudo ngrep -d en1 -x port 53 diff --git a/tests/Query/ExecutorTest.php b/tests/Query/ExecutorTest.php index 688e8560..998f1df9 100644 --- a/tests/Query/ExecutorTest.php +++ b/tests/Query/ExecutorTest.php @@ -24,27 +24,21 @@ public function setUp() $this->executor = new Executor($this->loop, $this->parser, $this->dumper); } - /** @test */ - public function prepareRequestShouldCreateRequestWithRecursionDesired() - { - $query = new Query('igor.io', Message::TYPE_A, Message::CLASS_IN, 1345656451); - $request = $this->executor->prepareRequest($query); - - $this->assertTrue($request->header->isQuery()); - $this->assertSame(1, $request->header->get('rd')); - } - /** @test */ public function queryShouldCreateUdpRequest() { - $conn = $this->createConnectionMock(); + $timer = $this->getMock('React\EventLoop\Timer\TimerInterface'); + $this->loop + ->expects($this->any()) + ->method('addTimer') + ->will($this->returnValue($timer)); $this->executor = $this->createExecutorMock(); $this->executor ->expects($this->once()) ->method('createConnection') ->with('8.8.8.8:53', 'udp') - ->will($this->returnNewConnectionMock()); + ->will($this->returnNewConnectionMock(false)); $query = new Query('igor.io', Message::TYPE_A, Message::CLASS_IN, 1345656451); $this->executor->query('8.8.8.8:53', $query, function () {}, function () {}); @@ -53,14 +47,18 @@ public function queryShouldCreateUdpRequest() /** @test */ public function resolveShouldCreateTcpRequestIfRequestIsLargerThan512Bytes() { - $conn = $this->createConnectionMock(); + $timer = $this->getMock('React\EventLoop\Timer\TimerInterface'); + $this->loop + ->expects($this->any()) + ->method('addTimer') + ->will($this->returnValue($timer)); $this->executor = $this->createExecutorMock(); $this->executor ->expects($this->once()) ->method('createConnection') ->with('8.8.8.8:53', 'tcp') - ->will($this->returnNewConnectionMock()); + ->will($this->returnNewConnectionMock(false)); $query = new Query(str_repeat('a', 512).'.igor.io', Message::TYPE_A, Message::CLASS_IN, 1345656451); $this->executor->query('8.8.8.8:53', $query, function () {}, function () {}); @@ -69,7 +67,7 @@ public function resolveShouldCreateTcpRequestIfRequestIsLargerThan512Bytes() /** @test */ public function resolveShouldCloseConnectionWhenCancelled() { - $conn = $this->createConnectionMock(); + $conn = $this->createConnectionMock(false); $conn->expects($this->once())->method('close'); $timer = $this->getMock('React\EventLoop\Timer\TimerInterface'); @@ -141,13 +139,11 @@ public function resolveShouldRetryWithTcpIfResponseIsTruncated() $this->parser ->expects($this->at(0)) - ->method('parseChunk') - ->with($this->anything(), $this->isInstanceOf('React\Dns\Model\Message')) + ->method('parseMessage') ->will($this->returnTruncatedResponse()); $this->parser ->expects($this->at(1)) - ->method('parseChunk') - ->with($this->anything(), $this->isInstanceOf('React\Dns\Model\Message')) + ->method('parseMessage') ->will($this->returnStandardResponse()); $this->executor = $this->createExecutorMock(); @@ -178,8 +174,7 @@ public function resolveShouldRetryWithTcpIfUdpThrows() $this->parser ->expects($this->once()) - ->method('parseChunk') - ->with($this->anything(), $this->isInstanceOf('React\Dns\Model\Message')) + ->method('parseMessage') ->will($this->returnStandardResponse()); $this->executor = $this->createExecutorMock(); @@ -210,7 +205,7 @@ public function resolveShouldFailIfBothUdpAndTcpThrow() $this->parser ->expects($this->never()) - ->method('parseChunk'); + ->method('parseMessage'); $this->executor = $this->createExecutorMock(); $this->executor @@ -251,8 +246,7 @@ public function resolveShouldFailIfResponseIsTruncatedAfterTcpRequest() $this->parser ->expects($this->once()) - ->method('parseChunk') - ->with($this->anything(), $this->isInstanceOf('React\Dns\Model\Message')) + ->method('parseMessage') ->will($this->returnTruncatedResponse()); $this->executor = $this->createExecutorMock(); @@ -283,8 +277,7 @@ public function resolveShouldCancelTimerWhenFullResponseIsReceived() $this->parser ->expects($this->once()) - ->method('parseChunk') - ->with($this->anything(), $this->isInstanceOf('React\Dns\Model\Message')) + ->method('parseMessage') ->will($this->returnStandardResponse()); $this->executor = $this->createExecutorMock(); @@ -318,20 +311,22 @@ public function resolveShouldCloseConnectionOnTimeout() ->expects($this->at(0)) ->method('createConnection') ->with('8.8.8.8:53', 'udp') - ->will($this->returnNewConnectionMock()); + ->will($this->returnNewConnectionMock(false)); + + $timer = $this->getMock('React\EventLoop\Timer\TimerInterface'); + $timer + ->expects($this->never()) + ->method('cancel'); $this->loop ->expects($this->once()) ->method('addTimer') ->with(5, $this->isInstanceOf('Closure')) - ->will($this->returnCallback(function ($time, $callback) use (&$timerCallback) { + ->will($this->returnCallback(function ($time, $callback) use (&$timerCallback, $timer) { $timerCallback = $callback; + return $timer; })); - $this->loop - ->expects($this->never()) - ->method('cancelTimer'); - $callback = $this->expectCallableNever(); $errorback = $this->createCallableMock(); @@ -364,7 +359,8 @@ private function returnStandardResponse() private function returnTruncatedResponse() { $that = $this; - $callback = function ($data, $response) use ($that) { + $callback = function ($data) use ($that) { + $response = new Message(); $that->convertMessageToTruncatedResponse($response); return $response; }; @@ -391,9 +387,9 @@ public function convertMessageToTruncatedResponse(Message $response) return $response; } - private function returnNewConnectionMock() + private function returnNewConnectionMock($emitData = true) { - $conn = $this->createConnectionMock(); + $conn = $this->createConnectionMock($emitData); $callback = function () use ($conn) { return $conn; @@ -402,15 +398,15 @@ private function returnNewConnectionMock() return $this->returnCallback($callback); } - private function createConnectionMock() + private function createConnectionMock($emitData = true) { $conn = $this->getMock('React\Socket\ConnectionInterface'); $conn ->expects($this->any()) ->method('on') ->with('data', $this->isInstanceOf('Closure')) - ->will($this->returnCallback(function ($name, $callback) { - $callback(null); + ->will($this->returnCallback(function ($name, $callback) use ($emitData) { + $emitData && $callback(null); })); return $conn;