diff --git a/composer.json b/composer.json index 51c15fc..f99ece9 100644 --- a/composer.json +++ b/composer.json @@ -42,8 +42,7 @@ "amphp/parser": "^1", "amphp/windows-registry": "^0.3", "daverandom/libdns": "^2.0.1", - "ext-filter": "*", - "danog/libdns-native": "^0.1.0" + "ext-filter": "*" }, "require-dev": { "amphp/phpunit-util": "^1", diff --git a/lib/BlockingFallbackResolver.php b/lib/BlockingFallbackResolver.php index f7d8ee3..cf1b6bb 100644 --- a/lib/BlockingFallbackResolver.php +++ b/lib/BlockingFallbackResolver.php @@ -2,11 +2,11 @@ namespace Amp\Dns; +use Amp\Dns\Native\NativeDecoderFactory; +use Amp\Dns\Native\NativeEncoderFactory; use Amp\Failure; use Amp\Promise; use Amp\Success; -use danog\LibDNSNative\NativeDecoderFactory; -use danog\LibDNSNative\NativeEncoderFactory; use LibDNS\Messages\MessageFactory; use LibDNS\Messages\MessageTypes; use LibDNS\Records\Question; diff --git a/lib/Native/NativeDecoder.php b/lib/Native/NativeDecoder.php new file mode 100644 index 0000000..a24d61e --- /dev/null +++ b/lib/Native/NativeDecoder.php @@ -0,0 +1,281 @@ +, Chris Wright + */ +class NativeDecoder +{ + /** + * @var \LibDNS\Packets\PacketFactory + */ + private $packetFactory; + + /** + * @var \LibDNS\Messages\MessageFactory + */ + private $messageFactory; + + /** + * @var \LibDNS\Records\QuestionFactory + */ + private $questionFactory; + + /** + * @var \LibDNS\Records\Types\TypeBuilder + */ + private $typeBuilder; + /** + * @var \LibDNS\Decoder\DecoderFactory + */ + private $decoderFactory; + + /** + * Map class names to IDs. + * + * @var array + */ + private $classMap = []; + /** + * Constructor. + * + * @param \LibDNS\Packets\PacketFactory $packetFactory + * @param \LibDNS\Messages\MessageFactory $messageFactory + * @param \LibDNS\Records\QuestionFactory $questionFactory + * @param \LibDNS\Records\Types\TypeBuilder $typeBuilder + * @param \LibDNS\Encoder\EncodingContextFactory $encodingContextFactory + * @param \LibDNS\Decoder\DecoderFactory $decoderFactory + * @param bool $allowTrailingData + */ + public function __construct( + PacketFactory $packetFactory, + MessageFactory $messageFactory, + QuestionFactory $questionFactory, + TypeBuilder $typeBuilder, + EncodingContextFactory $encodingContextFactory, + DecoderFactory $decoderFactory + ) { + $this->packetFactory = $packetFactory; + $this->messageFactory = $messageFactory; + $this->questionFactory = $questionFactory; + $this->typeBuilder = $typeBuilder; + $this->encodingContextFactory = $encodingContextFactory; + $this->decoderFactory = $decoderFactory; + + $classes = new \ReflectionClass('\\LibDNS\\Records\\ResourceClasses'); + foreach ($classes->getConstants() as $name => $value) { + $this->classMap[$name] = $value; + } + } + /** + * Decode a question record. + * + * + * @return \LibDNS\Records\Question + * @throws \UnexpectedValueException When the record is invalid + */ + private function decodeQuestionRecord(string $name, int $type): Question + { + /** @var \LibDNS\Records\Types\DomainName $domainName */ + $domainName = $this->typeBuilder->build(Types::DOMAIN_NAME); + $labels = \explode('.', $name); + if (!empty($last = \array_pop($labels))) { + $labels[] = $last; + } + $domainName->setLabels($labels); + + $question = $this->questionFactory->create($type); + $question->setName($domainName); + //$question->setClass($meta['class']); + return $question; + } + + /** + * Encode a question record. + * + * @param \LibDNS\Encoder\EncodingContext $encodingContext + * @param \LibDNS\Records\Question $record + */ + private function encodeQuestionRecord(EncodingContext $encodingContext, string $name, int $type) + { + if (!$encodingContext->isTruncated()) { + $packet = $encodingContext->getPacket(); + $record = $this->decodeQuestionRecord($name, $type); + $name = $this->encodeDomainName($record->getName(), $encodingContext); + $meta = \pack('n*', $record->getType(), $record->getClass()); + + if (12 + $packet->getLength()+\strlen($name) + 4 > 512) { + $encodingContext->isTruncated(true); + } else { + $packet->write($name); + $packet->write($meta); + } + } + } + /** + * Encode a DomainName field. + * + * @param \LibDNS\Records\Types\DomainName $domainName + * @param \LibDNS\Encoder\EncodingContext $encodingContext + * @return string + */ + private function encodeDomainName(DomainName $domainName, EncodingContext $encodingContext): string + { + $packetIndex = $encodingContext->getPacket()->getLength() + 12; + $labelRegistry = $encodingContext->getLabelRegistry(); + + $result = ''; + $labels = $domainName->getLabels(); + + if ($encodingContext->useCompression()) { + do { + $part = \implode('.', $labels); + $index = $labelRegistry->lookupIndex($part); + + if ($index === null) { + $labelRegistry->register($part, $packetIndex); + + $label = \array_shift($labels); + $length = \strlen($label); + + $result .= \chr($length).$label; + $packetIndex += $length + 1; + } else { + $result .= \pack('n', 0b1100000000000000 | $index); + break; + } + } while ($labels); + + if (!$labels) { + $result .= "\x00"; + } + } else { + foreach ($labels as $label) { + $result .= \chr(\strlen($label)).$label; + } + + $result .= "\x00"; + } + + return $result; + } + + /** + * Encode a resource record. + * + * @param \LibDNS\Encoder\EncodingContext $encodingContext + * @param array $record + */ + private function encodeResourceRecord(EncodingContext $encodingContext, array $record) + { + if (!$encodingContext->isTruncated()) { + /** @var \LibDNS\Records\Types\DomainName $domainName */ + $domainName = $this->typeBuilder->build(Types::DOMAIN_NAME); + $labels = \explode('.', $record['host']); + if (!empty($last = \array_pop($labels))) { + $labels[] = $last; + } + $domainName->setLabels($labels); + + $packet = $encodingContext->getPacket(); + $name = $this->encodeDomainName($domainName, $encodingContext); + + $data = $record['data']; + $meta = \pack('n2Nn', $record['type'], $this->classMap[$record['class']], $record['ttl'], \strlen($data)); + + if (12 + $packet->getLength()+\strlen($name) + 10+\strlen($data) > 512) { + $encodingContext->isTruncated(true); + } else { + $packet->write($name); + $packet->write($meta); + $packet->write($data); + } + } + } + + /** + * Decode a Message from JSON-encoded string. + * + * @param array $result The actual response + * @param string $domain The domain name that was queried + * @param int $type The record type that was queried + * @param array $authoritative Authoritative NS results + * @param array $additional Additional results + * @return \LibDNS\Messages\Message + * @throws \UnexpectedValueException When the packet data is invalid + * @throws \InvalidArgumentException When a type subtype is unknown + */ + public function decode(array $result, string $domain, int $type, array $authoritative = null, array $additional = null): Message + { + $additional = $additional ?? []; + $authoritative = $additional ?? []; + + $packet = $this->packetFactory->create(); + $encodingContext = $this->encodingContextFactory->create($packet, false); + $message = $this->messageFactory->create(); + + //$message->isAuthoritative(true); + $message->setType(MessageTypes::RESPONSE); + //$message->setID($requestId); + $message->setResponseCode(0); + $message->isTruncated(false); + $message->isRecursionDesired(false); + $message->isRecursionAvailable(false); + + $this->encodeQuestionRecord($encodingContext, $domain, $type); + + $expectedAnswers = \count($result); + for ($i = 0; $i < $expectedAnswers; $i++) { + $this->encodeResourceRecord($encodingContext, $result[$i]); + } + + $expectedAuth = \count($authoritative); + for ($i = 0; $i < $expectedAuth; $i++) { + $this->encodeResourceRecord($encodingContext, $authoritative[$i]); + } + + $expectedAdditional = \count($additional); + for ($i = 0; $i < $expectedAdditional; $i++) { + $this->encodeResourceRecord($encodingContext, $additional[$i]); + } + + $header = [ + 'id' => $message->getID(), + 'meta' => 0, + 'qd' => 1, + 'an' => $expectedAnswers, + 'ns' => $expectedAuth, + 'ar' => $expectedAdditional, + ]; + + $header['meta'] |= $message->getType() << 15; + $header['meta'] |= $message->getOpCode() << 11; + $header['meta'] |= ((int) $message->isAuthoritative()) << 10; + $header['meta'] |= ((int) $encodingContext->isTruncated()) << 9; + $header['meta'] |= ((int) $message->isRecursionDesired()) << 8; + $header['meta'] |= ((int) $message->isRecursionAvailable()) << 7; + $header['meta'] |= $message->getResponseCode(); + + $data = \pack('n*', $header['id'], $header['meta'], $header['qd'], $header['an'], $header['ns'], $header['ar']).$packet->read($packet->getLength()); + + return $this->decoderFactory->create()->decode($data); + } +} diff --git a/lib/Native/NativeDecoderFactory.php b/lib/Native/NativeDecoderFactory.php new file mode 100644 index 0000000..e35b04c --- /dev/null +++ b/lib/Native/NativeDecoderFactory.php @@ -0,0 +1,47 @@ +, Chris Wright + * @copyright Copyright (c) Chris Wright , + * @license http://www.opensource.org/licenses/mit-license.html MIT License + */ +namespace Amp\Dns\Native; + +use \LibDNS\Messages\MessageFactory; +use \LibDNS\Packets\PacketFactory; +use \LibDNS\Records\QuestionFactory; +use \LibDNS\Records\RecordCollectionFactory; +use \LibDNS\Records\TypeDefinitions\TypeDefinitionManager; +use \LibDNS\Records\Types\TypeBuilder; +use \LibDNS\Records\Types\TypeFactory; +use LibDNS\Decoder\DecoderFactory; +use LibDNS\Encoder\EncodingContextFactory; + +/** + * Creates NativeDecoder objects. + * + * @author Daniil Gentili , Chris Wright + */ +class NativeDecoderFactory +{ + /** + * Create a new NativeDecoder object. + * + * @param \LibDNS\Records\TypeDefinitions\TypeDefinitionManager $typeDefinitionManager + * @return NativeDecoder + */ + public function create(TypeDefinitionManager $typeDefinitionManager = null): NativeDecoder + { + $typeBuilder = new TypeBuilder(new TypeFactory); + + return new NativeDecoder( + new PacketFactory, + new MessageFactory(new RecordCollectionFactory), + new QuestionFactory, + $typeBuilder, + new EncodingContextFactory, + new DecoderFactory + ); + } +} diff --git a/lib/Native/NativeEncoder.php b/lib/Native/NativeEncoder.php new file mode 100644 index 0000000..36872c1 --- /dev/null +++ b/lib/Native/NativeEncoder.php @@ -0,0 +1,57 @@ +, Chris Wright + * @copyright Copyright (c) Chris Wright + * @license http://www.opensource.org/licenses/mit-license.html MIT License + * @version 2.0.0 + */ +namespace Amp\Dns\Native; + +use \LibDNS\Messages\Message; +use LibDNS\Messages\MessageTypes; + +/** + * Encodes Message objects to query strings. + * + * @category LibDNS + * @package Encoder + * @author Daniil Gentili , Chris Wright + */ +class NativeEncoder +{ + /** + * Encode a Message to URL payload. + * + * @param \LibDNS\Messages\Message $message The Message to encode + * @return array Array of parameters to pass to the \dns_get_record function + */ + public function encode(Message $message): array + { + if ($message->getType() !== MessageTypes::QUERY) { + throw new \InvalidArgumentException('Invalid question: is not a question record'); + } + $questions = $message->getQuestionRecords(); + if ($questions->count() === 0) { + throw new \InvalidArgumentException('Invalid question: 0 question records provided'); + } + if ($questions->count() !== 1) { + throw new \InvalidArgumentException('Invalid question: only one question record can be provided at a time'); + } + + $question = $questions->getRecordByIndex(0); + + return [ + \implode('.', $question->getName()->getLabels()), // Name + $question->getType(), // Type + null, // Authority records + null, // Additional records + true, // Raw results + ]; + } +} diff --git a/lib/Native/NativeEncoderFactory.php b/lib/Native/NativeEncoderFactory.php new file mode 100644 index 0000000..69a33c2 --- /dev/null +++ b/lib/Native/NativeEncoderFactory.php @@ -0,0 +1,34 @@ +, Chris Wright + * @copyright Copyright (c) Chris Wright + * @license http://www.opensource.org/licenses/mit-license.html MIT License + * @version 2.0.0 + */ +namespace Amp\Dns\Native; + +/** + * Creates NativeEncoder objects. + * + * @category LibDNS + * @package Encoder + * @author Daniil Gentili , Chris Wright + */ +class NativeEncoderFactory +{ + /** + * Create a new Encoder object. + * + * @return \LibDNS\Encoder\Encoder + */ + public function create(): NativeEncoder + { + return new NativeEncoder(); + } +} diff --git a/test/BlockingFallbackResolverTest.php b/test/BlockingFallbackResolverTest.php new file mode 100644 index 0000000..e714bbc --- /dev/null +++ b/test/BlockingFallbackResolverTest.php @@ -0,0 +1,45 @@ +expectException(\Error::class); + (new BlockingFallbackResolver)->resolve("abc.de", Record::TXT); + }); + } + + public function testIpAsArgumentWithIPv4Restriction() + { + Loop::run(function () { + $this->expectException(DnsException::class); + yield (new BlockingFallbackResolver)->resolve("::1", Record::A); + }); + } + + public function testIpAsArgumentWithIPv6Restriction() + { + Loop::run(function () { + $this->expectException(DnsException::class); + yield (new BlockingFallbackResolver)->resolve("127.0.0.1", Record::AAAA); + }); + } + + public function testInvalidName() + { + Loop::run(function () { + $this->expectException(InvalidNameException::class); + yield (new BlockingFallbackResolver)->resolve("go@gle.com", Record::A); + }); + } +}