From 73c750ce3f8b312c64bcdf00b8214f075fd626f9 Mon Sep 17 00:00:00 2001 From: samuelwilliams Date: Sun, 8 Mar 2020 08:38:04 +1100 Subject: [PATCH 1/2] Bind resolver feature. --- composer.json | 4 +- example/example.com.db | 38 +++++++++ src/RdataEncoder.php | 5 ++ src/Resolver/ArrayRdata.php | 69 ++++++++++++++++ src/Resolver/BindResolver.php | 65 +++++++++++++++ tests/Resolver/AbstractResolverTest.php | 103 ++++++++++++------------ tests/Resolver/BindResolverTest.php | 64 +++++++++++++++ tests/Resources/example.com-2.db | 38 +++++++++ tests/Resources/example.com.db | 21 +++++ tests/Resources/test2.com.db | 7 ++ 10 files changed, 363 insertions(+), 51 deletions(-) create mode 100644 example/example.com.db create mode 100644 src/Resolver/ArrayRdata.php create mode 100644 src/Resolver/BindResolver.php create mode 100644 tests/Resolver/BindResolverTest.php create mode 100644 tests/Resources/example.com-2.db create mode 100644 tests/Resources/example.com.db create mode 100644 tests/Resources/test2.com.db diff --git a/composer.json b/composer.json index 2cd35b9..132de4d 100644 --- a/composer.json +++ b/composer.json @@ -37,7 +37,9 @@ "psr/log": "^1.0", "symfony/filesystem": "^4.3", "symfony/console": "^4.3", - "vanilla/garden-cli": "^2.2" + "vanilla/garden-cli": "^2.2", + "badcow/dns": "^3.4", + "symfony/property-access": "^5.0" }, "autoload": { "psr-4": { diff --git a/example/example.com.db b/example/example.com.db new file mode 100644 index 0000000..6f6de4d --- /dev/null +++ b/example/example.com.db @@ -0,0 +1,38 @@ +$ORIGIN example.com. +$TTL 1337 +$INCLUDE hq.example.com.txt +@ IN SOA ( + example.com. ; MNAME + post.example.com. ; RNAME + 2014110501 ; SERIAL + 3600 ; REFRESH + 14400 ; RETRY + 604800 ; EXPIRE + 3600 ; MINIMUM + ); This is my Start of Authority Record; AKA SOA. + + ; NS RECORDS +@ NS ns1.nameserver.com. +@ NS ns2.nameserver.com. + +info TXT "This is some additional \"information\"" + + ; A RECORDS +sub.domain A 192.168.1.42 ; This is a local ip. + + ; AAAA RECORDS +ipv6.domain AAAA ::1 ; This is an IPv6 domain. + + ; MX RECORDS +@ MX 10 mail-gw1.example.net. +@ MX 20 mail-gw2.example.net. +@ MX 30 mail-gw3.example.net. + +mail IN TXT "THIS IS SOME TEXT; WITH A SEMICOLON" + +multicast APL ( + 1:192.168.0.0/23 + 2:2001:acad:1::/112 + !1:192.168.1.64/28 + !2:2001:acad:1::8/128 + ) diff --git a/src/RdataEncoder.php b/src/RdataEncoder.php index 994b59e..552e022 100644 --- a/src/RdataEncoder.php +++ b/src/RdataEncoder.php @@ -10,6 +10,7 @@ */ namespace yswery\DNS; +use yswery\DNS\Resolver\ArrayRdata; class RdataEncoder { @@ -36,6 +37,10 @@ class RdataEncoder */ public static function encodeRdata(int $type, $rdata): string { + if ($rdata instanceof ArrayRdata) { + return $rdata->getBadcowRdata()->toWire(); + } + if (!array_key_exists($type, self::$methodMap)) { throw new UnsupportedTypeException(sprintf('Record type "%s" is not a supported type.', RecordTypeEnum::getName($type))); } diff --git a/src/Resolver/ArrayRdata.php b/src/Resolver/ArrayRdata.php new file mode 100644 index 0000000..4ac2eb0 --- /dev/null +++ b/src/Resolver/ArrayRdata.php @@ -0,0 +1,69 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace yswery\DNS\Resolver; + +use ArrayAccess; +use Badcow\DNS\Rdata\RdataInterface; +use Symfony\Component\PropertyAccess\PropertyAccess; +use Symfony\Component\PropertyAccess\PropertyAccessor; + +/** + * Represents Badcow\DNS\Rdata\RdataInterface as an array. + */ +class ArrayRdata implements ArrayAccess +{ + /** + * @var RdataInterface + */ + private $rdata; + + /** + * @var PropertyAccessor + */ + private $propertyAccessor; + + public function __construct(RdataInterface $rdata) + { + $this->rdata = $rdata; + $this->propertyAccessor = PropertyAccess::createPropertyAccessor(); + } + + public function getBadcowRdata(): RdataInterface + { + return $this->rdata; + } + + public function offsetExists($offset): bool + { + return $this->propertyAccessor->isReadable($this->rdata, $offset); + } + + public function offsetGet($offset) + { + return $this->propertyAccessor->getValue($this->rdata, $offset); + } + + public function offsetSet($offset, $value): void + { + $this->propertyAccessor->setValue($this->rdata, $offset, $value); + } + + public function offsetUnset($offset): void + { + $this->propertyAccessor->setValue($this->rdata, $offset, null); + } + + public function __toString() + { + return $this->rdata->toText(); + } +} \ No newline at end of file diff --git a/src/Resolver/BindResolver.php b/src/Resolver/BindResolver.php new file mode 100644 index 0000000..a775367 --- /dev/null +++ b/src/Resolver/BindResolver.php @@ -0,0 +1,65 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace yswery\DNS\Resolver; + +use Badcow\DNS\Parser\ParseException; +use Badcow\DNS\Parser\Parser; +use Badcow\DNS\ResourceRecord as BadcowRR; +use Badcow\DNS\ZoneBuilder; +use yswery\DNS\ResourceRecord; + +class BindResolver extends AbstractResolver +{ + /** + * BindResolver constructor. + * + * @param array $files + * @throws ParseException + */ + public function __construct(array $files) + { + $this->isAuthoritative = true; + $this->allowRecursion = false; + + $resourceRecords = []; + + foreach ($files as $file) { + $fileContents = file_get_contents($file); + $zone = Parser::parse('.', $fileContents); + ZoneBuilder::fillOutZone($zone); + + foreach ($zone as $rr) { + $resourceRecords[] = self::convertResourceRecord($rr); + } + } + + $this->addZone($resourceRecords); + } + + /** + * Converts Badcow\DNS\ResourceRecord object to yswery\DNS\ResourceRecord object. + * + * @return ResourceRecord + */ + public static function convertResourceRecord(BadcowRR $badcowResourceRecord, bool $isQuestion = false): ResourceRecord + { + $rr = new ResourceRecord(); + $rr->setName($badcowResourceRecord->getName()); + $rr->setClass($badcowResourceRecord->getClassId()); + $rr->setRdata(new ArrayRdata($badcowResourceRecord->getRdata())); + $rr->setTtl($badcowResourceRecord->getTtl()); + $rr->setType($badcowResourceRecord->getRdata()->getTypeCode()); + $rr->setQuestion($isQuestion); + + return $rr; + } +} \ No newline at end of file diff --git a/tests/Resolver/AbstractResolverTest.php b/tests/Resolver/AbstractResolverTest.php index 739ae0a..8ed17bd 100644 --- a/tests/Resolver/AbstractResolverTest.php +++ b/tests/Resolver/AbstractResolverTest.php @@ -27,44 +27,39 @@ abstract class AbstractResolverTest extends TestCase public function testGetAnswer() { - $soa = (new ResourceRecord()) - ->setName('example.com.') - ->setClass(ClassEnum::INTERNET) - ->setTtl(10800) - ->setType(RecordTypeEnum::TYPE_SOA) - ->setRdata([ - 'mname' => 'example.com.', - 'rname' => 'postmaster.example.com.', - 'serial' => 2, - 'refresh' => 3600, - 'retry' => 7200, - 'expire' => 10800, - 'minimum' => 3600, - ]); - - $aaaa = (new ResourceRecord()) - ->setName('example.com.') - ->setClass(ClassEnum::INTERNET) - ->setTtl(7200) - ->setType(RecordTypeEnum::TYPE_AAAA) - ->setRdata('2001:acad:ad::32'); - - $soa_query = (new ResourceRecord()) + $query[] = (new ResourceRecord()) ->setName('example.com.') ->setType(RecordTypeEnum::TYPE_SOA) ->setClass(ClassEnum::INTERNET) ->setQuestion(true); - $aaaa_query = (new ResourceRecord()) + $query[] = (new ResourceRecord()) ->setName('example.com.') ->setType(RecordTypeEnum::TYPE_AAAA) ->setClass(ClassEnum::INTERNET) ->setQuestion(true); - $query = [$soa_query, $aaaa_query]; - $answer = [$soa, $aaaa]; - - $this->assertEquals($answer, $this->resolver->getAnswer($query)); + $answer = $this->resolver->getAnswer($query); + $this->assertCount(2, $answer); + list($soa, $aaaa) = $answer; + + $this->assertEquals('example.com.', $soa->getName()); + $this->assertEquals(ClassEnum::INTERNET, $soa->getClass()); + $this->assertEquals(10800, $soa->getTtl()); + $this->assertEquals(RecordTypeEnum::TYPE_SOA, $soa->getType()); + $this->assertEquals('example.com.', $soa->getRdata()['mname']); + $this->assertEquals('postmaster.example.com.', $soa->getRdata()['rname']); + $this->assertEquals(2, $soa->getRdata()['serial']); + $this->assertEquals(3600, $soa->getRdata()['refresh']); + $this->assertEquals(7200, $soa->getRdata()['retry']); + $this->assertEquals(10800, $soa->getRdata()['expire']); + $this->assertEquals(3600, $soa->getRdata()['minimum']); + + $this->assertEquals('example.com.', $aaaa->getName()); + $this->assertEquals(ClassEnum::INTERNET, $aaaa->getClass()); + $this->assertEquals(7200, $aaaa->getTtl()); + $this->assertEquals(RecordTypeEnum::TYPE_AAAA, $aaaa->getType()); + $this->assertEquals(inet_pton('2001:acad:ad::32'), inet_pton($aaaa->getRdata())); } public function testUnconfiguredRecordDoesNotResolve() @@ -84,19 +79,16 @@ public function testHostRecordReturnsArray() ->setType(RecordTypeEnum::TYPE_A) ->setQuestion(true); - $expectation[] = (new ResourceRecord()) - ->setName('test2.com.') - ->setType(RecordTypeEnum::TYPE_A) - ->setTtl(300) - ->setRdata('111.111.111.111'); - - $expectation[] = (new ResourceRecord()) - ->setName('test2.com.') - ->setType(RecordTypeEnum::TYPE_A) - ->setTtl(300) - ->setRdata('112.112.112.112'); - - $this->assertEquals($expectation, $this->resolver->getAnswer($question)); + $answer = $this->resolver->getAnswer($question); + $this->assertCount(2, $answer); + $this->assertEquals('test2.com.', $answer[0]->getName()); + $this->assertEquals(RecordTypeEnum::TYPE_A, $answer[0]->getType()); + $this->assertEquals('111.111.111.111', (string) $answer[0]->getRdata()); + $this->assertEquals(300, $answer[0]->getTtl()); + $this->assertEquals('test2.com.', $answer[1]->getName()); + $this->assertEquals(RecordTypeEnum::TYPE_A, $answer[1]->getType()); + $this->assertEquals('112.112.112.112', (string) $answer[1]->getRdata()); + $this->assertEquals(300, $answer[1]->getTtl()); } public function testWildcardDomains() @@ -106,13 +98,12 @@ public function testWildcardDomains() ->setType(RecordTypeEnum::TYPE_A) ->setQuestion(true); - $expectation[] = (new ResourceRecord()) - ->setName('badcow.subdomain.example.com.') - ->setType(RecordTypeEnum::TYPE_A) - ->setTtl(7200) - ->setRdata('192.168.1.42'); - - $this->assertEquals($expectation, $this->resolver->getAnswer($question)); + $answer = $this->resolver->getAnswer($question); + $this->assertCount(1, $answer); + $this->assertEquals('badcow.subdomain.example.com.', $answer[0]->getName()); + $this->assertEquals(1, $answer[0]->getType()); + $this->assertEquals('192.168.1.42', (string) $answer[0]->getRdata()); + $this->assertEquals(7200, $answer[0]->getTtl()); } /** @@ -137,7 +128,7 @@ public function testIsAuthority() public function testSrvRdata() { - $question[] = (new ResourceRecord()) + $query[] = (new ResourceRecord()) ->setName('_ldap._tcp.example.com.') ->setType(RecordTypeEnum::TYPE_SRV) ->setQuestion(true); @@ -153,6 +144,18 @@ public function testSrvRdata() 'target' => 'ldap.example.com.', ]); - $this->assertEquals($expectation, $this->resolver->getAnswer($question)); + $answer = $this->resolver->getAnswer($query); + $this->assertCount(1, $answer); + + $srv = $answer[0]; + $this->assertEquals('_ldap._tcp.example.com.', $srv->getName()); + $this->assertEquals(ClassEnum::INTERNET, $srv->getClass()); + $this->assertEquals(7200, $srv->getTtl()); + $this->assertEquals(RecordTypeEnum::TYPE_SRV, $srv->getType()); + + $this->assertEquals(1, $srv->getRdata()['priority']); + $this->assertEquals(5, $srv->getRdata()['weight']); + $this->assertEquals(389, $srv->getRdata()['port']); + $this->assertEquals('ldap.example.com.', $srv->getRdata()['target']); } } diff --git a/tests/Resolver/BindResolverTest.php b/tests/Resolver/BindResolverTest.php new file mode 100644 index 0000000..8ce29cd --- /dev/null +++ b/tests/Resolver/BindResolverTest.php @@ -0,0 +1,64 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace yswery\DNS\Tests\Resolver; + +use Badcow\DNS\Parser\ParseException; +use yswery\DNS\RdataEncoder; +use yswery\DNS\RecordTypeEnum; +use yswery\DNS\Resolver\BindResolver; +use yswery\DNS\ResourceRecord; +use yswery\DNS\UnsupportedTypeException; + +class BindResolverTest extends AbstractResolverTest +{ + /** + * @throws ParseException + */ + public function setUp() + { + $files = [ + __DIR__.'/../Resources/example.com.db', + __DIR__.'/../Resources/test2.com.db', + ]; + $this->resolver = new BindResolver($files); + } + + /** + * @throws ParseException + * @throws UnsupportedTypeException + */ + public function testResolver() + { + $files = [__DIR__.'/../Resources/example.com-2.db']; + $resolver = new BindResolver($files); + + $query = new ResourceRecord(); + $query->setQuestion(true); + $query->setType(2); + $query->setName('example.com.'); + + $nsAnswer = $resolver->getAnswer([$query]); + $this->assertCount(2, $nsAnswer); + $this->assertEquals('ns2.nameserver.com.', (string) $nsAnswer[1]->getRdata()); + $this->assertEquals(chr(3).'ns2'.chr(10).'nameserver'.chr(3).'com'.chr(0), RdataEncoder::encodeRdata(2, $nsAnswer[1]->getRdata())); + + $query = new ResourceRecord(); + $query->setQuestion(true); + $query->setType(RecordTypeEnum::TYPE_AAAA); + $query->setName('ipv6.domain.example.com.'); + + $ipv6 = $resolver->getAnswer([$query]); + $this->assertCount(1, $ipv6); + $this->assertEquals('0000:0000:0000:0000:0000:0000:0000:0001', (string) $ipv6[0]->getRdata()); + $this->assertEquals(inet_pton('::1'), RdataEncoder::encodeRdata(28, $ipv6[0]->getRdata())); + } +} diff --git a/tests/Resources/example.com-2.db b/tests/Resources/example.com-2.db new file mode 100644 index 0000000..6f6de4d --- /dev/null +++ b/tests/Resources/example.com-2.db @@ -0,0 +1,38 @@ +$ORIGIN example.com. +$TTL 1337 +$INCLUDE hq.example.com.txt +@ IN SOA ( + example.com. ; MNAME + post.example.com. ; RNAME + 2014110501 ; SERIAL + 3600 ; REFRESH + 14400 ; RETRY + 604800 ; EXPIRE + 3600 ; MINIMUM + ); This is my Start of Authority Record; AKA SOA. + + ; NS RECORDS +@ NS ns1.nameserver.com. +@ NS ns2.nameserver.com. + +info TXT "This is some additional \"information\"" + + ; A RECORDS +sub.domain A 192.168.1.42 ; This is a local ip. + + ; AAAA RECORDS +ipv6.domain AAAA ::1 ; This is an IPv6 domain. + + ; MX RECORDS +@ MX 10 mail-gw1.example.net. +@ MX 20 mail-gw2.example.net. +@ MX 30 mail-gw3.example.net. + +mail IN TXT "THIS IS SOME TEXT; WITH A SEMICOLON" + +multicast APL ( + 1:192.168.0.0/23 + 2:2001:acad:1::/112 + !1:192.168.1.64/28 + !2:2001:acad:1::8/128 + ) diff --git a/tests/Resources/example.com.db b/tests/Resources/example.com.db new file mode 100644 index 0000000..d49179b --- /dev/null +++ b/tests/Resources/example.com.db @@ -0,0 +1,21 @@ +$origin example.com. +$ttl 7200 + +@ IN 10800 SOA example.com. postmaster 2 3600 7200 10800 3600 +@ IN a 12.34.56.78 +@ IN ns ns1.test.com. +@ IN ns ns2.test.com. +@ IN a 90.12.34.56 +@ IN aaaa 2001:acad:ad::32 + +mail-gw1 IN aaaa 2001:acad:ad::64 +mail-gw2 IN aaaa 2001:acad:ad::92 + +www cname @ +@ MX 15 mail-gw1 +@ MX 20 mail-gw2 + +ldap a 192.168.3.89 +*.subdomain IN a 192.168.1.42 + +_ldap._tcp SRV 1 5 389 ldap.example.com. diff --git a/tests/Resources/test2.com.db b/tests/Resources/test2.com.db new file mode 100644 index 0000000..0512b47 --- /dev/null +++ b/tests/Resources/test2.com.db @@ -0,0 +1,7 @@ +$origin test2.com. +$ttl 300 + +@ IN A 111.111.111.111 +@ IN A 112.112.112.112 +@ IN MX 20 mail-gw1.test2.com. +@ IN MX 30 mail-gw2.test2.com. From 7fd09045b6f5bef5c1b8efcad4c8382e7aea4bb7 Mon Sep 17 00:00:00 2001 From: samuelwilliams Date: Sun, 8 Mar 2020 08:39:48 +1100 Subject: [PATCH 2/2] CS fix. --- src/RdataEncoder.php | 1 + src/Resolver/ArrayRdata.php | 2 +- src/Resolver/BindResolver.php | 3 ++- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/RdataEncoder.php b/src/RdataEncoder.php index 552e022..519701b 100644 --- a/src/RdataEncoder.php +++ b/src/RdataEncoder.php @@ -10,6 +10,7 @@ */ namespace yswery\DNS; + use yswery\DNS\Resolver\ArrayRdata; class RdataEncoder diff --git a/src/Resolver/ArrayRdata.php b/src/Resolver/ArrayRdata.php index 4ac2eb0..76e5ccd 100644 --- a/src/Resolver/ArrayRdata.php +++ b/src/Resolver/ArrayRdata.php @@ -66,4 +66,4 @@ public function __toString() { return $this->rdata->toText(); } -} \ No newline at end of file +} diff --git a/src/Resolver/BindResolver.php b/src/Resolver/BindResolver.php index a775367..e329467 100644 --- a/src/Resolver/BindResolver.php +++ b/src/Resolver/BindResolver.php @@ -23,6 +23,7 @@ class BindResolver extends AbstractResolver * BindResolver constructor. * * @param array $files + * * @throws ParseException */ public function __construct(array $files) @@ -62,4 +63,4 @@ public static function convertResourceRecord(BadcowRR $badcowResourceRecord, boo return $rr; } -} \ No newline at end of file +}