├── .gitignore ├── tests ├── Parser │ ├── Resources │ │ ├── testMultilineTxtRecords_expectation.txt │ │ ├── testKeepCommentsWithoutLinefeedAtEnd_expectation.txt │ │ ├── testKeepCommentsWithoutLinefeedAtEnd_sample.txt │ │ ├── testMultilineTxtRecords_sample.txt │ │ ├── ambiguous.acme.org.txt │ │ ├── IncludeControlEntryTests │ │ │ ├── _subdomain.mydomain.biz.db │ │ │ ├── mydomain.biz.db │ │ │ ├── testdomain.geek_expectation.db │ │ │ ├── testdomain.geek - extra info.db │ │ │ ├── mydomain.biz_expectation.db │ │ │ └── testdomain.geek.db │ │ ├── testClearComments_expectation.txt │ │ ├── testKeepComments_expectation.txt │ │ ├── testOriginDot_sample.txt │ │ ├── multipleOrigins.txt │ │ ├── testCollapseMultilines_expectation.txt │ │ ├── testOriginDot_expectation.txt │ │ ├── multipleOrigins_expectation.txt │ │ ├── testClearComments_sample.txt │ │ ├── testCollapseMultilinesWithComments_expectation.txt │ │ ├── 50.100.200.in-addr.arpa.db │ │ ├── testCollapseMultilines_sample.txt │ │ └── testConvolutedZone_sample.txt │ ├── CustomHandlerTest.php │ ├── ResourceRecordIteratorTest.php │ ├── ParseUnknownTypesTest.php │ └── ReverseRecordTest.php ├── Resources │ ├── wire │ │ ├── wire_test.data0 │ │ ├── wire_test.data8 │ │ ├── wire_test.data7 │ │ ├── wire_test.data2 │ │ ├── wire_test.data3 │ │ ├── wire_test.data1 │ │ └── wire_test.data5 │ ├── google.com.cer │ └── example.com_filled-out.txt ├── Rdata │ ├── RdataTraitTest.php │ ├── TypesTest.php │ ├── CdnskeyTest.php │ ├── FactoryTest.php │ ├── NsTest.php │ ├── UnknownTypeTest.php │ ├── CnameTest.php │ ├── DnameTest.php │ ├── SigTest.php │ ├── SpfTest.php │ ├── RpTest.php │ ├── CdsTest.php │ ├── DlvTest.php │ ├── OptTest.php │ ├── TaTest.php │ ├── MxTest.php │ ├── HinfoTest.php │ ├── AfsdbTest.php │ ├── PtrTest.php │ ├── KxTest.php │ ├── SrvTest.php │ ├── NsecTest.php │ ├── DsTest.php │ ├── KeyTest.php │ ├── Nsec3paramTest.php │ ├── TxtTest.php │ ├── ATest.php │ └── DnskeyTest.php ├── ZoneBuilderTest.php ├── Edns │ └── Option │ │ ├── CodesTest.php │ │ ├── FactoryTest.php │ │ ├── UnknownOptionTest.php │ │ ├── CLIENT_SUBNETTest.php │ │ ├── TCP_KEEPALIVETest.php │ │ └── COOKIETest.php ├── AlgorithmsTest.php ├── ClassesTest.php └── QuestionTest.php ├── phpstan.neon ├── lib ├── UnsetValueException.php ├── Rdata │ ├── SIG.php │ ├── CDS.php │ ├── SPF.php │ ├── TA.php │ ├── NS.php │ ├── CDNSKEY.php │ ├── DLV.php │ ├── UnsupportedTypeException.php │ ├── DNAME.php │ ├── RdataTrait.php │ ├── DecodeException.php │ ├── DNSKEY.php │ ├── AAAA.php │ ├── CNAME.php │ ├── RdataInterface.php │ ├── RP.php │ ├── A.php │ ├── PolymorphicRdata.php │ ├── AFSDB.php │ ├── HINFO.php │ ├── UnknownType.php │ ├── KX.php │ ├── MX.php │ ├── CSYNC.php │ └── OPT.php ├── Edns │ └── Option │ │ ├── UnsupportedOptionException.php │ │ ├── OptionTrait.php │ │ ├── DecodeException.php │ │ ├── OptionInterface.php │ │ ├── TCP_KEEPALIVE.php │ │ ├── UnknownOption.php │ │ ├── Factory.php │ │ ├── COOKIE.php │ │ └── Codes.php ├── Parser │ ├── Tokens.php │ ├── ZoneFileFetcherInterface.php │ ├── Comments.php │ ├── ParseException.php │ ├── ResourceRecordIterator.php │ ├── StringIterator.php │ └── TimeFormat.php ├── Opcode.php ├── Classes.php └── Rcode.php ├── phpunit.xml.dist ├── composer.json ├── .php-cs-fixer.dist.php ├── .github └── workflows │ └── PHP-8.yml ├── LICENSE └── docs ├── Parser ├── Custom-Rdata-Handlers.md ├── Basic-Usage.md ├── Include-Directive.md └── Unkown-Types.md ├── Validation.md ├── Message.md ├── Reverse-Records.md └── Zone.md /.gitignore: -------------------------------------------------------------------------------- 1 | vendor 2 | phpunit.xml 3 | composer.lock 4 | coverage.clover 5 | *.cache 6 | -------------------------------------------------------------------------------- /tests/Parser/Resources/testMultilineTxtRecords_expectation.txt: -------------------------------------------------------------------------------- 1 | test IN 7230 TXT "This is an " "example of a " "multiline TXT " "record.";This is a comment. -------------------------------------------------------------------------------- /tests/Parser/Resources/testKeepCommentsWithoutLinefeedAtEnd_expectation.txt: -------------------------------------------------------------------------------- 1 | $ORIGIN example.com. 2 | $TTL 3600 3 | sub.domain A 192.168.1.42;This is a local ip. -------------------------------------------------------------------------------- /phpstan.neon: -------------------------------------------------------------------------------- 1 | parameters: 2 | level: max 3 | checkMissingIterableValueType: false 4 | checkGenericClassInNonGenericObjectType: false 5 | paths: 6 | - lib/ -------------------------------------------------------------------------------- /tests/Parser/Resources/testKeepCommentsWithoutLinefeedAtEnd_sample.txt: -------------------------------------------------------------------------------- 1 | $ORIGIN example.com. 2 | $TTL 3600 3 | 4 | ; A RECORDS 5 | sub.domain A 192.168.1.42 ; This is a local ip. -------------------------------------------------------------------------------- /tests/Parser/Resources/testMultilineTxtRecords_sample.txt: -------------------------------------------------------------------------------- 1 | 2 | 3 | test IN 7230 TXT "This is an "( 4 | 5 | "example of a " 6 | "multiline TXT " 7 | )"record." ;This is a comment. 8 | 9 | -------------------------------------------------------------------------------- /tests/Parser/Resources/ambiguous.acme.org.txt: -------------------------------------------------------------------------------- 1 | $ORIGIN ambiguous.acme.org. 2 | 3 | ambiguous.acme.org. IN SOA ambiguous.acme.org. post.acme.org. 2019092401 3600 14400 604800 1800 4 | 5 | mx 900 IN A 200.100.50.35 6 | aaaa IN AAAA 2001:acdc:5889::35 7 | 3600 TXT "This name is silly." 8 | mx cname aaaa 9 | IN TXT "Mail Exchange IPv6 Address" 10 | -------------------------------------------------------------------------------- /tests/Parser/Resources/IncludeControlEntryTests/_subdomain.mydomain.biz.db: -------------------------------------------------------------------------------- 1 | $TTL 7200 2 | 3 | @ MX ( 4 | 10 5 | mail-gw1.example.net. 6 | ) ;The first mail gateway 7 | @ MX 20 mail-gw2.example.net. 8 | @ MX 30 mail-gw3.example.net. 9 | mail IN TXT "THIS IS SOME TEXT; WITH A SEMICOLON" 10 | 11 | $ORIGIN some_other_subdomain.mydomain.biz. 12 | -------------------------------------------------------------------------------- /tests/Resources/wire/wire_test.data0: -------------------------------------------------------------------------------- 1 | # MESSAGE HEADER 2 | 0x00 0x2a #id=42 3 | 0x00 # QR=0 (query) OPCODE=0 AA=0 TC=0 RD=0 4 | 0x00 # RA=0 BIT9=0 AD=0 CD=0 RCODE=0 5 | 0x00 0x01 #QDCOUNT 6 | 0x00 0x00 #ANCOUNT 7 | 0x00 0x00 #NSCOUNT 8 | 0x00 0x00 #ARCOUNT 9 | 10 | # QUESTION SECTION 11 | 0x03 0x66 0x6f 0x6f 0x03 0x62 0x61 0x72 0x03 0x63 0x6f 0x6d 0x00 0x00 0x01 0x00 0x01 # foo.bar.com. A IN 12 | 13 | -------------------------------------------------------------------------------- /tests/Parser/Resources/testClearComments_expectation.txt: -------------------------------------------------------------------------------- 1 | $ORIGIN example.com. 2 | $TTL 3600 3 | @ NS ns1.nameserver.com. 4 | @ NS ns2.nameserver.com. 5 | info TXT "This is some additional \"information\"" 6 | sub.domain A 192.168.1.42 7 | ipv6.domain AAAA ::1 8 | @ MX 10 mail-gw1.example.net. 9 | @ MX 20 mail-gw2.example.net. 10 | @ MX 30 mail-gw3.example.net. 11 | mail IN TXT "THIS IS SOME TEXT; WITH A SEMICOLON" -------------------------------------------------------------------------------- /lib/UnsetValueException.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace Badcow\DNS; 15 | 16 | class UnsetValueException extends \Exception 17 | { 18 | } 19 | -------------------------------------------------------------------------------- /tests/Resources/wire/wire_test.data8: -------------------------------------------------------------------------------- 1 | # HEADER SECTION 2 | 30 99 # ID 3 | 01 20 # FLAGS 4 | 00 01 # QDCOUNT=1 5 | 00 00 # ANCOUNT=0 6 | 00 00 # NSCOUNT=0 7 | 00 01 # ARCOUNT=1 8 | 9 | #QUESTION SECTION 10 | 05 67 6f 6f 67 65 03 63 6f 6d 00 00 01 00 01 # question section: google.com IN A 11 | 12 | #ADDITIONAL RECORDS 13 | 00 00 29 10 00 00 00 00 00 00 1c 00 08 00 08 00 01 20 00 c0 a8 01 01 00 0a 00 08 a8 51 73 83 92 4c a2 1e 00 0b 00 00 14 | -------------------------------------------------------------------------------- /tests/Resources/wire/wire_test.data7: -------------------------------------------------------------------------------- 1 | # HEADER SECTION 2 | 0xcf 0xcf # ID 3 | 0x01 0x20 # FLAGS 4 | 0x00 0x01 # QDCOUNT=1 5 | 0x00 0x00 # ANCOUNT=0 6 | 0x00 0x00 # NSCOUNT=0 7 | 0x00 0x01 # ARCOUNT=1 8 | 9 | #QUESTION SECTION 10 | 0x09 0x6a 0x65 0x79 0x73 0x65 0x72 0x76 0x65 0x72 0x03 0x63 0x6f 0x6d 0x00 11 | 12 | #ADDITIONAL RECORDS 13 | 0x00 0x01 0x00 0x01 0x00 0x01 0x29 0x10 0x00 0x00 0x00 0x00 0x00 0x00 0x0c 0x00 0x0a 0x00 0x08 0x29 0x22 0xfd 0x1c 0xe5 0xc0 0x21 0xd1 14 | -------------------------------------------------------------------------------- /lib/Rdata/SIG.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace Badcow\DNS\Rdata; 15 | 16 | class SIG extends RRSIG 17 | { 18 | public const TYPE = 'SIG'; 19 | public const TYPE_CODE = 24; 20 | } 21 | -------------------------------------------------------------------------------- /tests/Parser/Resources/testKeepComments_expectation.txt: -------------------------------------------------------------------------------- 1 | $ORIGIN example.com. 2 | $TTL 3600 3 | @ NS ns1.nameserver.com. 4 | @ NS ns2.nameserver.com. 5 | info TXT "This is some additional \"information\"" 6 | sub.domain A 192.168.1.42;This is a local ip. 7 | ipv6.domain AAAA ::1;This is an IPv6 domain. 8 | @ MX 10 mail-gw1.example.net.;This is the primary sever; and some more stuff. 9 | @ MX 20 mail-gw2.example.net. 10 | @ MX 30 mail-gw3.example.net. 11 | mail IN TXT "THIS IS SOME TEXT; WITH A SEMICOLON" -------------------------------------------------------------------------------- /tests/Parser/Resources/testOriginDot_sample.txt: -------------------------------------------------------------------------------- 1 | $ORIGIN . 2 | $TTL 3600 3 | 4 | otherdomain.biz IN SOA otherdomain.biz. post.otherdomain.biz. 2014110501 3600 14400 604800 3600 5 | 6 | otherdomain.biz NS ns1.nameserver.com. 7 | otherdomain.biz NS ns2.nameserver.com. 8 | 9 | info.otherdomain.biz TXT "This is some additional \"information\"" 10 | sub.domain.otherdomain.biz A 192.168.1.42 11 | ipv6.domain.otherdomain.biz AAAA ::1 12 | 13 | mail.otherdomain.biz IN CNAME mx1.bizmail. 14 | otherdomain.biz IN MX 10 mail 15 | -------------------------------------------------------------------------------- /tests/Parser/Resources/IncludeControlEntryTests/mydomain.biz.db: -------------------------------------------------------------------------------- 1 | $ORIGIN mydomain.biz. 2 | $TTL 3600 3 | 4 | @ IN SOA mydomain.biz. post.mydomain.biz. 2014110501 3600 14400 604800 3600 5 | 6 | @ NS ns1.nameserver.com. 7 | @ NS ns2.nameserver.com. 8 | 9 | info TXT "This is some additional \"information\"" 10 | sub.domain A 192.168.1.42 11 | ipv6.domain AAAA ::1 12 | 13 | $INCLUDE _subdomain.mydomain.biz.db _subdomain.mydomain.biz. ;include the subdomain _subdomain.mydomain.biz. 14 | 15 | mail IN CNAME mx1.bizmail. 16 | @ IN MX 10 mail -------------------------------------------------------------------------------- /tests/Parser/Resources/multipleOrigins.txt: -------------------------------------------------------------------------------- 1 | $ORIGIN mydomain.biz. 2 | $TTL 3600 3 | 4 | @ IN SOA mydomain.biz. post.mydomain.biz. 2014110501 3600 14400 604800 3600 5 | 6 | @ NS ns1.nameserver.com. 7 | @ NS ns2.nameserver.com. 8 | 9 | info TXT "This is some additional \"information\"" 10 | sub.domain A 192.168.1.42 11 | ipv6.domain AAAA ::1 12 | 13 | $ORIGIN _subdomain.mydomain.biz. 14 | 15 | @ MX 10 mail-gw1.example.net. 16 | @ MX 20 mail-gw2.example.net. 17 | @ MX 30 mail-gw3.example.net. 18 | mail IN TXT "THIS IS SOME TEXT; WITH A SEMICOLON" -------------------------------------------------------------------------------- /lib/Rdata/CDS.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace Badcow\DNS\Rdata; 15 | 16 | /** 17 | * {@link https://tools.ietf.org/html/rfc7344}. 18 | */ 19 | class CDS extends DS 20 | { 21 | public const TYPE = 'CDS'; 22 | public const TYPE_CODE = 59; 23 | } 24 | -------------------------------------------------------------------------------- /lib/Rdata/SPF.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace Badcow\DNS\Rdata; 15 | 16 | /** 17 | * {@link https://tools.ietf.org/html/rfc7208}. 18 | */ 19 | class SPF extends TXT 20 | { 21 | public const TYPE = 'SPF'; 22 | public const TYPE_CODE = 99; 23 | } 24 | -------------------------------------------------------------------------------- /lib/Rdata/TA.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace Badcow\DNS\Rdata; 15 | 16 | /** 17 | * {@link http://www.watson.org/~weiler/INI1999-19.pdf}. 18 | */ 19 | class TA extends DS 20 | { 21 | public const TYPE = 'TA'; 22 | public const TYPE_CODE = 32768; 23 | } 24 | -------------------------------------------------------------------------------- /lib/Rdata/NS.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace Badcow\DNS\Rdata; 15 | 16 | /** 17 | * @see https://tools.ietf.org/html/rfc1035#section-3.3.11 18 | */ 19 | class NS extends CNAME 20 | { 21 | public const TYPE = 'NS'; 22 | public const TYPE_CODE = 2; 23 | } 24 | -------------------------------------------------------------------------------- /lib/Rdata/CDNSKEY.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace Badcow\DNS\Rdata; 15 | 16 | /** 17 | * {@link https://tools.ietf.org/html/rfc7344#page-7}. 18 | */ 19 | class CDNSKEY extends DNSKEY 20 | { 21 | public const TYPE = 'CDNSKEY'; 22 | public const TYPE_CODE = 60; 23 | } 24 | -------------------------------------------------------------------------------- /lib/Rdata/DLV.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace Badcow\DNS\Rdata; 15 | 16 | /** 17 | * {@link https://tools.ietf.org/html/rfc4431}. 18 | */ 19 | class DLV extends DS 20 | { 21 | use RdataTrait; 22 | 23 | public const TYPE = 'DLV'; 24 | public const TYPE_CODE = 32769; 25 | } 26 | -------------------------------------------------------------------------------- /lib/Rdata/UnsupportedTypeException.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace Badcow\DNS\Rdata; 15 | 16 | class UnsupportedTypeException extends \Exception 17 | { 18 | public function __construct(string $type) 19 | { 20 | parent::__construct(sprintf('Rdata "%s" is not implemented.', $type)); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /tests/Parser/Resources/testCollapseMultilines_expectation.txt: -------------------------------------------------------------------------------- 1 | $ORIGIN example.com. 2 | $TTL 1337 3 | $INCLUDE hq.example.com.txt 4 | @ IN SOA example.com. post.example.com. 2014110501 3600 14400 604800 3600 5 | @ NS ns1.nameserver.com. 6 | @ NS ns2.nameserver.com. 7 | info TXT "This is some additional \"information\"" 8 | sub.domain A 192.168.1.42 9 | ipv6.domain AAAA ::1 10 | @ MX 10 mail-gw1.example.net. 11 | @ MX 20 mail-gw2.example.net. 12 | @ MX 30 mail-gw3.example.net. 13 | mail IN TXT "THIS IS SOME TEXT; WITH A SEMICOLON" 14 | 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 -------------------------------------------------------------------------------- /lib/Edns/Option/UnsupportedOptionException.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace Badcow\DNS\Edns\Option; 15 | 16 | class UnsupportedOptionException extends \Exception 17 | { 18 | public function __construct(string $option) 19 | { 20 | parent::__construct(sprintf('Option "%s" is not implemented.', $option)); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /tests/Parser/Resources/testOriginDot_expectation.txt: -------------------------------------------------------------------------------- 1 | $ORIGIN otherdomain.biz. 2 | $TTL 3600 3 | otherdomain.biz. 3600 IN SOA otherdomain.biz. post.otherdomain.biz. 2014110501 3600 14400 604800 3600 4 | otherdomain.biz. 3600 IN NS ns1.nameserver.com. 5 | otherdomain.biz. 3600 IN NS ns2.nameserver.com. 6 | info.otherdomain.biz. 3600 IN TXT "This is some additional \"information\"" 7 | sub.domain.otherdomain.biz. 3600 IN A 192.168.1.42 8 | ipv6.domain.otherdomain.biz. 3600 IN AAAA 0000:0000:0000:0000:0000:0000:0000:0001 9 | mail.otherdomain.biz. 3600 IN CNAME mx1.bizmail. 10 | otherdomain.biz. 3600 IN MX 10 mail.otherdomain.biz. 11 | -------------------------------------------------------------------------------- /tests/Parser/Resources/multipleOrigins_expectation.txt: -------------------------------------------------------------------------------- 1 | $ORIGIN mydomain.biz. 2 | $TTL 3600 3 | @ 3600 IN SOA mydomain.biz. post.mydomain.biz. 2014110501 3600 14400 604800 3600 4 | @ 3600 IN NS ns1.nameserver.com. 5 | @ 3600 IN NS ns2.nameserver.com. 6 | info 3600 IN TXT "This is some additional \"information\"" 7 | sub.domain 3600 IN A 192.168.1.42 8 | ipv6.domain 3600 IN AAAA ::1 9 | _subdomain.mydomain.biz. 3600 IN MX 10 mail-gw1.example.net. 10 | _subdomain.mydomain.biz. 3600 IN MX 20 mail-gw2.example.net. 11 | _subdomain.mydomain.biz. 3600 IN MX 30 mail-gw3.example.net. 12 | mail._subdomain.mydomain.biz. 3600 IN TXT "THIS IS SOME TEXT; WITH A SEMICOLON" 13 | -------------------------------------------------------------------------------- /lib/Edns/Option/OptionTrait.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace Badcow\DNS\Edns\Option; 15 | 16 | trait OptionTrait 17 | { 18 | public function getName(): string 19 | { 20 | /* @const NAME */ 21 | return static::NAME; 22 | } 23 | 24 | public function getCode(): int 25 | { 26 | /* @const TYPE_CODE */ 27 | return static::CODE; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /tests/Parser/Resources/IncludeControlEntryTests/testdomain.geek_expectation.db: -------------------------------------------------------------------------------- 1 | $ORIGIN testdomain.geek. 2 | $TTL 7200 3 | @ 1337 IN SOA testdomain.geek. post.testdomain.geek. 2014110501 3600 14400 604800 3600 4 | @ 1337 IN NS ns1.nameserver.com. 5 | @ 1337 IN NS ns2.nameserver.com. 6 | info 7200 IN TXT "This is some additional \"information\"" 7 | sub.domain 7200 IN A 192.168.1.42 8 | ipv6.domain 7200 IN AAAA ::1 9 | @ 7200 IN MX 10 mail-gw1.example.net. 10 | @ 7200 IN MX 20 mail-gw2.example.net. 11 | @ 7200 IN MX 30 mail-gw3.example.net. 12 | mail 7200 IN TXT "THIS IS SOME TEXT; WITH A SEMICOLON" 13 | multicast 7200 IN APL 1:192.168.0.0/23 2:2001:acad:1::/112 !1:192.168.1.64/28 !2:2001:acad:1::8/128 14 | -------------------------------------------------------------------------------- /tests/Parser/Resources/IncludeControlEntryTests/testdomain.geek - extra info.db: -------------------------------------------------------------------------------- 1 | $ORIGIN testdomain.geek. 2 | $TTL 1337 3 | @ IN SOA ( 4 | testdomain.geek. ; MNAME 5 | post.testdomain.geek. ; RNAME 6 | 2014110501 ; SERIAL 7 | 3600 ; REFRESH 8 | 14400 ; RETRY 9 | 604800 ; EXPIRE 10 | 3600 ; MINIMUM 11 | ); This is my Start of Authority Record; AKA SOA. 12 | 13 | ; NS RECORDS 14 | @ NS ns1.nameserver.com. 15 | @ NS ns2.nameserver.com. 16 | -------------------------------------------------------------------------------- /tests/Parser/Resources/testClearComments_sample.txt: -------------------------------------------------------------------------------- 1 | $ORIGIN example.com. 2 | $TTL 3600 3 | 4 | ; NS RECORDS 5 | @ NS ns1.nameserver.com. 6 | @ NS ns2.nameserver.com. 7 | 8 | info TXT "This is some additional \"information\"" 9 | 10 | ; A RECORDS 11 | sub.domain A 192.168.1.42 ; This is a local ip. 12 | 13 | ; AAAA RECORDS 14 | ipv6.domain AAAA ::1 ; This is an IPv6 domain. 15 | 16 | ; MX RECORDS 17 | @ MX 10 mail-gw1.example.net.;This is the primary sever; and some more stuff. 18 | @ MX 20 mail-gw2.example.net. 19 | @ MX 30 mail-gw3.example.net. 20 | 21 | mail IN TXT "THIS IS SOME TEXT; WITH A SEMICOLON" 22 | -------------------------------------------------------------------------------- /tests/Rdata/RdataTraitTest.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace Badcow\DNS\Tests\Rdata; 15 | 16 | use Badcow\DNS\Rdata\RdataTrait; 17 | use PHPUnit\Framework\TestCase; 18 | 19 | class RdataTraitTest extends TestCase 20 | { 21 | use RdataTrait; 22 | 23 | public const TYPE = 'RDATA_TEST'; 24 | 25 | public function testGetType(): void 26 | { 27 | $this->assertEquals(self::TYPE, $this->getType()); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /lib/Parser/Tokens.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace Badcow\DNS\Parser; 15 | 16 | class Tokens 17 | { 18 | public const BACKSLASH = '\\'; 19 | public const CARRIAGE_RETURN = "\r"; 20 | public const CLOSE_BRACKET = ')'; 21 | public const DOUBLE_QUOTES = '"'; 22 | public const LINE_FEED = "\n"; 23 | public const OPEN_BRACKET = '('; 24 | public const SEMICOLON = ';'; 25 | public const SPACE = ' '; 26 | public const TAB = "\t"; 27 | } 28 | -------------------------------------------------------------------------------- /tests/ZoneBuilderTest.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace Badcow\DNS\Tests; 15 | 16 | use Badcow\DNS\ZoneBuilder; 17 | use PHPUnit\Framework\TestCase; 18 | 19 | class ZoneBuilderTest extends TestCase 20 | { 21 | public function testBuild(): void 22 | { 23 | $zone = TestZone::buildTestZone(); 24 | $zoneBuilder = new ZoneBuilder(); 25 | $this->assertEquals(TestZone::getExpectation(), $output = $zoneBuilder->build($zone)); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /lib/Edns/Option/DecodeException.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace Badcow\DNS\Edns\Option; 15 | 16 | use Badcow\DNS\Rdata; 17 | 18 | class DecodeException extends \Exception 19 | { 20 | public function __construct(string $option, string $value, int $code = 0, ?\Throwable $previous = null) 21 | { 22 | $message = sprintf('Unable to decode %s option from binary data "%s"', $option, Rdata\DecodeException::binaryToHex($value)); 23 | parent::__construct($message, $code, $previous); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /tests/Parser/Resources/IncludeControlEntryTests/mydomain.biz_expectation.db: -------------------------------------------------------------------------------- 1 | $ORIGIN mydomain.biz. 2 | $TTL 3600 3 | @ 3600 IN SOA mydomain.biz. post.mydomain.biz. 2014110501 3600 14400 604800 3600 4 | @ 3600 IN NS ns1.nameserver.com. 5 | @ 3600 IN NS ns2.nameserver.com. 6 | info 3600 IN TXT "This is some additional \"information\"" 7 | sub.domain 3600 IN A 192.168.1.42 8 | ipv6.domain 3600 IN AAAA ::1 9 | ; include the subdomain _subdomain.mydomain.biz. 10 | _subdomain.mydomain.biz. 7200 IN MX 10 mail-gw1.example.net.; The first mail gateway 11 | _subdomain.mydomain.biz. 7200 IN MX 20 mail-gw2.example.net. 12 | _subdomain.mydomain.biz. 7200 IN MX 30 mail-gw3.example.net. 13 | mail._subdomain.mydomain.biz. 7200 IN TXT "THIS IS SOME TEXT; WITH A SEMICOLON" 14 | mail 3600 IN CNAME mx1.bizmail. 15 | @ 3600 IN MX 10 mail 16 | -------------------------------------------------------------------------------- /lib/Rdata/DNAME.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace Badcow\DNS\Rdata; 15 | 16 | /** 17 | * Class DnameRdata. 18 | * 19 | * The DNAME record provides redirection for a subtree of the domain 20 | * name tree in the DNS. That is, all names that end with a particular 21 | * suffix are redirected to another part of the DNS. 22 | * Based on RFC6672 23 | * 24 | * @see http://tools.ietf.org/html/rfc6672 25 | */ 26 | class DNAME extends CNAME 27 | { 28 | public const TYPE = 'DNAME'; 29 | public const TYPE_CODE = 39; 30 | } 31 | -------------------------------------------------------------------------------- /tests/Parser/Resources/testCollapseMultilinesWithComments_expectation.txt: -------------------------------------------------------------------------------- 1 | $ORIGIN example.com. 2 | $TTL 1337 3 | $INCLUDE hq.example.com.txt 4 | @ IN SOA example.com. post.example.com. 2014110501 3600 14400 604800 3600;MNAME RNAME SERIAL REFRESH RETRY EXPIRE MINIMUM This is my Start of Authority Record; AKA SOA. 5 | ;NS RECORDS 6 | @ NS ns1.nameserver.com. 7 | @ NS ns2.nameserver.com. 8 | info TXT "This is some additional \"information\"" 9 | ;A RECORDS 10 | sub.domain A 192.168.1.42;This is a local ip. 11 | ;AAAA RECORDS 12 | ipv6.domain AAAA ::1;This is an IPv6 domain. 13 | ;MX RECORDS 14 | @ MX 10 mail-gw1.example.net. 15 | @ MX 20 mail-gw2.example.net. 16 | @ MX 30 mail-gw3.example.net. 17 | mail IN TXT "THIS IS SOME TEXT; WITH A SEMICOLON" 18 | 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 -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 10 | ./lib 11 | 12 | 13 | ./tests 14 | ./vendor 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | ./tests 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "badcow/dns", 3 | "description": "A PHP library for creating DNS zone files based on RFC1035", 4 | "license": "MIT", 5 | "authors": [ 6 | { 7 | "name": "Sam Williams", 8 | "email": "sam@badcow.co" 9 | } 10 | ], 11 | "minimum-stability": "stable", 12 | "require": { 13 | "php": "^8.0", 14 | "rlanvin/php-ip": "^3.0", 15 | "christian-riesen/base32": "^1.5.2" 16 | }, 17 | "require-dev": { 18 | "phpunit/phpunit": "^9.0", 19 | "friendsofphp/php-cs-fixer": "^3.5.0", 20 | "phpstan/phpstan": "^1.4" 21 | }, 22 | "autoload": { 23 | "psr-4": { 24 | "Badcow\\DNS\\": "lib/" 25 | } 26 | }, 27 | "autoload-dev": { 28 | "psr-4": { 29 | "Badcow\\DNS\\Tests\\": "tests/" 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /lib/Parser/ZoneFileFetcherInterface.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace Badcow\DNS\Parser; 15 | 16 | /** 17 | * Interface ZoneFileFetcherInterface: used to get the contents of other zone files that are stated in the $INCLUDE directive. 18 | */ 19 | interface ZoneFileFetcherInterface 20 | { 21 | /** 22 | * Fetches the contents of a zone file with a given path. 23 | * 24 | * @param string $path the path, relative or otherwise, to a zone file 25 | * 26 | * @return string the text contents of the zone file 27 | */ 28 | public function fetch(string $path): string; 29 | } 30 | -------------------------------------------------------------------------------- /tests/Parser/Resources/IncludeControlEntryTests/testdomain.geek.db: -------------------------------------------------------------------------------- 1 | $ORIGIN testdomain.geek. 2 | $TTL 7200 3 | $INCLUDE testdomain.geek\ -\ extra\ info.db ; This contains the SOA ans NS data 4 | 5 | info TXT "This is some additional \"information\"" 6 | 7 | ; A RECORDS 8 | sub.domain A 192.168.1.42 ; This is a local ip. 9 | 10 | ; AAAA RECORDS 11 | ipv6.domain AAAA ::1 ; This is an IPv6 domain. 12 | 13 | ; MX RECORDS 14 | @ MX 10 mail-gw1.example.net. 15 | @ MX 20 mail-gw2.example.net. 16 | @ MX 30 mail-gw3.example.net. 17 | 18 | mail IN TXT "THIS IS SOME TEXT; WITH A SEMICOLON" 19 | 20 | multicast APL ( 21 | 1:192.168.0.0/23 22 | 2:2001:acad:1::/112 23 | !1:192.168.1.64/28 24 | !2:2001:acad:1::8/128 25 | ) 26 | -------------------------------------------------------------------------------- /tests/Resources/wire/wire_test.data2: -------------------------------------------------------------------------------- 1 | 1707 8180 2 | 0001 0005 0002 0002 027a 6202 6d78 0361 3 | 6f6c 0363 6f6d 0000 0100 01c0 0c00 0100 4 | 0100 000b a100 04c6 5110 21c0 0c00 0100 5 | 0100 000b a100 04c6 5110 22c0 0c00 0100 6 | 0100 000b a100 04c6 5110 23c0 0c00 0100 7 | 0100 000b a100 04c6 5110 24c0 0c00 0100 8 | 0100 000b a100 04c6 5110 2502 6d78 0341 9 | 4f4c 0363 6f6d 0000 0200 0100 000d 9900 10 | 0c06 646e 732d 3031 026e 73c0 72c0 6f00 11 | 0200 0100 000d 9900 0906 646e 732d 3032 12 | c08c c085 0001 0001 0000 0d99 0004 c651 13 | 11e8 c09d 0001 0001 0000 0d99 0004 cdbc 14 | 9de8 15 | -------------------------------------------------------------------------------- /tests/Resources/wire/wire_test.data3: -------------------------------------------------------------------------------- 1 | 1706 8180 2 | 0001 0005 0002 0002 027a 6102 6d78 0361 3 | 6f6c 0363 6f6d 0000 0100 01c0 0c00 0100 4 | 0100 000b a100 04c6 5110 05c0 0c00 0100 5 | 0100 000b a100 04c6 5110 01c0 0c00 0100 6 | 0100 000b a100 04c6 5110 02c0 0c00 0100 7 | 0100 000b a100 04c6 5110 03c0 0c00 0100 8 | 0100 000b a100 04c6 5110 0402 6d78 0341 9 | 4f4c 0363 6f6d 0000 0200 0100 000d 9900 10 | 0c06 646e 732d 3031 026e 73c0 72c0 6f00 11 | 0200 0100 000d 9900 0906 646e 732d 3032 12 | c08c c085 0001 0001 0000 0d99 0004 c651 13 | 11e8 c09d 0001 0001 0000 0d99 0004 cdbc 14 | 9de8 15 | -------------------------------------------------------------------------------- /.php-cs-fixer.dist.php: -------------------------------------------------------------------------------- 1 | 7 | 8 | For the full copyright and license information, please view the LICENSE 9 | file that was distributed with this source code. 10 | EOF; 11 | 12 | $finder = PhpCsFixer\Finder::create() 13 | ->in(__DIR__.'/lib') 14 | ->in(__DIR__.'/tests') 15 | ->exclude(__DIR__.'/tests/Resources') 16 | ->exclude(__DIR__.'/tests/Parser/Resources') 17 | ; 18 | 19 | $config = new PhpCsFixer\Config(); 20 | $config->setRiskyAllowed(true) 21 | ->setRules([ 22 | '@Symfony' => true, 23 | 'header_comment' => ['header' => $header], 24 | 'array_syntax' => ['syntax' => 'short'], 25 | 'ordered_imports' => ['sort_algorithm' => 'alpha'], 26 | 'void_return' => true, 27 | 'declare_strict_types' => true, 28 | ]) 29 | ->setFinder($finder) 30 | ; 31 | 32 | return $config; -------------------------------------------------------------------------------- /lib/Rdata/RdataTrait.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace Badcow\DNS\Rdata; 15 | 16 | trait RdataTrait 17 | { 18 | /** 19 | * Get the string representation of the Rdata type. 20 | * 21 | * @return string Rdata type, e.g. "A", "MX", "NS", etc. 22 | */ 23 | public function getType(): string 24 | { 25 | /* @const TYPE */ 26 | return static::TYPE; 27 | } 28 | 29 | /** 30 | * Get the integer type code of the Rdata type as defined by IANA. 31 | * 32 | * @return int IANA Rdata type code 33 | */ 34 | public function getTypeCode(): int 35 | { 36 | /* @const TYPE_CODE */ 37 | return static::TYPE_CODE; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /.github/workflows/PHP-8.yml: -------------------------------------------------------------------------------- 1 | name: PHP 8 2 | 3 | on: 4 | - push 5 | - pull_request 6 | 7 | jobs: 8 | run: 9 | runs-on: ubuntu-latest 10 | strategy: 11 | fail-fast: false 12 | matrix: 13 | php: ['8.0', '8.1', '8.2', '8.3'] 14 | name: PHP ${{ matrix.php }} 15 | steps: 16 | - name: Checkout 17 | uses: actions/checkout@v4 18 | 19 | - name: Install PHP 20 | uses: shivammathur/setup-php@v2 21 | with: 22 | php-version: ${{ matrix.php }} 23 | extensions: gmp 24 | 25 | - name: Display versions 26 | run: | 27 | php -r 'foreach (get_loaded_extensions() as $extension) echo $extension . " " . phpversion($extension) . PHP_EOL;' 28 | php -i 29 | 30 | - name: Composer install 31 | run: composer install -n 32 | 33 | - name: PHPUnit 34 | run: vendor/bin/phpunit 35 | 36 | - name: Test Coverage 37 | run: vendor/bin/phpunit --coverage-text 38 | -------------------------------------------------------------------------------- /tests/Parser/Resources/50.100.200.in-addr.arpa.db: -------------------------------------------------------------------------------- 1 | $ORIGIN 50.100.200.in-addr.arpa. 2 | $TTL 3600 3 | 4 | @ IN SOA ( 5 | ns.acme.com. ; MNAME 6 | noc.acme.com. ; RNAME 7 | 2014110501 ; SERIAL 8 | 3600 ; REFRESH 9 | 14400 ; RETRY 10 | 604800 ; EXPIRE 11 | 3600 ; MINIMUM 12 | ) 13 | 14 | ; NS RECORDS 15 | @ NS ns1.acme.com. 16 | @ NS ns2.acme.com. 17 | 18 | 1 IN 1080 PTR gw01.core.acme.com. 19 | 1 IN 1080 PTR gw02.core.acme.com. 20 | 21 | $TTL 1080 22 | 23 | 50 IN PTR mx1.acme.com. 24 | 52 IN PTR mx2.acme.com. 25 | 26 | 70 7200 IN PTR ns1.acme.com. 27 | 72 7200 IN PTR ns2.acme.com. 28 | 29 | 150 200 PTR smtp.example.com. 30 | 170 150 IN PTR netscape.com. 31 | -------------------------------------------------------------------------------- /tests/Rdata/TypesTest.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace Badcow\DNS\Tests\Rdata; 15 | 16 | use Badcow\DNS\Rdata\Types; 17 | use Badcow\DNS\Rdata\UnsupportedTypeException; 18 | use PHPUnit\Framework\TestCase; 19 | 20 | class TypesTest extends TestCase 21 | { 22 | /** 23 | * @throws UnsupportedTypeException 24 | */ 25 | public function testGetTypeCode(): void 26 | { 27 | $this->assertEquals(1, Types::getTypeCode('A')); 28 | $this->assertEquals(1234, Types::getTypeCode('TYPE1234')); 29 | 30 | $this->expectException(UnsupportedTypeException::class); 31 | $this->expectExceptionMessage('RData type "XX" is not supported.'); 32 | Types::getTypeCode('XX'); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /tests/Edns/Option/CodesTest.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace Badcow\DNS\Tests\Edns\Option; 15 | 16 | use Badcow\DNS\Edns\Option\Codes; 17 | use Badcow\DNS\Edns\Option\UnsupportedOptionException; 18 | use PHPUnit\Framework\TestCase; 19 | 20 | class CodesTest extends TestCase 21 | { 22 | public function testIsValid(): void 23 | { 24 | $this->assertTrue(Codes::isValid(Codes::COOKIE)); 25 | $this->assertTrue(Codes::isValid('COOKIE')); 26 | } 27 | 28 | public function testGetName(): void 29 | { 30 | $this->assertEquals('COOKIE', Codes::getName(Codes::COOKIE)); 31 | $this->expectException(UnsupportedOptionException::class); 32 | $this->assertTrue(Codes::getName(1024)); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /lib/Opcode.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace Badcow\DNS; 15 | 16 | /** 17 | * {@link https://www.iana.org/assignments/dns-parameters/dns-parameters.xhtml#dns-parameters-5}. 18 | */ 19 | class Opcode 20 | { 21 | /** 22 | * [RFC1035]. 23 | */ 24 | public const QUERY = 0; 25 | 26 | /** 27 | * Inverse Query [RFC3425] (Obsolete). 28 | */ 29 | public const IQUERY = 1; 30 | 31 | /** 32 | * [RFC1035]. 33 | */ 34 | public const STATUS = 2; 35 | 36 | /** 37 | * [RFC1996]. 38 | */ 39 | public const NOTIFY = 4; 40 | 41 | /** 42 | * [RFC2136]. 43 | */ 44 | public const UPDATE = 5; 45 | 46 | /** 47 | * DNS Stateful Operations [RFC8490]. 48 | */ 49 | public const DSO = 6; 50 | } 51 | -------------------------------------------------------------------------------- /lib/Rdata/DecodeException.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace Badcow\DNS\Rdata; 15 | 16 | class DecodeException extends \Exception 17 | { 18 | public function __construct(string $type, string $rdata, int $code = 0, ?\Throwable $previous = null) 19 | { 20 | $message = sprintf('Unable to decode %s record rdata from binary data "%s"', $type, self::binaryToHex($rdata)); 21 | parent::__construct($message, $code, $previous); 22 | } 23 | 24 | /** 25 | * Convert a binary string into hexadecimal values. 26 | */ 27 | public static function binaryToHex(string $rdata): string 28 | { 29 | $hex = array_map(function ($char) { 30 | return sprintf('0x%02x', ord($char)); 31 | }, str_split($rdata)); 32 | 33 | return implode(' ', $hex); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012 Badcow 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 7 | of the Software, and to permit persons to whom the Software is furnished to do 8 | so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. -------------------------------------------------------------------------------- /tests/Edns/Option/FactoryTest.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace Badcow\DNS\Tests\Edns\Option; 15 | 16 | use Badcow\DNS\Edns\Option\Codes; 17 | use Badcow\DNS\Edns\Option\COOKIE; 18 | use Badcow\DNS\Edns\Option\Factory; 19 | use Badcow\DNS\Edns\Option\UnsupportedOptionException; 20 | use PHPUnit\Framework\TestCase; 21 | 22 | class FactoryTest extends TestCase 23 | { 24 | public function testNewOptionFromName(): void 25 | { 26 | $this->assertInstanceOf(COOKIE::class, Factory::newOptionFromName('COOKIE')); 27 | 28 | $this->expectException(UnsupportedOptionException::class); 29 | Factory::newOptionFromName('INVALID'); 30 | } 31 | 32 | public function testIsOptionCodeImplemented(): void 33 | { 34 | $this->assertTrue(Factory::isOptionCodeImplemented(Codes::COOKIE)); 35 | $this->assertFalse(Factory::isOptionCodeImplemented(1024)); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /lib/Parser/Comments.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace Badcow\DNS\Parser; 15 | 16 | class Comments 17 | { 18 | /** 19 | * No comments are parsed. 20 | */ 21 | public const NONE = 0; 22 | 23 | /** 24 | * Inline comments that appear at the end of a resource record. 25 | */ 26 | public const END_OF_ENTRY = 1; 27 | 28 | /** 29 | * Multi-line record comments: those comments that appear within multi-line brackets. E.g. 30 | * acme.org. IN MX ( 31 | * 30 ;This comment will be parsed. 32 | * mail-gw3 ;So will this comment. 33 | * ). 34 | */ 35 | public const MULTILINE = 2; 36 | 37 | /** 38 | * Orphan comments appear without a resource record. Usually these are section headers. 39 | */ 40 | public const ORPHAN = 4; 41 | 42 | /** 43 | * Parse all comments. 44 | */ 45 | public const ALL = 7; 46 | } 47 | -------------------------------------------------------------------------------- /tests/Resources/wire/wire_test.data1: -------------------------------------------------------------------------------- 1 | # message header 2 | 000a #id=10 3 | 85 # QR=1 (response) OPCODE=0 AA=1 TC=0 RD=1 4 | 80 # RA=1 BIT9=0 AD=0 CD=0 RCODE=0 5 | 0001 #QDCOUNT 6 | 0003 #ANCOUNT 7 | 0000 #NSCOUNT 8 | 0003 #ARCOUNT 9 | 10 | #QUERY 11 | #[3] vix [3] com . NS IN 12 | 03 766978 03 636f6d 00 0002 0001 # question section: vix.com IN NS 13 | 14 | #ANSWERS 15 | #[12] NS IN 3600 rdlen=11 [5] isrv1 [2] p a [12] (vix.com.) 16 | c00c 0002 0001 00000e10 000b 05 69 73 72 76 31 02 70 61 c00c # vix.com IN NS 3600 17 | 18 | #[12] NS IN 3600 rdlen=9 [6] n s - e x t [12] (vix.com.) 19 | c00c 0002 0001 00000e10 0009 06 6e 73 2d 65 78 74 c00c 20 | 21 | #[12] NS IN 3600 rdlen=14 [3] n s 1 [4] g n a c [3] c o m [.] 22 | c00c 0002 0001 00000e10 000e 03 6e 73 31 04 67 6e 61 63 03 63 6f 6d 00 23 | 24 | #ADDITIONALS 25 | #[37] A IN 3600 rdlen=4 204.152.184.134 (37=isrv1.pa.vix.com.) 26 | c025 0001 0001 00000e10 0004 cc98b886 27 | 28 | #[60] A IN 3600 rdlen=4 204.152.184.64 (60=ns-ext.vix.com.) 29 | c03c 0001 0001 00000e10 0004 cc98b840 30 | 31 | #[81] A IN 172362 rdlen=4 198.151.248.246 (81=ns1.gnac.com.) 32 | c051 0001 0001 0002a14a 0004 c697f8f6 33 | -------------------------------------------------------------------------------- /lib/Rdata/DNSKEY.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace Badcow\DNS\Rdata; 15 | 16 | /** 17 | * Class DNSKEY. 18 | * 19 | * {@link https://tools.ietf.org/html/rfc4034#section-2.1} 20 | */ 21 | class DNSKEY extends KEY 22 | { 23 | public const TYPE = 'DNSKEY'; 24 | public const TYPE_CODE = 48; 25 | 26 | /** 27 | * The Protocol Field MUST have value 3, and the DNSKEY RR MUST be 28 | * treated as invalid during signature verification if it is found to be 29 | * some value other than 3. 30 | * {@link https://tools.ietf.org/html/rfc4034#section-2.1.2}. 31 | * 32 | * @var int 33 | */ 34 | protected $protocol = 3; 35 | 36 | public function setProtocol(int $protocol): void 37 | { 38 | if (3 !== $protocol) { 39 | throw new \InvalidArgumentException('DNSKEY RData: parameter can only be set to "3".'); 40 | } 41 | 42 | parent::setProtocol($protocol); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /docs/Parser/Custom-Rdata-Handlers.md: -------------------------------------------------------------------------------- 1 | Using Custom RData Handlers 2 | =========================== 3 | Out-of-the-box, the library will handle most RData types that are regularly encountered. Occasionally, you may encounter 4 | an unsupported type. You can add your own RData handler method for the record type. For example, you may want to support 5 | the non-standard `SPF` record type, and return a `TXT` instance. 6 | ```php 7 | $spf = function (\ArrayIterator $iterator): Badcow\DNS\Rdata\TXT { 8 | $string = ''; 9 | while ($iterator->valid()) { 10 | $string .= $iterator->current() . ' '; 11 | $iterator->next(); 12 | } 13 | $string = trim($string, ' "'); //Remove whitespace and quotes 14 | 15 | $spf = new Badcow\DNS\Rdata\TXT; 16 | $spf->setText($string); 17 | 18 | return $spf; 19 | }; 20 | 21 | $customHandlers = ['SPF' => $spf]; 22 | 23 | $record = 'example.com. 7200 IN SPF "v=spf1 a mx ip4:69.64.153.131 include:_spf.google.com ~all"'; 24 | $parser = new \Badcow\DNS\Parser\Parser($customHandlers); 25 | $zone = $parser->makeZone('example.com.', $record); 26 | ``` 27 | 28 | You can also overwrite the default handlers if you wish, as long as your handler method returns an instance of 29 | `Badcow\DNS\Rdata\RdataInterface`. 30 | -------------------------------------------------------------------------------- /tests/Resources/wire/wire_test.data5: -------------------------------------------------------------------------------- 1 | #This is the same as wire_test.data1 except all labels have been uncompressed. 2 | 3 | # MESSAGE HEADER 4 | 000a #id=10 5 | 85 # QR=1 (response) OPCODE=0 AA=1 TC=0 RD=1 6 | 80 # RA=1 BIT9=0 AD=0 CD=0 RCODE=0 7 | 0001 #QDCOUNT 8 | 0003 #ANCOUNT 9 | 0000 #NSCOUNT 10 | 0003 #ARCOUNT 11 | 12 | # QUESTION SECTION 13 | 03 766978 03 636f6d 00 0002 0001 #vix.com IN NS 14 | 15 | # ANSWER SECTION 16 | 03 766978 03 636f6d 00 0002 0001 00000e10 0012 05 69 73 72 76 31 02 70 61 03 76 69 78 03 63 6f 6d 00 # vix.com IN NS 3600 isrv1.pa.vix.com. 17 | 03 766978 03 636f6d 00 0002 0001 00000e10 0010 06 6e 73 2d 65 78 74 03 76 69 78 03 63 6f 6d 00 # vix.com IN NS 3600 ns-ext.vix.com. 18 | 03 766978 03 636f6d 00 0002 0001 00000e10 000e 03 6e 73 31 04 67 6e 61 63 03 63 6f 6d 00 # vix.com IN NS 3600 ns1.gnac.com. 19 | 20 | # ADDITIONAL SECTION 21 | 05 6973727631 02 7061 03 766978 03 636f6d 00 0001 0001 00000e10 0004 cc98b886 # isrv1.pa.vix.com. IN A 3600 204.152.184.134 22 | 06 6e732d657874 03 766978 03 636f6d 00 0001 0001 00000e10 0004 cc98b840 # ns-ext.vix.com. IN A 3600 204.152.184.64 23 | 03 6e7331 04 676e6163 03 636f6d 00 0001 0001 0002a14a 0004 c697f8f6 # ns1.gnac.com. IN A 172362 198.151.248.246 24 | -------------------------------------------------------------------------------- /lib/Rdata/AAAA.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace Badcow\DNS\Rdata; 15 | 16 | use Badcow\DNS\Validator; 17 | 18 | /** 19 | * @see https://tools.ietf.org/html/rfc3596#section-2.1 20 | */ 21 | class AAAA extends A 22 | { 23 | public const TYPE = 'AAAA'; 24 | public const TYPE_CODE = 28; 25 | 26 | public function setAddress(string $address): void 27 | { 28 | if (!Validator::ipv6($address)) { 29 | throw new \InvalidArgumentException(sprintf('The address "%s" is not a valid IPv6 address.', $address)); 30 | } 31 | 32 | $this->address = $address; 33 | } 34 | 35 | /** 36 | * @throws DecodeException 37 | */ 38 | public function fromWire(string $rdata, int &$offset = 0, ?int $rdLength = null): void 39 | { 40 | if (false === $address = @inet_ntop(substr($rdata, $offset, 16))) { 41 | throw new DecodeException(static::TYPE, $rdata); 42 | } 43 | $offset += 16; 44 | 45 | $this->setAddress($address); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /docs/Validation.md: -------------------------------------------------------------------------------- 1 | Validation 2 | ========== 3 | 4 | Very little validation is done automatically when setting RData or Resource Record objects, so the onus is on the 5 | implementor to validate the resource data and the zone record. Contained within the library, however, are some useful 6 | validation tools. These are static functions contained within `Badcow\DNS\Validator` (here, simply referred to as 7 | `Validator`). 8 | 9 | ## Validate the Zone 10 | `Validator::zone()` will inspect a number of properties of a Zone, namely: 11 | * There is exactly one SOA record, 12 | * There are NS records, and 13 | * There is exactly one record class (while it is permissible to use obsolete classes such as CHAOS, in all modern 14 | contexts, this is always IN). 15 | 16 | The return value is a binary sum of error codes which can be determined using boolean operands. A valid zone will 17 | return `0`, AKA `Validator::ZONE_OKAY`. _Exempli gratia:_ 18 | ```php 19 | $zone //Some Badcow\DNS\Zone 20 | if (Validator::zone($zone) & Validator::ZONE_NO_SOA) echo "There are no SOA Records"; 21 | if (Validator::zone($zone) & Validator::ZONE_TOO_MANY_CLASSES) echo "There are too many classes."; 22 | ``` 23 | 24 | The return codes are: 25 | * `ZONE_NO_SOA` 26 | * `ZONE_TOO_MANY_SOA` 27 | * `ZONE_NO_NS` 28 | * `ZONE_NO_CLASS` 29 | * `ZONE_TOO_MANY_CLASSES` 30 | * `ZONE_OKAY` 31 | 32 | -------------------------------------------------------------------------------- /tests/AlgorithmsTest.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace Badcow\DNS\Tests; 15 | 16 | use Badcow\DNS\Algorithms; 17 | use PHPUnit\Framework\TestCase; 18 | 19 | class AlgorithmsTest extends TestCase 20 | { 21 | public function testGetMnemonic(): void 22 | { 23 | $this->assertEquals('RSASHA1', Algorithms::getMnemonic(5)); 24 | } 25 | 26 | /** 27 | * @throws \InvalidArgumentException 28 | */ 29 | public function testGetMnemonicThrowsExceptionOnInvalidAlgorithm(): void 30 | { 31 | $this->expectException(\InvalidArgumentException::class); 32 | $this->expectExceptionMessage('"1337" is not a valid algorithm.'); 33 | Algorithms::getMnemonic(1337); 34 | } 35 | 36 | /** 37 | * @throws \InvalidArgumentException 38 | */ 39 | public function testGetAlgorithmValueThrowsExceptionOnInvalidMnemonic(): void 40 | { 41 | $this->expectException(\InvalidArgumentException::class); 42 | $this->expectExceptionMessage('"INVALID_MNEMONIC" is not a valid algorithm mnemonic.'); 43 | Algorithms::getAlgorithmValue('INVALID_MNEMONIC'); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /tests/Parser/Resources/testCollapseMultilines_sample.txt: -------------------------------------------------------------------------------- 1 | $ORIGIN example.com. 2 | $TTL 1337 3 | $INCLUDE hq.example.com.txt 4 | @ IN SOA ( 5 | example.com. ; MNAME 6 | post.example.com. ; RNAME 7 | 2014110501 ; SERIAL 8 | 3600 ; REFRESH 9 | 14400 ; RETRY 10 | 604800 ; EXPIRE 11 | 3600 ; MINIMUM 12 | ); This is my Start of Authority Record; AKA SOA. 13 | 14 | ; NS RECORDS 15 | @ NS ns1.nameserver.com. 16 | @ NS ns2.nameserver.com. 17 | 18 | info TXT "This is some additional \"information\"" 19 | 20 | ; A RECORDS 21 | sub.domain A 192.168.1.42 ; This is a local ip. 22 | 23 | ; AAAA RECORDS 24 | ipv6.domain AAAA ::1 ; This is an IPv6 domain. 25 | 26 | ; MX RECORDS 27 | @ MX 10 mail-gw1.example.net. 28 | @ MX 20 mail-gw2.example.net. 29 | @ MX 30 mail-gw3.example.net. 30 | 31 | mail IN TXT "THIS IS SOME TEXT; WITH A SEMICOLON" 32 | 33 | multicast APL ( 34 | 1:192.168.0.0/23 35 | 2:2001:acad:1::/112 36 | !1:192.168.1.64/28 37 | !2:2001:acad:1::8/128 38 | ) 39 | -------------------------------------------------------------------------------- /tests/Rdata/CdnskeyTest.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace Badcow\DNS\Tests\Rdata; 15 | 16 | use Badcow\DNS\Algorithms; 17 | use Badcow\DNS\Rdata\CDNSKEY; 18 | use Badcow\DNS\Rdata\Factory; 19 | use PHPUnit\Framework\TestCase; 20 | 21 | class CdnskeyTest extends TestCase 22 | { 23 | /** 24 | * @var string 25 | */ 26 | private static $publicKey = 'AQPSKmynfzW4kyBv015MUG2DeIQ3Cbl+BBZH4b/0PY1kxkmvHjcZc8nokfzj31GajIQKY+5CptLr3buXA10hWqTkF7H6RfoRqXQeogmMHfpftf6zMv1LyBUgia7za6ZEzOJBOztyvhjL742iU/TpPSEDhm2SNKLijfUppn1UaNvv4w=='; 27 | 28 | public function testFactory(): void 29 | { 30 | $cdnskey = Factory::CDNSKEY(256, Algorithms::RSASHA1, base64_decode(self::$publicKey)); 31 | $output = '256 3 5 '.self::$publicKey; 32 | 33 | $this->assertInstanceOf(CDNSKEY::class, $cdnskey); 34 | $this->assertEquals(256, $cdnskey->getFlags()); 35 | $this->assertEquals(5, $cdnskey->getAlgorithm()); 36 | $this->assertEquals(base64_decode(self::$publicKey), $cdnskey->getPublicKey()); 37 | $this->assertEquals(3, $cdnskey->getProtocol()); 38 | $this->assertEquals($output, $cdnskey->toText()); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /tests/ClassesTest.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace Badcow\DNS\Tests; 15 | 16 | use Badcow\DNS\Classes; 17 | 18 | class ClassesTest extends \PHPUnit\Framework\TestCase 19 | { 20 | public function testIsValidClass(): void 21 | { 22 | $this->assertTrue(Classes::isValid('IN')); 23 | $this->assertTrue(Classes::isValid('HS')); 24 | $this->assertTrue(Classes::isValid('CH')); 25 | 26 | $this->assertFalse(Classes::isValid('INTERNET')); 27 | $this->assertFalse(Classes::isValid('in')); 28 | $this->assertFalse(Classes::isValid('In')); 29 | $this->assertFalse(Classes::isValid('hS')); 30 | } 31 | 32 | public function testGetClassId(): void 33 | { 34 | $this->assertEquals(1, Classes::getClassId('IN')); 35 | $this->assertEquals(4, Classes::getClassId('HS')); 36 | $this->assertEquals(3, Classes::getClassId('CH')); 37 | } 38 | 39 | public function testGetClassIdThrowsExceptionForUndefinedClass(): void 40 | { 41 | $this->expectException(\InvalidArgumentException::class); 42 | $this->expectExceptionMessage('Class "XX" is not a valid DNS class.'); 43 | Classes::getClassId('XX'); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /lib/Parser/ParseException.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace Badcow\DNS\Parser; 15 | 16 | class ParseException extends \Exception 17 | { 18 | /** 19 | * @var StringIterator 20 | */ 21 | private $stringIterator; 22 | 23 | /** 24 | * ParseException constructor. 25 | */ 26 | public function __construct(string $message = '', ?StringIterator $stringIterator = null, \Throwable $previous = null) 27 | { 28 | if (null !== $stringIterator) { 29 | $this->stringIterator = $stringIterator; 30 | $message .= sprintf(' [Line no: %d]', $this->getLineNumber()); 31 | } 32 | 33 | parent::__construct($message, 0, $previous); 34 | } 35 | 36 | /** 37 | * Get line number of current entry on the StringIterator. 38 | */ 39 | private function getLineNumber(): int 40 | { 41 | $pos = $this->stringIterator->key(); 42 | $this->stringIterator->rewind(); 43 | $lineNo = 1; 44 | 45 | while ($this->stringIterator->key() < $pos) { 46 | if ($this->stringIterator->is(Tokens::LINE_FEED)) { 47 | ++$lineNo; 48 | } 49 | $this->stringIterator->next(); 50 | } 51 | 52 | return $lineNo; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /tests/Rdata/FactoryTest.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace Badcow\DNS\Tests\Rdata; 15 | 16 | use Badcow\DNS\Rdata\Factory; 17 | use Badcow\DNS\Rdata\UnsupportedTypeException; 18 | use PHPUnit\Framework\TestCase; 19 | 20 | class FactoryTest extends TestCase 21 | { 22 | public function getTestData(): array 23 | { 24 | $namespace = '\\Badcow\\DNS\\Rdata\\'; 25 | 26 | return [ 27 | ['CNAME', 5, $namespace.'CNAME'], 28 | ['AAAA', 28, $namespace.'AAAA'], 29 | ['RRSIG', 46, $namespace.'RRSIG'], 30 | ]; 31 | } 32 | 33 | /** 34 | * @dataProvider getTestData 35 | * 36 | * @throws UnsupportedTypeException 37 | */ 38 | public function testNewRdataFromNameAndId(string $type, int $typeCode, string $classname): void 39 | { 40 | $this->assertInstanceOf($classname, Factory::newRdataFromName($type)); 41 | $this->assertInstanceOf($classname, Factory::newRdataFromId($typeCode)); 42 | } 43 | 44 | /** 45 | * @throws UnsupportedTypeException 46 | */ 47 | public function testNewRdataFromNameThrowsExceptionForUnknownType(): void 48 | { 49 | $this->expectException(UnsupportedTypeException::class); 50 | Factory::newRdataFromName('rsig'); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /tests/Parser/Resources/testConvolutedZone_sample.txt: -------------------------------------------------------------------------------- 1 | $origin example.com. 2 | $ttl 3600 3 | @ 300 IN SOA ( 4 | ns1.beget.ru. hostmaster.beget.ru. 2015060403 5 | 300 600 86400 300 6 | ) 7 | @ 600 IN A 5.101.153.38 8 | @ 600 IN AAAA ::ffff:192.168.10.23 9 | @ 300 IN NS ns1.beget.ru. 10 | _xmpp-server._tcp.icq.beget.ru 600 IN SRV 10 0 5247 jabber 11 | autoconfig 600 IN CNAME cf-ssl00000-protected.example.com. 12 | arhangelsk 600 IN MX 10 mx2.beget.ru. 13 | @ 600 IN TXT "Some text" " another Some text" 14 | test 600 IN TXT (;11111 15 | "Some text" ; or like this 16 | " another Some text" 17 | ) 18 | testtxt 600 IN TXT ("v=DKIM1; k=rsa; p=MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDZKI3U+9acu3NfEy0NJHIPydxnPLPpnAJ7" 19 | "k2JdrsLqAK1uouMudHI20pgE8RMldB/TeWKXYoRidcGCZWXleUzldDTwZAMDQNpdH1uuxym0VhoZpPbI1RXwpgHRTbCk49VqlC") 20 | testmx 600 IN MX 20 @;345345 21 | 300 IN MX 30 mail.com. 22 | IN MX 35 mail-gw3 23 | MX 40 mail-gw4 24 | testmx2 600 IN MX 20 .;23''33'123;;123; 25 | _domainkeytxt 600 IN TXT "t=y;o=~;\0";312 26 | www 600 IN TXT "@ A 79.125.10.157 " 27 | txt3 600 IN TXT "www CNAME ;;;;'werwerwer'\0010" 28 | nstest 300 IN NS ns1 29 | nstest2 300 IN NS 85.249.229.194;111 30 | xn----7sbfndkfpirgcajeli2a4pnc.xn----7sbbfcqfo2cfcagacemif0ap5q 300 IN NS 1.1.1.1 31 | casino 600 IN NS @;234 32 | bonus 600 IN CNAME @;111 33 | ;234234;234;234234234234 34 | * 600 IN CNAME s;111 35 | @ 600 IN SRV 10 0 5269 @ ;wer;wer;wer;wer;; 36 | xmpp 600 IN SRV 10 0 5222 81.211.107.230. ;;;werwerwer 37 | www222 0 IN CNAME lifun.ru. 38 | 46.20.191.35 3600 IN PTR office;1231 39 | ;qqq 300 IN OLE "omg" 40 | -------------------------------------------------------------------------------- /lib/Edns/Option/OptionInterface.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace Badcow\DNS\Edns\Option; 15 | 16 | interface OptionInterface 17 | { 18 | /** 19 | * Get the string representation of the Option type. 20 | * 21 | * @return string Option name type, e.g. "COOKIE", "CLIENT_SUBNET", etc. 22 | */ 23 | public function getName(): string; 24 | 25 | /** 26 | * Get the integer name code of the Option as defined by IANA. 27 | * 28 | * @return int IANA Option type code 29 | */ 30 | public function getCode(): int; 31 | 32 | /** 33 | * Return a DNS Server response formatted representation of the Option. 34 | * 35 | * @return string packed binary form of Option 36 | */ 37 | public function toWire(): string; 38 | 39 | /** 40 | * Populate Option from its wire representation. 41 | * 42 | * @param string $optionValue packed binary form of Option 43 | * @param int $offset the current offset or pointer, position of the start of Option relative to the whole $optionValue string 44 | * @param int|null $optionLength the length of the Option string, if null it is taken to be the whole $optionValue parameter string 45 | */ 46 | public function fromWire(string $optionValue, int &$offset = 0, ?int $optionLength = null): void; 47 | } 48 | -------------------------------------------------------------------------------- /lib/Rdata/CNAME.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace Badcow\DNS\Rdata; 15 | 16 | use Badcow\DNS\Message; 17 | 18 | /** 19 | * @see https://tools.ietf.org/html/rfc1035#section-3.3.1 20 | */ 21 | class CNAME implements RdataInterface 22 | { 23 | use RdataTrait; 24 | 25 | public const TYPE = 'CNAME'; 26 | public const TYPE_CODE = 5; 27 | 28 | /** 29 | * @var string|null 30 | */ 31 | protected $target; 32 | 33 | public function setTarget(string $target): void 34 | { 35 | $this->target = $target; 36 | } 37 | 38 | /** 39 | * @return string 40 | */ 41 | public function getTarget(): ?string 42 | { 43 | return $this->target; 44 | } 45 | 46 | public function toText(): string 47 | { 48 | return $this->target ?? ''; 49 | } 50 | 51 | public function toWire(): string 52 | { 53 | if (null === $this->target) { 54 | throw new \InvalidArgumentException('Target must be set.'); 55 | } 56 | 57 | return Message::encodeName($this->target); 58 | } 59 | 60 | public function fromText(string $text): void 61 | { 62 | $this->setTarget($text); 63 | } 64 | 65 | public function fromWire(string $rdata, int &$offset = 0, ?int $rdLength = null): void 66 | { 67 | $this->setTarget(Message::decodeName($rdata, $offset)); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /lib/Parser/ResourceRecordIterator.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace Badcow\DNS\Parser; 15 | 16 | class ResourceRecordIterator extends \ArrayIterator 17 | { 18 | public function __construct(string $resourceRecord) 19 | { 20 | parent::__construct(explode(Tokens::SPACE, $resourceRecord)); 21 | } 22 | 23 | /** 24 | * Return pointer to previous position. 25 | */ 26 | public function prev(): void 27 | { 28 | if (!$this->valid()) { 29 | $this->end(); 30 | 31 | return; 32 | } 33 | 34 | $this->seek((int) $this->key() - 1); 35 | } 36 | 37 | /** 38 | * Set pointer to the end of the array. 39 | */ 40 | public function end(): void 41 | { 42 | $lastPos = $this->count() - 1; 43 | $this->seek($lastPos); 44 | } 45 | 46 | /** 47 | * Get all the remaining values of an iterator as an array. This will move the pointer to the end of the array. 48 | */ 49 | public function getRemainingAsString(): string 50 | { 51 | $values = []; 52 | while ($this->valid()) { 53 | $values[] = $this->current(); 54 | $this->next(); 55 | } 56 | 57 | return implode(Tokens::SPACE, $values); 58 | } 59 | 60 | public function __toString(): string 61 | { 62 | return implode(Tokens::SPACE, $this->getArrayCopy()); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /tests/Rdata/NsTest.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace Badcow\DNS\Tests\Rdata; 15 | 16 | use Badcow\DNS\Rdata\NS; 17 | use PHPUnit\Framework\TestCase; 18 | 19 | class NsTest extends TestCase 20 | { 21 | public function testSetNsdname(): void 22 | { 23 | $target = 'foo.example.com.'; 24 | $ns = new NS(); 25 | $ns->setTarget($target); 26 | 27 | $this->assertEquals($target, $ns->getTarget()); 28 | } 29 | 30 | public function testOutput(): void 31 | { 32 | $target = 'foo.example.com.'; 33 | $ns = new NS(); 34 | $ns->setTarget($target); 35 | 36 | $this->assertEquals($target, $ns->toText()); 37 | $this->assertEquals($target, $ns->toText()); 38 | } 39 | 40 | public function testFromText(): void 41 | { 42 | $text = 'host.example.com.'; 43 | /** @var NS $cname */ 44 | $cname = new NS(); 45 | $cname->fromText($text); 46 | 47 | $this->assertEquals($text, $cname->getTarget()); 48 | } 49 | 50 | public function testWire(): void 51 | { 52 | $host = 'host.example.com.'; 53 | $expectation = chr(4).'host'.chr(7).'example'.chr(3).'com'.chr(0); 54 | 55 | /** @var NS $ns */ 56 | $ns = new NS(); 57 | $ns->fromWire($expectation); 58 | 59 | $this->assertEquals($expectation, $ns->toWire()); 60 | $this->assertEquals($host, $ns->getTarget()); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /tests/Rdata/UnknownTypeTest.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace Badcow\DNS\Tests\Rdata; 15 | 16 | use Badcow\DNS\Rdata\UnknownType; 17 | use PHPUnit\Framework\TestCase; 18 | 19 | class UnknownTypeTest extends TestCase 20 | { 21 | public function testToText(): void 22 | { 23 | $expectation = '\# 32 9aa065581e1a247d5e884a44adfa7cb4a849c7b90ade83c8fb9eae5984ea7fba'; 24 | $uk = new UnknownType(); 25 | $uk->setTypeCode(1234); 26 | $uk->setData(hex2bin('9aa065581e1a247d5e884a44adfa7cb4a849c7b90ade83c8fb9eae5984ea7fba')); 27 | 28 | $this->assertEquals($expectation, $uk->toText()); 29 | } 30 | 31 | public function testFromText(): void 32 | { 33 | $uk = new UnknownType(); 34 | $uk->fromText('\# 32 9aa065581e1a247d5e884a44adfa7cb4a849c7b90ade83c8fb9eae5984ea7fba'); 35 | $this->assertEquals(hex2bin('9aa065581e1a247d5e884a44adfa7cb4a849c7b90ade83c8fb9eae5984ea7fba'), $uk->getData()); 36 | } 37 | 38 | public function testWire(): void 39 | { 40 | $expectation = '\# 32 9aa065581e1a247d5e884a44adfa7cb4a849c7b90ade83c8fb9eae5984ea7fba'; 41 | $wireFormat = hex2bin('9aa065581e1a247d5e884a44adfa7cb4a849c7b90ade83c8fb9eae5984ea7fba'); 42 | $uk = new UnknownType(); 43 | $uk->fromWire($wireFormat); 44 | 45 | $this->assertEquals($expectation, $uk->toText()); 46 | $this->assertEquals($wireFormat, $uk->toWire()); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /tests/Rdata/CnameTest.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace Badcow\DNS\Tests\Rdata; 15 | 16 | use Badcow\DNS\Rdata\CNAME; 17 | use PHPUnit\Framework\TestCase; 18 | 19 | class CnameTest extends TestCase 20 | { 21 | public function testOutput(): void 22 | { 23 | $target = 'foo.example.com.'; 24 | $cname = new CNAME(); 25 | $cname->setTarget($target); 26 | 27 | $this->assertEquals($target, $cname->toText()); 28 | } 29 | 30 | public function testFromText(): void 31 | { 32 | $text = 'host.example.com.'; 33 | /** @var CNAME $cname */ 34 | $cname = new CNAME(); 35 | $cname->fromText($text); 36 | 37 | $this->assertEquals($text, $cname->getTarget()); 38 | } 39 | 40 | public function testWire(): void 41 | { 42 | $host = 'host.example.com.'; 43 | $expectation = chr(4).'host'.chr(7).'example'.chr(3).'com'.chr(0); 44 | 45 | /** @var CNAME $cname */ 46 | $cname = new CNAME(); 47 | $cname->fromWire($expectation); 48 | 49 | $this->assertEquals($expectation, $cname->toWire()); 50 | $this->assertEquals($host, $cname->getTarget()); 51 | 52 | //Test that toWire() will throw an exception if no target is set. 53 | $cname = new CNAME(); 54 | $this->expectException(\InvalidArgumentException::class); 55 | $this->expectExceptionMessage('Target must be set.'); 56 | $cname->toWire(); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /tests/Rdata/DnameTest.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace Badcow\DNS\Tests\Rdata; 15 | 16 | use Badcow\DNS\Rdata\DNAME; 17 | use PHPUnit\Framework\TestCase; 18 | 19 | class DnameTest extends TestCase 20 | { 21 | public function testSetTarget(): void 22 | { 23 | $target = 'foo.example.com.'; 24 | $dname = new DNAME(); 25 | $dname->setTarget($target); 26 | 27 | $this->assertEquals($target, $dname->getTarget()); 28 | } 29 | 30 | public function testOutput(): void 31 | { 32 | $target = 'foo.example.com.'; 33 | $dname = new DNAME(); 34 | $dname->setTarget($target); 35 | 36 | $this->assertEquals($target, $dname->toText()); 37 | $this->assertEquals($target, $dname->toText()); 38 | } 39 | 40 | public function testFromText(): void 41 | { 42 | $text = 'host.example.com.'; 43 | /** @var DNAME $cname */ 44 | $cname = new DNAME(); 45 | $cname->fromText($text); 46 | 47 | $this->assertEquals($text, $cname->getTarget()); 48 | } 49 | 50 | public function testWire(): void 51 | { 52 | $host = 'host.example.com.'; 53 | $expectation = chr(4).'host'.chr(7).'example'.chr(3).'com'.chr(0); 54 | 55 | /** @var DNAME $dname */ 56 | $dname = new DNAME(); 57 | $dname->fromWire($expectation); 58 | 59 | $this->assertEquals($expectation, $dname->toWire()); 60 | $this->assertEquals($host, $dname->getTarget()); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /tests/Edns/Option/UnknownOptionTest.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace Badcow\DNS\Tests\Edns\Option; 15 | 16 | use Badcow\DNS\Edns\Option\UnknownOption; 17 | use PHPUnit\Framework\TestCase; 18 | 19 | class UnknownOptionTest extends TestCase 20 | { 21 | /** 22 | * @var UnknownOption 23 | */ 24 | private $option; 25 | 26 | public function setUp(): void 27 | { 28 | $this->option = new UnknownOption(); 29 | } 30 | 31 | public function testGetterSetters(): void 32 | { 33 | $this->option->setOptionCode(123); 34 | $this->assertEquals(123, $this->option->getNameCode()); 35 | $this->assertEquals('OPTION123', $this->option->getName()); 36 | } 37 | 38 | public function testToWire(): void 39 | { 40 | $noData = new UnknownOption(); 41 | $this->assertEquals('', $noData->toWire()); 42 | 43 | $withData = new UnknownOption(); 44 | $withData->setData('HelloWorld'); 45 | $this->assertEquals('HelloWorld', $withData->toWire()); 46 | } 47 | 48 | public function testFromWire1(): void 49 | { 50 | $wire = ''; 51 | $noData = new UnknownOption(); 52 | $noData->fromWire($wire); 53 | $this->assertEmpty($noData->getData()); 54 | } 55 | 56 | public function testFromWire2(): void 57 | { 58 | $wire = 'HelloWorld'; 59 | $withData = new UnknownOption(); 60 | $withData->fromWire($wire); 61 | $this->assertEquals('HelloWorld', $withData->getData()); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /tests/Resources/google.com.cer: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIEvjCCA6agAwIBAgIQQ2AE7r1PEHwCAAAAAEiVEDANBgkqhkiG9w0BAQsFADBC 3 | MQswCQYDVQQGEwJVUzEeMBwGA1UEChMVR29vZ2xlIFRydXN0IFNlcnZpY2VzMRMw 4 | EQYDVQQDEwpHVFMgQ0EgMU8xMB4XDTE5MTAxNjEyMjgzMloXDTIwMDEwODEyMjgz 5 | MlowaDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCkNhbGlmb3JuaWExFjAUBgNVBAcT 6 | DU1vdW50YWluIFZpZXcxEzARBgNVBAoTCkdvb2dsZSBMTEMxFzAVBgNVBAMTDnd3 7 | dy5nb29nbGUuY29tMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEnoRygN85bi1T 8 | +J+CF22EIRSdS2JNyMvmHygVx3irNicagWKZpvE7b4WamxYg2AvqBsdvnPneOxHR 9 | Jc6Q0AJdQaOCAlMwggJPMA4GA1UdDwEB/wQEAwIHgDATBgNVHSUEDDAKBggrBgEF 10 | BQcDATAMBgNVHRMBAf8EAjAAMB0GA1UdDgQWBBTDmKwDi3Plr7hI6f+eDounoVvH 11 | 0DAfBgNVHSMEGDAWgBSY0fhuEOvPm+xgnxiQG6DrfQn9KzBkBggrBgEFBQcBAQRY 12 | MFYwJwYIKwYBBQUHMAGGG2h0dHA6Ly9vY3NwLnBraS5nb29nL2d0czFvMTArBggr 13 | BgEFBQcwAoYfaHR0cDovL3BraS5nb29nL2dzcjIvR1RTMU8xLmNydDAZBgNVHREE 14 | EjAQgg53d3cuZ29vZ2xlLmNvbTAhBgNVHSAEGjAYMAgGBmeBDAECAjAMBgorBgEE 15 | AdZ5AgUDMC8GA1UdHwQoMCYwJKAioCCGHmh0dHA6Ly9jcmwucGtpLmdvb2cvR1RT 16 | MU8xLmNybDCCAQMGCisGAQQB1nkCBAIEgfQEgfEA7wB1ALIeBcyLos2KIE6HZvkr 17 | uYolIGdr2vpw57JJUy3vi5BeAAABbdTBfH8AAAQDAEYwRAIgSt1msvylbbmaFBkF 18 | /mOKZW2ehZMW6gzrGPZN9dBBizoCICpT4KMdCJ55KlsYtXQdNKVewym61xy78Vap 19 | NhNzKoaDAHYAXqdz+d9WwOe1Nkh90EngMnqRmgyEoRIShBh1loFxRVgAAAFt1MF8 20 | lAAABAMARzBFAiEAr0C6rbCg/25yMzPNPyfmq0/gBErlWq20xzTMEk/b5EgCIDe3 21 | syzJYupXo7DjkfK8x/wZTRTeJeSas5m8BV1scV5CMA0GCSqGSIb3DQEBCwUAA4IB 22 | AQAKPpbaFgWefJfX3OKcEcX7ZCtCKf1PSx9mH5jN4NfKgV9UYmtUugLmqGcAXsSq 23 | dHYoZIKoJQ4DAhQ09BeWVwvknScj34fFIDD8V+zVTtgc/NSHeUgA+TzhAdeyCmce 24 | KglWSkywIV/h0rJgvA31HKmUCYDZypRbsVpqVFOwC1FMNXDE2YlyKXBLh8HzS4nv 25 | R+pcRM60RwEzi/SLkRoUeiG6wS0/V7xbPubMcwrpOj4InYWUDFVaL8IfHj4nSh8s 26 | bcqBGGPXLa/R42PJf1SJIGEgOzHaHp1atY8XLTc/zOYL/yb7zP/bPXGmdziKomxI 27 | gjO/YnjQ4O64SucdYAlAp5cJ 28 | -----END CERTIFICATE----- 29 | -------------------------------------------------------------------------------- /lib/Edns/Option/TCP_KEEPALIVE.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace Badcow\DNS\Edns\Option; 15 | 16 | /** 17 | * @see https://www.rfc-editor.org/rfc/rfc7828.html#section-3.1 18 | */ 19 | class TCP_KEEPALIVE implements OptionInterface 20 | { 21 | use OptionTrait; 22 | 23 | public const NAME = 'TCP_KEEPALIVE'; 24 | public const CODE = 11; 25 | 26 | /** 27 | * @var int|null 28 | */ 29 | protected $timeout; 30 | 31 | public function getTimeout(): ?int 32 | { 33 | return $this->timeout; 34 | } 35 | 36 | public function setTimeout(?int $timeout): void 37 | { 38 | $this->timeout = $timeout; 39 | } 40 | 41 | public function toWire(): string 42 | { 43 | if (null === $this->timeout) { 44 | return ''; 45 | } 46 | 47 | return pack('n', $this->timeout); 48 | } 49 | 50 | public function fromWire(string $optionValue, int &$offset = 0, ?int $optionLength = null): void 51 | { 52 | $optionLength = $optionLength ?? strlen($optionValue); 53 | if (0 !== $optionLength and 2 !== $optionLength) { 54 | throw new DecodeException(self::NAME, $optionValue); 55 | } 56 | if (2 === $optionLength) { 57 | $integers = unpack('ntimeout', $optionValue, $offset); 58 | if (false === $integers) { 59 | throw new DecodeException(self::NAME, $optionValue); 60 | } 61 | $offset += 2; 62 | $this->timeout = $integers['timeout']; 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /docs/Parser/Basic-Usage.md: -------------------------------------------------------------------------------- 1 | Basic Usage 2 | =========== 3 | 4 | ```php 5 | $file = file_get_contents('/path/to/example.com.txt'); 6 | 7 | $zone = Badcow\DNS\Parser\Parser::parse('example.com.', $file); 8 | ``` 9 | 10 | Simple as that. 11 | 12 | 13 | ## Example 14 | 15 | ### BIND Record 16 | ```text 17 | $ORIGIN example.com. 18 | $TTL 3600 19 | @ IN SOA ( 20 | example.com. ; MNAME 21 | post.example.com. ; RNAME 22 | 2014110501 ; SERIAL 23 | 3600 ; REFRESH 24 | 14400 ; RETRY 25 | 604800 ; EXPIRE 26 | 3600 ; MINIMUM 27 | ) 28 | 29 | ; NS RECORDS 30 | @ NS ns1.nameserver.com. 31 | @ NS ns2.nameserver.com. 32 | 33 | info TXT "This is some additional \"information\"" 34 | 35 | ; A RECORDS 36 | sub.domain A 192.168.1.42 ; This is a local ip. 37 | 38 | ; AAAA RECORDS 39 | ipv6.domain AAAA ::1 ; This is an IPv6 domain. 40 | 41 | ; MX RECORDS 42 | @ MX 10 mail-gw1.example.net. 43 | @ MX 20 mail-gw2.example.net. 44 | @ MX 30 mail-gw3.example.net. 45 | 46 | mail IN TXT "THIS IS SOME TEXT; WITH A SEMICOLON" 47 | ``` 48 | 49 | ### Processing the record 50 | ```php 51 | getName(); //Returns example.com. 59 | foreach ($zone->getResourceRecords() as $record) { 60 | $record->getName(); 61 | $record->getClass(); 62 | $record->getTtl(); 63 | $record->getRdata()->toText(); 64 | } 65 | ``` -------------------------------------------------------------------------------- /tests/Edns/Option/CLIENT_SUBNETTest.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace Badcow\DNS\Tests\Edns\Option; 15 | 16 | use Badcow\DNS\Edns\Option\CLIENT_SUBNET; 17 | use PHPUnit\Framework\TestCase; 18 | 19 | class CLIENT_SUBNETTest extends TestCase 20 | { 21 | /** 22 | * @var CLIENT_SUBNET 23 | */ 24 | private $option; 25 | 26 | public function setUp(): void 27 | { 28 | $this->option = new CLIENT_SUBNET(); 29 | } 30 | 31 | public function testGetterSetters(): void 32 | { 33 | $this->assertEquals('CLIENT_SUBNET', $this->option->getName()); 34 | } 35 | 36 | public function testToWire(): void 37 | { 38 | $address = '200.100.50.1'; 39 | $this->option->setFamily(CLIENT_SUBNET::FAMILY_IPV4); // 0x0001 40 | $this->option->setSourceNetmask(24); // 0x18 41 | $this->option->setScopeNetmask(22); // 0x16 42 | $this->option->setAddress($address); 43 | $expectation = "\x00\x01\x18\x16".inet_pton($address); 44 | 45 | $this->assertEquals($expectation, $this->option->toWire()); 46 | } 47 | 48 | public function testFromWire(): void 49 | { 50 | $address = '200.100.50.1'; 51 | $wire = "\x00\x01\x18\x16".inet_pton($address); 52 | $option = new CLIENT_SUBNET(); 53 | $option->fromWire($wire); 54 | $this->assertEquals(CLIENT_SUBNET::FAMILY_IPV4, $option->getFamily()); 55 | $this->assertEquals(24, $option->getSourceNetmask()); 56 | $this->assertEquals(22, $option->getScopeNetmask()); 57 | $this->assertEquals($address, $option->getAddress()); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /tests/Parser/CustomHandlerTest.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace Badcow\DNS\Tests\Parser; 15 | 16 | use Badcow\DNS\Classes; 17 | use Badcow\DNS\Parser\ParseException; 18 | use Badcow\DNS\Parser\Parser; 19 | use Badcow\DNS\Rdata\TXT; 20 | use PHPUnit\Framework\TestCase; 21 | 22 | class CustomHandlerTest extends TestCase 23 | { 24 | public function spfHandler(\ArrayIterator $iterator): TXT 25 | { 26 | $string = ''; 27 | while ($iterator->valid()) { 28 | $string .= $iterator->current().' '; 29 | $iterator->next(); 30 | } 31 | $string = trim($string, ' "'); //Remove whitespace and quotes 32 | 33 | $spf = new TXT(); 34 | $spf->setText($string); 35 | 36 | return $spf; 37 | } 38 | 39 | /** 40 | * @throws ParseException 41 | */ 42 | public function testCustomHandler(): void 43 | { 44 | $customHandlers = ['SPF' => [$this, 'spfHandler']]; 45 | 46 | $record = 'example.com. 7200 IN SPF "v=spf1 a mx ip4:69.64.153.131 include:_spf.google.com ~all"'; 47 | $parser = new Parser($customHandlers); 48 | $zone = $parser->makeZone('example.com.', $record); 49 | $rr = $zone->getResourceRecords()[0]; 50 | 51 | $this->assertEquals('TXT', $rr->getType()); 52 | $this->assertEquals('example.com.', $rr->getName()); 53 | $this->assertEquals(7200, $rr->getTtl()); 54 | $this->assertEquals(Classes::INTERNET, $rr->getClass()); 55 | $this->assertNotNull($rr->getRdata()); 56 | $this->assertEquals('v=spf1 a mx ip4:69.64.153.131 include:_spf.google.com ~all', $rr->getRdata()->getText()); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /tests/Rdata/SigTest.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace Badcow\DNS\Tests\Rdata; 15 | 16 | use Badcow\DNS\Algorithms; 17 | use Badcow\DNS\Rdata\A; 18 | use Badcow\DNS\Rdata\Factory; 19 | use PHPUnit\Framework\TestCase; 20 | 21 | class SigTest extends TestCase 22 | { 23 | public function testFactory(): void 24 | { 25 | $signature = base64_decode('oJB1W6WNGv+ldvQ3WDG0MQkg5IEhjRip8WTrPYGv07h108dUKGMeDPKijVCHX3DDKdfb+v6oB9wfuh3DTJXUA'. 26 | 'fI/M0zmO/zz8bW0Rznl8O3tGNazPwQKkRN20XPXV6nwwfoXmJQbsLNrLfkGJ5D6fwFm8nN+6pBzeDQfsS3Ap3o='); 27 | 28 | $sig = Factory::SIG( 29 | A::TYPE, 30 | Algorithms::RSASHA1, 31 | 3, 32 | 86400, 33 | \DateTime::createFromFormat('Ymd H:i:s', '20220101 00:00:00'), 34 | \DateTime::createFromFormat('Ymd H:i:s', '20180101 00:00:00'), 35 | 2642, 36 | 'example.com.', 37 | $signature 38 | ); 39 | 40 | $this->assertEquals(A::TYPE, $sig->getTypeCovered()); 41 | $this->assertEquals(Algorithms::RSASHA1, $sig->getAlgorithm()); 42 | $this->assertEquals(3, $sig->getLabels()); 43 | $this->assertEquals(86400, $sig->getOriginalTtl()); 44 | $this->assertEquals(\DateTime::createFromFormat('Ymd H:i:s', '20220101 00:00:00'), $sig->getSignatureExpiration()); 45 | $this->assertEquals(\DateTime::createFromFormat('Ymd H:i:s', '20180101 00:00:00'), $sig->getSignatureInception()); 46 | $this->assertEquals(2642, $sig->getKeyTag()); 47 | $this->assertEquals('example.com.', $sig->getSignersName()); 48 | $this->assertEquals($signature, $sig->getSignature()); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /lib/Rdata/RdataInterface.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace Badcow\DNS\Rdata; 15 | 16 | interface RdataInterface 17 | { 18 | /** 19 | * Get the string representation of the Rdata type. 20 | * 21 | * @return string Rdata type, e.g. "A", "MX", "NS", etc. 22 | */ 23 | public function getType(): string; 24 | 25 | /** 26 | * Get the integer type code of the Rdata type as defined by IANA. 27 | * 28 | * @return int IANA Rdata type code 29 | */ 30 | public function getTypeCode(): int; 31 | 32 | /** 33 | * Return the string representation of the Rdata. 34 | * 35 | * @return string formatted Rdata output as would appear in BIND records 36 | */ 37 | public function toText(): string; 38 | 39 | /** 40 | * Return a DNS Server response formatted representation of the Rdata. 41 | * 42 | * @return string packed binary form of Rdata 43 | */ 44 | public function toWire(): string; 45 | 46 | /** 47 | * Populate Rdata object from its textual representation. 48 | * 49 | * @param string $text Rendered Rdata text to populate object 50 | */ 51 | public function fromText(string $text): void; 52 | 53 | /** 54 | * Populate Rdata from its wire representation. 55 | * 56 | * @param string $rdata packed binary form of Rdata 57 | * @param int $offset the current offset or pointer, position of the start of Rdata relative to the whole $rdata string 58 | * @param int|null $rdLength the length of the Rdata string, if null it is taken to be the whole $rdata parameter string 59 | */ 60 | public function fromWire(string $rdata, int &$offset = 0, ?int $rdLength = null): void; 61 | } 62 | -------------------------------------------------------------------------------- /tests/Edns/Option/TCP_KEEPALIVETest.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace Badcow\DNS\Tests\Edns\Option; 15 | 16 | use Badcow\DNS\Edns\Option\DecodeException; 17 | use Badcow\DNS\Edns\Option\TCP_KEEPALIVE; 18 | use PHPUnit\Framework\TestCase; 19 | 20 | class TCP_KEEPALIVETest extends TestCase 21 | { 22 | /** 23 | * @var TCP_KEEPALIVE 24 | */ 25 | private $option; 26 | 27 | public function setUp(): void 28 | { 29 | $this->option = new TCP_KEEPALIVE(); 30 | } 31 | 32 | public function testGetterSetters(): void 33 | { 34 | $this->assertEquals('TCP_KEEPALIVE', $this->option->getName()); 35 | } 36 | 37 | public function testToWire(): void 38 | { 39 | $noLimit = new TCP_KEEPALIVE(); 40 | $this->assertEquals('', $noLimit->toWire()); 41 | 42 | $withLimit = new TCP_KEEPALIVE(); 43 | $withLimit->setTimeout(1000); 44 | $this->assertEquals("\x03\xE8", $withLimit->toWire()); 45 | } 46 | 47 | public function testFromWire1(): void 48 | { 49 | $wire = ''; 50 | $noLimit = new TCP_KEEPALIVE(); 51 | $noLimit->fromWire($wire); 52 | $this->assertNull($noLimit->getTimeout()); 53 | } 54 | 55 | public function testFromWire2(): void 56 | { 57 | $this->expectException(DecodeException::class); 58 | $wire = "\xFF\xFF\xFF"; 59 | $wrongTimeout = new TCP_KEEPALIVE(); 60 | $wrongTimeout->fromWire($wire); 61 | } 62 | 63 | public function testFromWire3(): void 64 | { 65 | $wire = "\x07\xD0"; 66 | $withLimit = new TCP_KEEPALIVE(); 67 | $withLimit->fromWire($wire); 68 | $this->assertEquals(2000, $withLimit->getTimeout()); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /tests/Rdata/SpfTest.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace Badcow\DNS\Tests\Rdata; 15 | 16 | use Badcow\DNS\Rdata\Factory; 17 | use Badcow\DNS\Rdata\SPF; 18 | use PHPUnit\Framework\TestCase; 19 | 20 | class SpfTest extends TestCase 21 | { 22 | public function testFromText(): void 23 | { 24 | $text = '"v=spf1 ip4:192.0.2.0/24 ip4:198.51.100.123 a -all"'; 25 | 26 | /** @var SPF $spf */ 27 | $spf = new SPF(); 28 | $spf->fromText($text); 29 | 30 | $this->assertEquals('SPF', $spf->getType()); 31 | $this->assertEquals('v=spf1 ip4:192.0.2.0/24 ip4:198.51.100.123 a -all', $spf->getText()); 32 | } 33 | 34 | public function testToText(): void 35 | { 36 | $spf = new SPF(); 37 | $spf->setText('v=spf1 ip4:192.0.2.0/24 ip4:198.51.100.123 a -all'); 38 | 39 | $this->assertEquals('"v=spf1 ip4:192.0.2.0/24 ip4:198.51.100.123 a -all"', $spf->toText()); 40 | $this->assertEquals('SPF', $spf->getType()); 41 | } 42 | 43 | public function testWire(): void 44 | { 45 | $wireFormat = chr(49).'v=spf1 ip4:192.0.2.0/24 ip4:198.51.100.123 a -all'; 46 | $offset = 1; 47 | $rdLength = 49; 48 | 49 | $spf = new SPF(); 50 | $spf->setText('v=spf1 ip4:192.0.2.0/24 ip4:198.51.100.123 a -all'); 51 | 52 | $fromWire = new SPF(); 53 | $fromWire->fromWire($wireFormat, $offset, $rdLength); 54 | $this->assertEquals($spf, $fromWire); 55 | } 56 | 57 | public function testFactory(): void 58 | { 59 | $wireFormat = 'v=spf1 ip4:192.0.2.0/24 ip4:198.51.100.123 a -all'; 60 | 61 | $spf = new SPF(); 62 | $spf->setText($wireFormat); 63 | 64 | $this->assertEquals($spf, Factory::SPF($wireFormat)); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /lib/Edns/Option/UnknownOption.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace Badcow\DNS\Edns\Option; 15 | 16 | class UnknownOption implements OptionInterface 17 | { 18 | /** 19 | * @var int 20 | */ 21 | private $optionCode; 22 | 23 | /** 24 | * @var string|null 25 | */ 26 | private $data; 27 | 28 | /** 29 | * @var string|null 30 | */ 31 | private $name; 32 | 33 | public function setOptionCode(int $optionCode): void 34 | { 35 | $this->optionCode = $optionCode; 36 | } 37 | 38 | public function getNameCode(): int 39 | { 40 | return $this->optionCode; 41 | } 42 | 43 | public function getName(): string 44 | { 45 | if (null !== $this->name) { 46 | return $this->name; 47 | } 48 | 49 | return 'OPTION'.$this->optionCode; 50 | } 51 | 52 | public function setName(string $name): void 53 | { 54 | $this->name = $name; 55 | } 56 | 57 | /** 58 | * @return string 59 | */ 60 | public function getData(): ?string 61 | { 62 | return $this->data; 63 | } 64 | 65 | /** 66 | * @param string $data 67 | */ 68 | public function setData(?string $data): void 69 | { 70 | $this->data = $data; 71 | } 72 | 73 | public function getCode(): int 74 | { 75 | return $this->optionCode; 76 | } 77 | 78 | public function toWire(): string 79 | { 80 | return $this->data ?? ''; 81 | } 82 | 83 | public function fromWire(string $optionValue, int &$offset = 0, ?int $optionLength = null): void 84 | { 85 | $optionLength = $optionLength ?? strlen($optionValue); 86 | $this->setData(substr($optionValue, $offset, $optionLength)); 87 | $offset += $optionLength; 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /lib/Edns/Option/Factory.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace Badcow\DNS\Edns\Option; 15 | 16 | class Factory 17 | { 18 | /** 19 | * Creates a new Option object from a name. 20 | * 21 | * @throws UnsupportedOptionException 22 | */ 23 | public static function newOptionFromName(string $name): OptionInterface 24 | { 25 | if (!self::isOptionImplemented($name)) { 26 | throw new UnsupportedOptionException($name); 27 | } 28 | 29 | $className = self::getOptionClassName($name); 30 | $optionInterface = new $className(); 31 | if (!$optionInterface instanceof OptionInterface) { 32 | throw new \UnexpectedValueException(sprintf('Badcow\DNS\Edns\Option expected; "%s" instantiated.', gettype($optionInterface))); 33 | } 34 | 35 | return $optionInterface; 36 | } 37 | 38 | /** 39 | * @throws UnsupportedOptionException 40 | */ 41 | public static function newOptionFromId(int $id): OptionInterface 42 | { 43 | return self::newOptionFromName(Codes::getName($id)); 44 | } 45 | 46 | public static function isOptionImplemented(string $name): bool 47 | { 48 | return class_exists(self::getOptionClassName($name)); 49 | } 50 | 51 | public static function isOptionCodeImplemented(int $optionCode): bool 52 | { 53 | try { 54 | return self::isOptionImplemented(Codes::getName($optionCode)); 55 | } catch (UnsupportedOptionException $e) { 56 | return false; 57 | } 58 | } 59 | 60 | /** 61 | * Get the fully qualified class name of the Option class for $option. 62 | */ 63 | public static function getOptionClassName(string $option): string 64 | { 65 | return __NAMESPACE__.'\\'.strtoupper($option); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /tests/Parser/ResourceRecordIteratorTest.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace Badcow\DNS\Tests\Parser; 15 | 16 | use Badcow\DNS\Parser\ResourceRecordIterator; 17 | use PHPUnit\Framework\TestCase; 18 | 19 | class ResourceRecordIteratorTest extends TestCase 20 | { 21 | public function testNavigation(): void 22 | { 23 | $string = 'this is a string consisting of some words'; 24 | $iterator = new ResourceRecordIterator($string); 25 | 26 | $this->assertEquals(0, $iterator->key()); 27 | 28 | $iterator->end(); 29 | $this->assertEquals('words', $iterator->current()); 30 | 31 | $iterator->next(); 32 | $this->assertFalse($iterator->valid()); 33 | 34 | $iterator->prev(); 35 | $this->assertEquals('words', $iterator->current()); 36 | 37 | $iterator->seek(2); 38 | $this->assertEquals('a', $iterator->current()); 39 | 40 | $iterator->prev(); 41 | $this->assertEquals('is', $iterator->current()); 42 | 43 | $iterator->seek(0); 44 | $this->assertEquals('this', $iterator->current()); 45 | 46 | $this->expectException(\OutOfBoundsException::class); 47 | $iterator->prev(); 48 | } 49 | 50 | public function testGetRemainingAsString(): void 51 | { 52 | $string = 'this is a string consisting of some words'; 53 | $iterator = new ResourceRecordIterator($string); 54 | $iterator->seek(3); 55 | 56 | $this->assertEquals('string consisting of some words', $iterator->getRemainingAsString()); 57 | } 58 | 59 | public function testToString(): void 60 | { 61 | $string = 'this is a string consisting of some words'; 62 | $iterator = new ResourceRecordIterator($string); 63 | 64 | $this->assertEquals($string, (string) $iterator); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /tests/Rdata/RpTest.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace Badcow\DNS\Tests\Rdata; 15 | 16 | use Badcow\DNS\Rdata\Factory; 17 | use Badcow\DNS\Rdata\RP; 18 | use PHPUnit\Framework\TestCase; 19 | 20 | class RpTest extends TestCase 21 | { 22 | public function testFactory(): void 23 | { 24 | $expectation = 'louie.trantor.umd.edu. lam1.people.umd.edu.'; 25 | $rp = Factory::RP('louie.trantor.umd.edu.', 'lam1.people.umd.edu.'); 26 | 27 | $this->assertEquals($expectation, $rp->toText()); 28 | } 29 | 30 | public function testFromText(): void 31 | { 32 | $rp = new RP(); 33 | $rp->fromText('louie.trantor.umd.edu. lam1.people.umd.edu.'); 34 | 35 | $this->assertEquals('louie.trantor.umd.edu.', $rp->getMailboxDomainName()); 36 | $this->assertEquals('lam1.people.umd.edu.', $rp->getTxtDomainName()); 37 | } 38 | 39 | public function testToText(): void 40 | { 41 | $expectation = 'louie.trantor.umd.edu. lam1.people.umd.edu.'; 42 | $rp = new RP(); 43 | $rp->setMailboxDomainName('louie.trantor.umd.edu.'); 44 | $rp->setTxtDomainName('lam1.people.umd.edu.'); 45 | 46 | $this->assertEquals($expectation, $rp->toText()); 47 | } 48 | 49 | public function testWire(): void 50 | { 51 | $expectation = chr(5).'louie'.chr(7).'trantor'.chr(3).'umd'.chr(3).'edu'.chr(0). 52 | chr(4).'lam1'.chr(6).'people'.chr(3).'umd'.chr(3).'edu'.chr(0); 53 | 54 | $rp = new RP(); 55 | $rp->setMailboxDomainName('louie.trantor.umd.edu.'); 56 | $rp->setTxtDomainName('lam1.people.umd.edu.'); 57 | 58 | $this->assertEquals($expectation, $rp->toWire()); 59 | $fromWire = new RP(); 60 | $fromWire->fromWire($expectation); 61 | $this->assertEquals($rp, $fromWire); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /lib/Parser/StringIterator.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace Badcow\DNS\Parser; 15 | 16 | class StringIterator extends \ArrayIterator 17 | { 18 | /** 19 | * StringIterator constructor. 20 | */ 21 | public function __construct(string $string = '') 22 | { 23 | parent::__construct(str_split($string)); 24 | } 25 | 26 | /** 27 | * Test if current character is equal to a value, or (if $value is an array) is one of the values in the array. 28 | * 29 | * @param string|array $value test if current character is equal to, or is in, $value 30 | * 31 | * @return bool true if current character is, or is one of, the values 32 | */ 33 | public function is($value): bool 34 | { 35 | if (is_array($value)) { 36 | return in_array($this->current(), $value); 37 | } 38 | 39 | return (string) $value === $this->current(); 40 | } 41 | 42 | /** 43 | * Test if current character is not equal to a value, or (if $value is an array) is not any of the values in the array. 44 | * 45 | * @param string|array $value test if current character is not equal to, or is not any of, $value 46 | * 47 | * @return bool true if current character is not, or is not one of, the values 48 | */ 49 | public function isNot($value): bool 50 | { 51 | return !$this->is($value); 52 | } 53 | 54 | public function getRemainingAsString(): string 55 | { 56 | $string = ''; 57 | while ($this->valid()) { 58 | $string .= $this->current(); 59 | $this->next(); 60 | } 61 | 62 | return $string; 63 | } 64 | 65 | /** 66 | * @return string 67 | */ 68 | public function __toString() 69 | { 70 | return implode('', $this->getArrayCopy()); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /tests/Rdata/CdsTest.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace Badcow\DNS\Tests\Rdata; 15 | 16 | use Badcow\DNS\Algorithms; 17 | use Badcow\DNS\Rdata\CDS; 18 | use Badcow\DNS\Rdata\Factory; 19 | use PHPUnit\Framework\TestCase; 20 | 21 | class CdsTest extends TestCase 22 | { 23 | private static $digest = '2BB183AF5F22588179A53B0A98631FAD1A292118'; 24 | 25 | public function testFactory(): void 26 | { 27 | $keyTag = 60485; 28 | $ds = Factory::CDS($keyTag, Algorithms::RSASHA1, hex2bin(self::$digest), CDS::DIGEST_SHA1); 29 | 30 | $this->assertEquals($keyTag, $ds->getKeyTag()); 31 | $this->assertEquals(Algorithms::RSASHA1, $ds->getAlgorithm()); 32 | $this->assertEquals(hex2bin(self::$digest), $ds->getDigest()); 33 | $this->assertEquals(CDS::DIGEST_SHA1, $ds->getDigestType()); 34 | } 35 | 36 | public function testFromText(): void 37 | { 38 | $expectation = new CDS(); 39 | $expectation->setKeyTag(60485); 40 | $expectation->setAlgorithm(Algorithms::RSASHA1); 41 | $expectation->setDigestType(CDS::DIGEST_SHA1); 42 | $expectation->setDigest(hex2bin(self::$digest)); 43 | 44 | $cds = new CDS(); 45 | $cds->fromText('60485 5 1 '.self::$digest); 46 | $this->assertInstanceOf(CDS::class, $cds); 47 | $this->assertEquals($expectation, $cds); 48 | } 49 | 50 | public function testWire(): void 51 | { 52 | $cds = new CDS(); 53 | $cds->setKeyTag(60485); 54 | $cds->setAlgorithm(Algorithms::RSASHA1); 55 | $cds->setDigestType(CDS::DIGEST_SHA1); 56 | $cds->setDigest(hex2bin(self::$digest)); 57 | $wireFormat = $cds->toWire(); 58 | $fromWire = new CDS(); 59 | $fromWire->fromWire($wireFormat); 60 | 61 | $this->assertInstanceOf(CDS::class, $fromWire); 62 | $this->assertEquals($cds, $fromWire); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /tests/Rdata/DlvTest.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace Badcow\DNS\Tests\Rdata; 15 | 16 | use Badcow\DNS\Algorithms; 17 | use Badcow\DNS\Rdata\DLV; 18 | use Badcow\DNS\Rdata\Factory; 19 | use PHPUnit\Framework\TestCase; 20 | 21 | class DlvTest extends TestCase 22 | { 23 | private static $digest = '2BB183AF5F22588179A53B0A98631FAD1A292118'; 24 | 25 | public function testFactory(): void 26 | { 27 | $keyTag = 60485; 28 | $ds = Factory::DLV($keyTag, Algorithms::RSASHA1, hex2bin(self::$digest), DLV::DIGEST_SHA1); 29 | 30 | $this->assertEquals($keyTag, $ds->getKeyTag()); 31 | $this->assertEquals(Algorithms::RSASHA1, $ds->getAlgorithm()); 32 | $this->assertEquals(hex2bin(self::$digest), $ds->getDigest()); 33 | $this->assertEquals(DLV::DIGEST_SHA1, $ds->getDigestType()); 34 | } 35 | 36 | public function testFromText(): void 37 | { 38 | $expectation = new DLV(); 39 | $expectation->setKeyTag(60485); 40 | $expectation->setAlgorithm(Algorithms::RSASHA1); 41 | $expectation->setDigestType(DLV::DIGEST_SHA1); 42 | $expectation->setDigest(hex2bin(self::$digest)); 43 | 44 | $dlv = new DLV(); 45 | $dlv->fromText('60485 5 1 '.self::$digest); 46 | $this->assertInstanceOf(DLV::class, $dlv); 47 | $this->assertEquals($expectation, $dlv); 48 | } 49 | 50 | public function testWire(): void 51 | { 52 | $dlv = new DLV(); 53 | $dlv->setKeyTag(60485); 54 | $dlv->setAlgorithm(Algorithms::RSASHA1); 55 | $dlv->setDigestType(DLV::DIGEST_SHA1); 56 | $dlv->setDigest(hex2bin(self::$digest)); 57 | $wireFormat = $dlv->toWire(); 58 | $fromWire = new DLV(); 59 | $fromWire->fromWire($wireFormat); 60 | 61 | $this->assertInstanceOf(DLV::class, $fromWire); 62 | $this->assertEquals($dlv, $fromWire); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /lib/Rdata/RP.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace Badcow\DNS\Rdata; 15 | 16 | use Badcow\DNS\Message; 17 | use Badcow\DNS\Parser\Tokens; 18 | 19 | /** 20 | * {@link https://tools.ietf.org/html/rfc1183}. 21 | */ 22 | class RP implements RdataInterface 23 | { 24 | use RdataTrait; 25 | 26 | public const TYPE = 'RP'; 27 | public const TYPE_CODE = 17; 28 | 29 | /** 30 | * @var string 31 | */ 32 | private $mailboxDomainName; 33 | 34 | /** 35 | * @var string 36 | */ 37 | private $txtDomainName; 38 | 39 | public function getMailboxDomainName(): string 40 | { 41 | return $this->mailboxDomainName; 42 | } 43 | 44 | public function setMailboxDomainName(string $mailboxDomainName): void 45 | { 46 | $this->mailboxDomainName = $mailboxDomainName; 47 | } 48 | 49 | public function getTxtDomainName(): string 50 | { 51 | return $this->txtDomainName; 52 | } 53 | 54 | public function setTxtDomainName(string $txtDomainName): void 55 | { 56 | $this->txtDomainName = $txtDomainName; 57 | } 58 | 59 | public function toText(): string 60 | { 61 | return sprintf('%s %s', $this->mailboxDomainName, $this->txtDomainName); 62 | } 63 | 64 | public function toWire(): string 65 | { 66 | return Message::encodeName($this->mailboxDomainName).Message::encodeName($this->txtDomainName); 67 | } 68 | 69 | public function fromText(string $text): void 70 | { 71 | $rdata = explode(Tokens::SPACE, $text); 72 | $this->setMailboxDomainName($rdata[0]); 73 | $this->setTxtDomainName($rdata[1]); 74 | } 75 | 76 | public function fromWire(string $rdata, int &$offset = 0, ?int $rdLength = null): void 77 | { 78 | $this->setMailboxDomainName(Message::decodeName($rdata, $offset)); 79 | $this->setTxtDomainName(Message::decodeName($rdata, $offset)); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /tests/Resources/example.com_filled-out.txt: -------------------------------------------------------------------------------- 1 | $ORIGIN example.com. 2 | $TTL 3600 3 | example.com. 3600 IN SOA ( 4 | example.com. ; MNAME 5 | post.example.com. ; RNAME 6 | 2014110501 ; SERIAL 7 | 3600 ; REFRESH 8 | 14400 ; RETRY 9 | 604800 ; EXPIRE 10 | 3600 ; MINIMUM 11 | ) 12 | 13 | ; NS RECORDS 14 | example.com. 3600 IN NS ns1.nameserver.com. 15 | example.com. 3600 IN NS ns2.nameserver.com. 16 | 17 | ; A RECORDS 18 | sub.domain.example.com. 3600 IN A 192.168.1.42; This is a local ip. 19 | 20 | ; AAAA RECORDS 21 | ipv6.domain.example.com. 3600 IN AAAA 0000:0000:0000:0000:0000:0000:0000:0001; This is an IPv6 domain. 22 | 23 | ; MX RECORDS 24 | example.com. 3600 IN MX 10 mail-gw1.example.net. 25 | example.com. 3600 IN MX 20 mail-gw2.example.net. 26 | example.com. 3600 IN MX 30 mail-gw3.example.net. 27 | 28 | ; LOC RECORDS 29 | canberra.example.com. 3600 IN LOC ( 30 | 35 18 27.000 S ; LATITUDE 31 | 149 7 27.840 E ; LONGITUDE 32 | 500.00m ; ALTITUDE 33 | 20.12m ; SIZE 34 | 200.30m ; HORIZONTAL PRECISION 35 | 300.10m ; VERTICAL PRECISION 36 | ); This is Canberra 37 | 38 | ; SRV RECORDS 39 | _ftp._tcp.example.com. 3600 IN SRV 10 10 21 files.example.com. 40 | 41 | ; APL RECORDS 42 | multicast.example.com. 3600 IN APL ( 43 | 1:192.168.0.0/23 44 | 2:2001:acad:1::/112 45 | !1:192.168.1.64/28 46 | !2:2001:acad:1::8/128 47 | ) 48 | -------------------------------------------------------------------------------- /lib/Classes.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace Badcow\DNS; 15 | 16 | class Classes 17 | { 18 | public const INTERNET = 'IN'; 19 | public const CSNET = 'CS'; 20 | public const CHAOS = 'CH'; 21 | public const HESIOD = 'HS'; 22 | 23 | /** 24 | * @var array 25 | */ 26 | public static $classes = [ 27 | self::CHAOS => 'CHAOS', 28 | self::CSNET => 'CSNET', 29 | self::HESIOD => 'Hesiod', 30 | self::INTERNET => 'Internet', 31 | ]; 32 | 33 | public const CLASS_IDS = [ 34 | self::CHAOS => 3, 35 | self::CSNET => 2, 36 | self::HESIOD => 4, 37 | self::INTERNET => 1, 38 | ]; 39 | 40 | /** 41 | * @const string[] 42 | */ 43 | public const IDS_CLASSES = [ 44 | 1 => 'IN', 45 | 2 => 'CS', 46 | 3 => 'CH', 47 | 4 => 'HS', 48 | ]; 49 | 50 | /** 51 | * Determine if a class is valid. 52 | */ 53 | public static function isValid(string $class): bool 54 | { 55 | if (array_key_exists($class, self::$classes)) { 56 | return true; 57 | } 58 | 59 | return 1 === preg_match('/^CLASS\d+$/', $class); 60 | } 61 | 62 | /** 63 | * @throws \InvalidArgumentException 64 | */ 65 | public static function getClassId(string $className): int 66 | { 67 | if (!self::isValid($className)) { 68 | throw new \InvalidArgumentException(sprintf('Class "%s" is not a valid DNS class.', $className)); 69 | } 70 | 71 | if (1 === preg_match('/^CLASS(\d+)$/', $className, $matches)) { 72 | return (int) $matches[1]; 73 | } 74 | 75 | return self::CLASS_IDS[$className]; 76 | } 77 | 78 | public static function getClassName(int $classId): string 79 | { 80 | if (array_key_exists($classId, self::IDS_CLASSES)) { 81 | return self::IDS_CLASSES[$classId]; 82 | } 83 | 84 | return 'CLASS'.$classId; 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /tests/QuestionTest.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace Badcow\DNS\Tests; 15 | 16 | use Badcow\DNS\Question; 17 | use Badcow\DNS\Rdata\UnsupportedTypeException; 18 | use PHPUnit\Framework\TestCase; 19 | 20 | class QuestionTest extends TestCase 21 | { 22 | /** 23 | * @throws UnsupportedTypeException 24 | */ 25 | public function testToWire(): void 26 | { 27 | $q = new Question(); 28 | $q->setName('example.com.'); 29 | $q->setType('NS'); 30 | $q->setClass('IN'); 31 | 32 | $expectation = chr(7).'example'.chr(3).'com'.chr(0).pack('nn', 2, 1); 33 | 34 | $this->assertEquals($expectation, $q->toWire()); 35 | } 36 | 37 | public function testSetClassThrowsException(): void 38 | { 39 | $this->expectException(\InvalidArgumentException::class); 40 | $this->expectExceptionMessage('Invalid class: "65536".'); 41 | $q = new Question(); 42 | $q->setClassId(65536); 43 | } 44 | 45 | public function testSetNameThrowsException(): void 46 | { 47 | $this->expectException(\InvalidArgumentException::class); 48 | $this->expectExceptionMessage('"abc123.com" is not a fully qualified domain name.'); 49 | $q = new Question(); 50 | $q->setName('abc123.com'); 51 | } 52 | 53 | public function testFromWire(): void 54 | { 55 | $wireFormat = chr(7).'example'.chr(3).'com'.chr(0).pack('nn', 2, 1); 56 | 57 | $q = Question::fromWire($wireFormat); 58 | 59 | $this->assertEquals('example.com.', $q->getName()); 60 | $this->assertEquals('NS', $q->getType()); 61 | $this->assertEquals('IN', $q->getClass()); 62 | } 63 | 64 | public function testSetTypeCodeThrowsException(): void 65 | { 66 | $this->expectException(\DomainException::class); 67 | $this->expectExceptionMessage('TypeCode must be an unsigned 16-bit integer. "65536" given.'); 68 | $q = new Question(); 69 | $q->setTypeCode(65536); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /docs/Message.md: -------------------------------------------------------------------------------- 1 | Message 2 | ======= 3 | Badcow DNS can parse a DNS message and create PHP objects representing the message. 4 | 5 | ## Example 6 | ### Parsing a message 7 | Say we have a message as a binary encoded string. The message is represented in hexadecimal below: 8 | ``` 9 | 000a 8580 0001 0003 0000 0003 0376 6978 0363 6f6d 0000 0200 01c0 0c00 0200 0100 000e 1000 0b05 6973 7276 3102 7061 c00c 10 | c00c 0002 0001 0000 0e10 0009 066e 732d 6578 74c0 0cc0 0c00 0200 0100 000e 1000 0e03 6e73 3104 676e 6163 0363 6f6d 00c0 11 | 2500 0100 0100 000e 1000 04cc 98b8 86c0 3c00 0100 0100 000e 1000 04cc 98b8 40c0 5100 0100 0100 02a1 4a00 04c6 97f8 f6 12 | ``` 13 | _Note: It is not necessary for you to be familiar with the encoding of a DNS message packet, but an understanding of the key 14 | components of a DNS message is of great assistance. [Please see RFC1035 section 4](https://tools.ietf.org/html/rfc1035#section-4) 15 | for details of the message format._ 16 | 17 | The above message can be parsed into a PHP object... 18 | ```php 19 | $binaryMessage = '...'; // Some binary string 20 | $message = Badcow\DNS\Message::fromWire($binaryMessage); //This will return a Badcow\DNS\Message object. 21 | echo $message->getId(); //The message ID, in this case: 10. 22 | echo $message->isResponse() ? 'Response' : 'Query'; //Whether the message is a response or query. 23 | 24 | //Iterate over the QUESTION section of the message. These are a collection of Badcow\DNS\Question objects. 25 | foreach ($message->getQuestions() as $question) { 26 | echo $question->getName(); 27 | echo $question->getType(); 28 | } 29 | 30 | //Iterate of the ANSWER section. These are \Badcow\DNS\ResourceRecord objects. 31 | foreach ($message->getAnswers() as $answer) { 32 | echo $answer->getName(); 33 | echo $answer->getType(); 34 | echo $answer->getRdata()->toText(); //The rdata 35 | } 36 | ``` 37 | 38 | ### Encoding a message 39 | ```php 40 | $question = new \Badcow\DNS\Question(); 41 | $question->setName('example.com.'); 42 | $question->setClass('IN'); 43 | $question->setType('A'); 44 | 45 | $message = new \Badcow\DNS\Message(); 46 | $message->setId(123); 47 | $message->setQuery(true); 48 | 49 | $message->setRecursionDesired(true); 50 | $message->addQuestion($question); 51 | 52 | $binaryMessage = $message->toWire(); 53 | 54 | ``` -------------------------------------------------------------------------------- /docs/Parser/Include-Directive.md: -------------------------------------------------------------------------------- 1 | $INCLUDE Directive 2 | ================== 3 | [RFC-1035](https://www.ietf.org/rfc/rfc1035.txt) section 5.1 permits the `$INCLUDE` control entry for importing external 4 | files: 5 | ``` 6 | $INCLUDE [] [] 7 | ``` 8 | In the specification, the `file-name` can be absolute or relative; the `domain-name` is optional and, if set, specifies 9 | the `$ORIGIN` to be used for the imported sub-zone. _Exempli gratia:_ 10 | ``` 11 | $INCLUDE headquarters.example.com.db headquarters.example.com. ;This imports the headquarters subdomain. 12 | ``` 13 | The Badcow DNS Parser will import these files if you have specified a class implementing the `Badcow\DNS\Parser\ZoneFileFetcherInterface`. 14 | This class is then passed to the parser as the second argument on its constructor: 15 | ``` 16 | \Badcow\DNS\Parser\Parser::__construct(array $rdataHandlers, ZoneFileFetcherInterface $fetcher); 17 | ``` 18 | The interface has only one method to be implemented, `ZoneFileFetcherInterface::fetch(string $path)`. This is called within 19 | the parser to fetch included files. This has been implemented to ensure that arbitrary files cannot be included by a zone. 20 | Additionally, it may be the case that the zone files are not kept on a local disk, so a zoneFileFetcher can be designed 21 | to grab files from any location (an SMB server, for example). 22 | 23 | ## Example 24 | The following zone file includes some subdomains at the end: 25 | ``` 26 | ;/home/dns/zones/testdomain.geek.db 27 | $ORIGIN testdomain.geek. 28 | $TTL 7200 29 | @ IN SOA testdomain.geek. post.testdomain.geek. 2014110501 3600 14400 604800 3600 30 | @ IN NS ns1.nameserver.com. 31 | @ IN NS ns2.nameserver.com. 32 | 33 | $INCLUDE email-domains.txt mail.testdomain.geek. 34 | ``` 35 | 36 | ```php 37 | makeZone('testdomain.geek.', $zoneFile); 53 | ``` -------------------------------------------------------------------------------- /tests/Rdata/OptTest.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace Badcow\DNS\Tests\Rdata; 15 | 16 | use Badcow\DNS\Edns\Option\OptionInterface; 17 | use Badcow\DNS\Edns\Option\TCP_KEEPALIVE; 18 | use Badcow\DNS\Edns\Option\UnknownOption; 19 | use Badcow\DNS\Rdata\DecodeException; 20 | use Badcow\DNS\Rdata\OPT; 21 | use PHPUnit\Framework\TestCase; 22 | 23 | class OptTest extends TestCase 24 | { 25 | public function testOutput(): void 26 | { 27 | $opt = new OPT(); 28 | $this->assertEmpty($opt->toText()); 29 | } 30 | 31 | public function testFromText(): void 32 | { 33 | $this->expectException(\Exception::class); 34 | $text = ''; 35 | $opt = new OPT(); 36 | $opt->fromText($text); 37 | } 38 | 39 | public function testFromWire1(): void 40 | { 41 | $wire = "\x00\x0B\x00\x00\x00\xFF\x00\x01A"; 42 | 43 | $opt = new OPT(); 44 | $opt->fromWire($wire); 45 | 46 | $options = $opt->getOptions(); 47 | $this->assertCount(2, $options); 48 | $this->assertContainsOnlyInstancesOf(OptionInterface::class, $options); 49 | $this->assertInstanceOf(TCP_KEEPALIVE::class, $options[0]); 50 | $this->assertInstanceOf(UnknownOption::class, $options[1]); 51 | $this->assertEquals(255, $options[1]->getNameCode()); 52 | } 53 | 54 | public function testFromWire2(): void 55 | { 56 | $this->expectException(DecodeException::class); 57 | $wire = "\x00\x00"; 58 | $opt = new OPT(); 59 | $opt->fromWire($wire); 60 | } 61 | 62 | public function testToWire(): void 63 | { 64 | $options = []; 65 | $options[0] = new TCP_KEEPALIVE(); 66 | $options[1] = new UnknownOption(); 67 | $options[1]->setOptionCode(255); 68 | $options[1]->setData('A'); 69 | 70 | $opt = new OPT(); 71 | $this->assertEmpty($opt->toWire()); 72 | $opt->setOptions($options); 73 | 74 | $wire = "\x00\x0B\x00\x00\x00\xFF\x00\x01A"; 75 | $this->assertEquals($wire, $opt->toWire()); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /lib/Rdata/A.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace Badcow\DNS\Rdata; 15 | 16 | use Badcow\DNS\Validator; 17 | 18 | /** 19 | * @see https://tools.ietf.org/html/rfc1035#section-3.4.1 20 | */ 21 | class A implements RdataInterface 22 | { 23 | use RdataTrait; 24 | 25 | public const TYPE = 'A'; 26 | public const TYPE_CODE = 1; 27 | 28 | /** 29 | * @var string|null 30 | */ 31 | protected $address; 32 | 33 | public function setAddress(string $address): void 34 | { 35 | if (!Validator::ipv4($address)) { 36 | throw new \InvalidArgumentException(sprintf('The address "%s" is not a valid IPv4 address.', $address)); 37 | } 38 | 39 | $this->address = $address; 40 | } 41 | 42 | /** 43 | * @return string 44 | */ 45 | public function getAddress(): ?string 46 | { 47 | return $this->address; 48 | } 49 | 50 | public function toText(): string 51 | { 52 | return $this->address ?? ''; 53 | } 54 | 55 | /** 56 | * @throws \InvalidArgumentException 57 | */ 58 | public function toWire(): string 59 | { 60 | if (!isset($this->address)) { 61 | throw new \InvalidArgumentException('No IP address has been set.'); 62 | } 63 | 64 | if (false === $encoded = @inet_pton($this->address)) { 65 | throw new \InvalidArgumentException(sprintf('The IP address "%s" cannot be encoded. Check that it is a valid IP address.', $this->address)); 66 | } 67 | 68 | return $encoded; 69 | } 70 | 71 | public function fromText(string $text): void 72 | { 73 | $this->setAddress($text); 74 | } 75 | 76 | /** 77 | * @throws DecodeException 78 | */ 79 | public function fromWire(string $rdata, int &$offset = 0, ?int $rdLength = null): void 80 | { 81 | if (false === $address = @inet_ntop(substr($rdata, $offset, 4))) { 82 | throw new DecodeException(static::TYPE, $rdata); 83 | } 84 | $offset += 4; 85 | 86 | $this->setAddress($address); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /docs/Parser/Unkown-Types.md: -------------------------------------------------------------------------------- 1 | Parsing Unknown or Unsupported Types 2 | ==================================== 3 | 4 | PolymorphicRdata Class 5 | ---------------------- 6 | 7 | In DNS records, there may be rdata types that are not implemented in this library (though these should now be few). For 8 | example: 9 | ```text 10 | files.example.com. IN 3600 MD UDEL.ARPA. 11 | ``` 12 | The above entry refers to the obsolete `MD` resource data type. Whilst `MD` is a valid rdata type, it has not been implemented 13 | in the Badcow DNS Library. When this `MD` record is parsed, it will parsed into an instance of `Badcow\DNS\Rdata\PolymorphicRdata`. 14 | 15 | The `PolymorphicRdata` class is used for any *valid* rdata type that has not been implemented by the library and whose records 16 | have a valid text representation. When this type is outputted to text, the data will remain unchanged, but the data cannot 17 | be outputted in a wire format. 18 | 19 | If, however, you are using a wholly unknown Rdata type (i.e. one which has not been specified by IANA) then the parser will 20 | throw a `ParseException`. In this case, if the rdata type is valid (or apart of some specific functionality not otherwise 21 | defined) it may be useful define a [custom rdata handler](Custom-Rdata-Handlers.md). 22 | 23 | UnknownType Class 24 | ------------------ 25 | The same record from above can be represented in a text file as: 26 | ```text 27 | files.example.com. IN 3600 TYPE3 \# 11 04 55 44 45 4c 04 41 52 50 41 00 28 | ``` 29 | This representation conforms with _RFC 3597 - Handling of Unknown DNS Resource Record (RR) Types_. In this case, the record 30 | will be parsed into an instance of `Badcow\DNS\Rdata\UnknownType`. 31 | 32 | The `UnknownType` class is used to handle records that are formatted in this way. This class can output the _wire format_ 33 | of the record and the text representation above. This is the preferred way of handling unknown, unsupported, or otherwise 34 | "made up" rdata types. 35 | 36 | If the type is implemented, then the parser will render an instance of that type instead of the `UnknownType` class. 37 | _Exempli Gratia_: 38 | ```text 39 | files.example.com. IN 3600 TYPE1 \# 4 c0 a8 01 64 40 | ``` 41 | The above record type, TYPE1, is a known and supported type - an `A` record with the four octets (bytes) of the IP address 42 | `192.168.1.100`. This record will be parsed to an instance of `Badcow\DNS\Rdata\A` and *NOT* as an `UnknownType` class. -------------------------------------------------------------------------------- /lib/Edns/Option/COOKIE.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace Badcow\DNS\Edns\Option; 15 | 16 | class COOKIE implements OptionInterface 17 | { 18 | use OptionTrait; 19 | 20 | public const NAME = 'COOKIE'; 21 | public const CODE = 10; 22 | 23 | /** 24 | * @var string|null 25 | */ 26 | protected $clientCookie; 27 | 28 | /** 29 | * @var string|null 30 | */ 31 | protected $serverCookie; 32 | 33 | public function getClientCookie(): ?string 34 | { 35 | return $this->clientCookie; 36 | } 37 | 38 | public function setClientCookie(?string $clientCookie): void 39 | { 40 | if (null !== $clientCookie and 8 != strlen($clientCookie)) { 41 | throw new \InvalidArgumentException('Length of client cookie must be 8 bytes'); 42 | } 43 | $this->clientCookie = $clientCookie; 44 | } 45 | 46 | public function getServerCookie(): ?string 47 | { 48 | return $this->serverCookie; 49 | } 50 | 51 | public function setServerCookie(?string $serverCookie): void 52 | { 53 | if (null !== $serverCookie) { 54 | $length = strlen($serverCookie); 55 | if ($length < 8 or $length > 32) { 56 | throw new \InvalidArgumentException('Length of server cookie must be between 8 to 32 bytes'); 57 | } 58 | } 59 | $this->serverCookie = $serverCookie; 60 | } 61 | 62 | public function toWire(): string 63 | { 64 | return $this->clientCookie.$this->serverCookie; 65 | } 66 | 67 | public function fromWire(string $optionValue, int &$offset = 0, ?int $optionLength = null): void 68 | { 69 | $optionLength = $optionLength ?? strlen($optionValue); 70 | if ($optionLength < 8 or (8 != $optionLength and ($optionLength < 16 or $optionLength > 40))) { 71 | throw new DecodeException(static::NAME, $optionValue); 72 | } 73 | $this->clientCookie = substr($optionValue, $offset, 8); 74 | $offset += 8; 75 | if ($optionLength > 8) { 76 | $this->serverCookie = substr($optionValue, $offset, $optionLength - 8); 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /tests/Rdata/TaTest.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace Badcow\DNS\Tests\Rdata; 15 | 16 | use Badcow\DNS\Algorithms; 17 | use Badcow\DNS\Rdata\Factory; 18 | use Badcow\DNS\Rdata\TA; 19 | use PHPUnit\Framework\TestCase; 20 | 21 | class TaTest extends TestCase 22 | { 23 | private static $digest = '2BB183AF5F22588179A53B0A98631FAD1A292118'; 24 | 25 | public function testOutput(): void 26 | { 27 | $expectation = '60485 5 1 '.self::$digest; 28 | 29 | $ta = new TA(); 30 | $ta->setKeyTag(60485); 31 | $ta->setAlgorithm(Algorithms::RSASHA1); 32 | $ta->setDigestType(TA::DIGEST_SHA1); 33 | $ta->setDigest(hex2bin(self::$digest)); 34 | 35 | $this->assertEquals($expectation, $ta->toText()); 36 | } 37 | 38 | public function testFactory(): void 39 | { 40 | $keyTag = 60485; 41 | $ta = Factory::TA($keyTag, Algorithms::RSASHA1, self::$digest, TA::DIGEST_SHA1); 42 | 43 | $this->assertEquals($keyTag, $ta->getKeyTag()); 44 | $this->assertEquals(Algorithms::RSASHA1, $ta->getAlgorithm()); 45 | $this->assertEquals(self::$digest, $ta->getDigest()); 46 | $this->assertEquals(TA::DIGEST_SHA1, $ta->getDigestType()); 47 | } 48 | 49 | public function testFromText(): void 50 | { 51 | $expectation = new TA(); 52 | $expectation->setKeyTag(60485); 53 | $expectation->setAlgorithm(Algorithms::RSASHA1); 54 | $expectation->setDigestType(TA::DIGEST_SHA1); 55 | $expectation->setDigest(hex2bin(self::$digest)); 56 | 57 | $fromText = new TA(); 58 | $fromText->fromText('60485 5 1 '.self::$digest); 59 | $this->assertEquals($expectation, $fromText); 60 | } 61 | 62 | public function testWire(): void 63 | { 64 | $ta = new TA(); 65 | $ta->setKeyTag(60485); 66 | $ta->setAlgorithm(Algorithms::RSASHA1); 67 | $ta->setDigestType(TA::DIGEST_SHA1); 68 | $ta->setDigest(hex2bin(self::$digest)); 69 | $wireFormat = $ta->toWire(); 70 | 71 | $fromWire = new TA(); 72 | $fromWire->fromWire($wireFormat); 73 | $this->assertEquals($ta, $fromWire); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /lib/Edns/Option/Codes.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace Badcow\DNS\Edns\Option; 15 | 16 | class Codes 17 | { 18 | public const LLQ = 1; 19 | public const UL = 2; 20 | public const NSID = 3; 21 | public const DAU = 5; 22 | public const DHU = 6; 23 | public const N3U = 7; 24 | public const CLIENT_SUBNET = 8; 25 | public const EXPIRE = 9; 26 | public const COOKIE = 10; 27 | public const TCP_KEEPALIVE = 11; 28 | public const PADDING = 12; 29 | public const CHAIN = 13; 30 | public const KEY_CHAIN = 14; 31 | public const DNS_ERROR = 15; 32 | public const CLIENT_TAG = 16; 33 | public const SERVER_TAG = 17; 34 | 35 | /** 36 | * @var array 37 | */ 38 | public static $names = [ 39 | self::LLQ => 'LLQ', 40 | self::UL => 'UL', 41 | self::NSID => 'NSID', 42 | self::DAU => 'DAU', 43 | self::DHU => 'DHU', 44 | self::N3U => 'N3U', 45 | self::CLIENT_SUBNET => 'CLIENT_SUBNET', 46 | self::EXPIRE => 'EXPIRE', 47 | self::COOKIE => 'COOKIE', 48 | self::TCP_KEEPALIVE => 'TCP_KEEPALIVE', 49 | self::PADDING => 'PADDING', 50 | self::CHAIN => 'CHAIN', 51 | self::KEY_CHAIN => 'KEY_CHAIN', 52 | self::DNS_ERROR => 'DNS_ERROR', 53 | self::CLIENT_TAG => 'CLIENT_TAG', 54 | self::SERVER_TAG => 'SERVER_TAG', 55 | ]; 56 | 57 | /** 58 | * @param int|string $option either the option name (string) or the option code (integer) 59 | */ 60 | public static function isValid($option): bool 61 | { 62 | if (is_int($option)) { 63 | return array_key_exists($option, self::$names); 64 | } 65 | 66 | return in_array($option, self::$names); 67 | } 68 | 69 | /** 70 | * Get the name of an Option code. E.g. Codes::getName(8) return 'CLIENT_SUBNET'. 71 | * 72 | * @param int $code The index of the code 73 | * 74 | * @throws UnsupportedOptionException 75 | */ 76 | public static function getName(int $code): string 77 | { 78 | if (!self::isValid($code)) { 79 | throw new UnsupportedOptionException(sprintf('The integer "%d" does not correspond to a supported code.', $code)); 80 | } 81 | 82 | return self::$names[$code]; 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /lib/Rdata/PolymorphicRdata.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace Badcow\DNS\Rdata; 15 | 16 | /** 17 | * Used to create RData of types that have not yet been implemented in the library. 18 | */ 19 | class PolymorphicRdata implements RdataInterface 20 | { 21 | /** 22 | * The RData type. 23 | * 24 | * @var string 25 | */ 26 | private $type; 27 | 28 | /** 29 | * @var string|null 30 | */ 31 | private $data; 32 | 33 | /** 34 | * @var int 35 | */ 36 | private $typeCode = 0; 37 | 38 | /** 39 | * PolymorphicRdata constructor. 40 | */ 41 | public function __construct(?string $type = null, ?string $data = null) 42 | { 43 | if (null !== $type) { 44 | $this->setType($type); 45 | } 46 | 47 | if (null !== $data) { 48 | $this->setData($data); 49 | } 50 | } 51 | 52 | public function setType(string $type): void 53 | { 54 | try { 55 | $this->typeCode = Types::getTypeCode($type); 56 | } catch (UnsupportedTypeException $e) { 57 | $this->typeCode = 0; 58 | } 59 | $this->type = $type; 60 | } 61 | 62 | public function getType(): string 63 | { 64 | return $this->type; 65 | } 66 | 67 | public function setTypeCode(int $typeCode): void 68 | { 69 | $this->typeCode = $typeCode; 70 | } 71 | 72 | public function getTypeCode(): int 73 | { 74 | return $this->typeCode; 75 | } 76 | 77 | public function setData(string $data): void 78 | { 79 | $this->data = $data; 80 | } 81 | 82 | public function getData(): ?string 83 | { 84 | return $this->data; 85 | } 86 | 87 | public function toText(): string 88 | { 89 | return $this->getData() ?? ''; 90 | } 91 | 92 | public function toWire(): string 93 | { 94 | return $this->data ?? ''; 95 | } 96 | 97 | public function fromText(string $text): void 98 | { 99 | $this->setData($text); 100 | } 101 | 102 | public function fromWire(string $rdata, int &$offset = 0, ?int $rdLength = null): void 103 | { 104 | $this->setData(substr($rdata, $offset, $rdLength ?? strlen($rdata))); 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /lib/Rdata/AFSDB.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace Badcow\DNS\Rdata; 15 | 16 | use Badcow\DNS\Message; 17 | 18 | class AFSDB implements RdataInterface 19 | { 20 | use RdataTrait; 21 | 22 | public const TYPE = 'AFSDB'; 23 | public const TYPE_CODE = 18; 24 | 25 | /** 26 | * @var int|null 27 | */ 28 | private $subType; 29 | 30 | /** 31 | * @var string|null 32 | */ 33 | private $hostname; 34 | 35 | public function getSubType(): ?int 36 | { 37 | return $this->subType; 38 | } 39 | 40 | public function setSubType(int $subType): void 41 | { 42 | $this->subType = $subType; 43 | } 44 | 45 | public function getHostname(): ?string 46 | { 47 | return $this->hostname; 48 | } 49 | 50 | public function setHostname(string $hostname): void 51 | { 52 | $this->hostname = $hostname; 53 | } 54 | 55 | /** 56 | * @throws \InvalidArgumentException 57 | */ 58 | public function toText(): string 59 | { 60 | if (!isset($this->subType)) { 61 | throw new \InvalidArgumentException('No sub-type has been set on AFSDB object.'); 62 | } 63 | 64 | if (!isset($this->hostname)) { 65 | throw new \InvalidArgumentException('No hostname has been set on AFSDB object.'); 66 | } 67 | 68 | return sprintf('%d %s', $this->subType, $this->hostname); 69 | } 70 | 71 | public function toWire(): string 72 | { 73 | if (!isset($this->subType) || !isset($this->hostname)) { 74 | throw new \InvalidArgumentException('Subtype and hostname must be both be set.'); 75 | } 76 | 77 | return pack('n', $this->subType).Message::encodeName($this->hostname); 78 | } 79 | 80 | public function fromText(string $text): void 81 | { 82 | $rdata = explode(' ', $text); 83 | 84 | $this->setSubType((int) $rdata[0]); 85 | $this->setHostname($rdata[1]); 86 | } 87 | 88 | public function fromWire(string $rdata, int &$offset = 0, ?int $rdLength = null): void 89 | { 90 | if (false === $subType = unpack('n', $rdata, $offset)) { 91 | throw new DecodeException(static::TYPE, $rdata); 92 | } 93 | $this->setSubType($subType[1]); 94 | $offset += 2; 95 | $this->setHostname(Message::decodeName($rdata, $offset)); 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /tests/Rdata/MxTest.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace Badcow\DNS\Tests\Rdata; 15 | 16 | use Badcow\DNS\Rdata\MX; 17 | use PHPUnit\Framework\TestCase; 18 | 19 | class MxTest extends TestCase 20 | { 21 | public function testSetters(): void 22 | { 23 | $target = 'foo.example.com.'; 24 | $preference = 10; 25 | $mx = new MX(); 26 | $mx->setExchange($target); 27 | $mx->setPreference($preference); 28 | 29 | $this->assertEquals($target, $mx->getExchange()); 30 | $this->assertEquals($preference, $mx->getPreference()); 31 | } 32 | 33 | public function testOutput(): void 34 | { 35 | $target = 'foo.example.com.'; 36 | $mx = new MX(); 37 | $mx->SetExchange($target); 38 | $mx->setPreference(42); 39 | 40 | $this->assertEquals('42 foo.example.com.', $mx->toText()); 41 | } 42 | 43 | public function testOutputThrowsExceptionWhenMissingPreference(): void 44 | { 45 | $mx = new MX(); 46 | $mx->setExchange('mail.google.com.'); 47 | 48 | $this->expectException(\InvalidArgumentException::class); 49 | $this->expectExceptionMessage('No preference has been set on MX object.'); 50 | $mx->toText(); 51 | } 52 | 53 | public function testOutputThrowsExceptionWhenMissingExchange(): void 54 | { 55 | $mx = new MX(); 56 | $mx->setPreference(15); 57 | 58 | $this->expectException(\InvalidArgumentException::class); 59 | $this->expectExceptionMessage('No exchange has been set on MX object.'); 60 | $mx->toText(); 61 | } 62 | 63 | public function testFromText(): void 64 | { 65 | $text = '10 mail.example.com.'; 66 | /** @var MX $mx */ 67 | $mx = new MX(); 68 | $mx->fromText($text); 69 | 70 | $this->assertEquals(10, $mx->getPreference()); 71 | $this->assertEquals('mail.example.com.', $mx->getExchange()); 72 | } 73 | 74 | public function testWire(): void 75 | { 76 | $mx = new MX(); 77 | $mx->setExchange('mail.example.com.'); 78 | $mx->setPreference(10); 79 | 80 | $expectation = pack('n', 10).chr(4).'mail'.chr(7).'example'.chr(3).'com'.chr(0); 81 | 82 | $this->assertEquals($expectation, $mx->toWire()); 83 | $fromWire = new MX(); 84 | $fromWire->fromWire($expectation); 85 | $this->assertEquals($mx, $fromWire); 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /docs/Reverse-Records.md: -------------------------------------------------------------------------------- 1 | # Reverse Records 2 | 3 | The DNS Library can natively handle reverse IP records for both IPv4 and IPv6. 4 | 5 | ## IPv4 Example 6 | ```php 7 | use Badcow\DNS\Rdata\Factory; 8 | use Badcow\DNS\{Zone, ResourceRecord, AlignedBuilder, Classes, ZoneBuilder}; 9 | use Badcow\DNS\Ip\PTR; 10 | 11 | $parent = PTR::reverseIpv4('158.133.7'); //Returns 7.133.158.in-addr.arpa. 12 | 13 | $zone = new Zone($parent, 10800); 14 | 15 | $resourceRecords = [ 16 | new ResourceRecord('1', Factory::Ptr('gw-01.badcow.co.')), 17 | new ResourceRecord('2', Factory::Ptr('gw-02.badcow.co.')), 18 | new ResourceRecord('10', Factory::Ptr('badcow.co.')), 19 | new ResourceRecord('15', Factory::Ptr('mail.badcow.co.'), 3600), 20 | new ResourceRecord('51', Factory::Ptr('esw-01.badcow.co.')), 21 | new ResourceRecord('52', Factory::Ptr('esw-02.badcow.co.')), 22 | ]; 23 | 24 | $zone->fromArray($resourceRecords); 25 | 26 | echo AlignedBuilder::build($zone); 27 | ``` 28 | ### Output 29 | ```text 30 | $ORIGIN 7.133.158.in-addr.arpa. 31 | $TTL 10800 32 | 33 | ; PTR RECORDS 34 | 10 IN PTR badcow.co. 35 | 15 3600 IN PTR mail.badcow.co. 36 | 1 IN PTR gw-01.badcow.co. 37 | 2 IN PTR gw-02.badcow.co. 38 | 51 IN PTR esw-01.badcow.co. 39 | 52 IN PTR esw-02.badcow.co. 40 | ``` 41 | 42 | ## IPv6 Example 43 | ```php 44 | use Badcow\DNS\Rdata\Factory; 45 | use Badcow\DNS\{Zone, ResourceRecord, AlignedBuilder, Classes, ZoneBuilder}; 46 | use Badcow\DNS\Ip\PTR; 47 | 48 | //$parent = PTR::reverseIpv4('158.133.7'); //Returns 7.133.158.in-addr.arpa. 49 | $parent = PTR::reverseIpv6('2001:acad:5889:0:0:0:0'); 50 | 51 | $zone = new Zone($parent, 10800); 52 | 53 | $resourceRecords = [ 54 | new ResourceRecord(PTR::reverseIpv6('1', false), Factory::Ptr('gw-01.badcow.co.')), 55 | new ResourceRecord(PTR::reverseIpv6('2', false), Factory::Ptr('gw-02.badcow.co.')), 56 | new ResourceRecord(PTR::reverseIpv6('bad', false), Factory::Ptr('badcow.co.')), 57 | new ResourceRecord(PTR::reverseIpv6('ff', false), Factory::Ptr('mail.badcow.co.'), 3600, Classes::INTERNET), 58 | new ResourceRecord(PTR::reverseIpv6('aa1', false), Factory::Ptr('esw-01.badcow.co.')), 59 | new ResourceRecord(PTR::reverseIpv6('aa2', false), Factory::Ptr('esw-02.badcow.co.')), 60 | ]; 61 | 62 | $zone->fromArray($resourceRecords); 63 | 64 | echo AlignedBuilder::build($zone); 65 | ``` 66 | ### Output 67 | ```text 68 | $ORIGIN 0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.9.8.8.5.d.a.c.a.1.0.0.2.ip6.arpa. 69 | $TTL 10800 70 | 71 | ; PTR RECORDS 72 | 1.0.0.0 PTR gw-01.badcow.co. 73 | 1.a.a.0 PTR esw-01.badcow.co. 74 | 2.0.0.0 PTR gw-02.badcow.co. 75 | 2.a.a.0 PTR esw-02.badcow.co. 76 | d.a.b.0 PTR badcow.co. 77 | f.f.0.0 3600 IN PTR mail.badcow.co. 78 | ``` -------------------------------------------------------------------------------- /tests/Rdata/HinfoTest.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace Badcow\DNS\Tests\Rdata; 15 | 16 | use Badcow\DNS\Rdata\Factory; 17 | use Badcow\DNS\Rdata\HINFO; 18 | use PHPUnit\Framework\TestCase; 19 | 20 | class HinfoTest extends TestCase 21 | { 22 | public function testToText(): void 23 | { 24 | $cpu = '2.7GHz'; 25 | $os = 'Ubuntu 12.04'; 26 | $expectation = '"2.7GHz" "Ubuntu 12.04"'; 27 | $hinfo = new HINFO(); 28 | $hinfo->setCpu($cpu); 29 | $hinfo->setOs($os); 30 | 31 | $this->assertEquals($expectation, $hinfo->toText()); 32 | } 33 | 34 | public function testGetters(): void 35 | { 36 | $cpu = '2.7GHz'; 37 | $os = 'Ubuntu 12.04'; 38 | $hinfo = new HINFO(); 39 | $hinfo->setCpu($cpu); 40 | $hinfo->setOs($os); 41 | 42 | $this->assertEquals($cpu, $hinfo->getCpu()); 43 | $this->assertEquals($os, $hinfo->getOs()); 44 | } 45 | 46 | public function testGetType(): void 47 | { 48 | $hinfo = new HINFO(); 49 | $this->assertEquals('HINFO', $hinfo->getType()); 50 | } 51 | 52 | public function testGetTypeCode(): void 53 | { 54 | $hinfo = new HINFO(); 55 | $this->assertEquals(13, $hinfo->getTypeCode()); 56 | } 57 | 58 | public function testFromWire(): void 59 | { 60 | $hinfo = new HINFO(); 61 | $hinfo->fromWire('"2.7GHz" "Ubuntu 12.04"'); 62 | $this->assertEquals('2.7GHz', $hinfo->getCpu()); 63 | $this->assertEquals('Ubuntu 12.04', $hinfo->getOs()); 64 | } 65 | 66 | public function testFromText(): void 67 | { 68 | $hinfo = new HINFO(); 69 | $hinfo->fromText('2.7GHz "Ubuntu 12.04"'); 70 | $this->assertEquals('2.7GHz', $hinfo->getCpu()); 71 | $this->assertEquals('Ubuntu 12.04', $hinfo->getOs()); 72 | } 73 | 74 | public function testToWire(): void 75 | { 76 | $cpu = '2.7GHz'; 77 | $os = 'Ubuntu 12.04'; 78 | $expectation = '"2.7GHz" "Ubuntu 12.04"'; 79 | $hinfo = new HINFO(); 80 | $hinfo->setCpu($cpu); 81 | $hinfo->setOs($os); 82 | 83 | $this->assertEquals($expectation, $hinfo->toWire()); 84 | } 85 | 86 | public function testFactory(): void 87 | { 88 | $hinfo = Factory::HINFO('SGI-IRIS-INDY', 'IRIX'); 89 | $this->assertEquals('SGI-IRIS-INDY', $hinfo->getCpu()); 90 | $this->assertEquals('IRIX', $hinfo->getOs()); 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /tests/Rdata/AfsdbTest.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace Badcow\DNS\Tests\Rdata; 15 | 16 | use Badcow\DNS\Rdata\AFSDB; 17 | use Badcow\DNS\Rdata\Factory; 18 | use PHPUnit\Framework\TestCase; 19 | 20 | /** 21 | * {@link https://tools.ietf.org/html/rfc1183}. 22 | */ 23 | class AfsdbTest extends TestCase 24 | { 25 | public function testOutput(): void 26 | { 27 | $hostname = 'foo.example.com.'; 28 | $afsdb = new AFSDB(); 29 | $afsdb->setHostname($hostname); 30 | $afsdb->setSubType(2); 31 | 32 | $this->assertEquals('2 foo.example.com.', $afsdb->toText()); 33 | } 34 | 35 | public function testOutputThrowsExceptionWhenMissingSubType(): void 36 | { 37 | $afsdb = new AFSDB(); 38 | $afsdb->setHostname('foo.example.com.'); 39 | 40 | $this->expectException(\InvalidArgumentException::class); 41 | $this->expectExceptionMessage('No sub-type has been set on AFSDB object.'); 42 | $afsdb->toText(); 43 | } 44 | 45 | public function testOutputThrowsExceptionWhenMissingHostname(): void 46 | { 47 | $afsdb = new AFSDB(); 48 | $afsdb->setSubType(15); 49 | 50 | $this->expectException(\InvalidArgumentException::class); 51 | $this->expectExceptionMessage('No hostname has been set on AFSDB object.'); 52 | $afsdb->toText(); 53 | } 54 | 55 | public function testFromText(): void 56 | { 57 | $text = '2 foo.example.com.'; 58 | /** @var AFSDB $afsdb */ 59 | $afsdb = new AFSDB(); 60 | $afsdb->fromText($text); 61 | 62 | $this->assertEquals(2, $afsdb->getSubType()); 63 | $this->assertEquals('foo.example.com.', $afsdb->getHostname()); 64 | } 65 | 66 | public function testWire(): void 67 | { 68 | $afsdb = new AFSDB(); 69 | $afsdb->setHostname('foo.example.com.'); 70 | $afsdb->setSubType(2); 71 | 72 | $expectation = pack('n', 2).chr(3).'foo'.chr(7).'example'.chr(3).'com'.chr(0); 73 | 74 | $fromWire = new AFSDB(); 75 | $fromWire->fromWire($expectation); 76 | 77 | $this->assertEquals($expectation, $afsdb->toWire()); 78 | $this->assertEquals($afsdb, $fromWire); 79 | } 80 | 81 | public function testFactory(): void 82 | { 83 | $afsdb = Factory::AFSDB(2, 'foo.example.com.'); 84 | 85 | $this->assertInstanceOf(AFSDB::class, $afsdb); 86 | $this->assertEquals('2 foo.example.com.', $afsdb->toText()); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /lib/Rdata/HINFO.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace Badcow\DNS\Rdata; 15 | 16 | use Badcow\DNS\Parser\StringIterator; 17 | use Badcow\DNS\Parser\Tokens; 18 | 19 | /** 20 | * @see https://tools.ietf.org/html/rfc1035#section-3.3.2 21 | */ 22 | class HINFO implements RdataInterface 23 | { 24 | use RdataTrait; 25 | 26 | public const TYPE = 'HINFO'; 27 | public const TYPE_CODE = 13; 28 | 29 | /** 30 | * @var string|null 31 | */ 32 | private $cpu; 33 | 34 | /** 35 | * @var string|null 36 | */ 37 | private $os; 38 | 39 | public function setCpu(?string $cpu): void 40 | { 41 | $this->cpu = $cpu; 42 | } 43 | 44 | /** 45 | * @return string 46 | */ 47 | public function getCpu(): ?string 48 | { 49 | return $this->cpu; 50 | } 51 | 52 | public function setOs(?string $os): void 53 | { 54 | $this->os = $os; 55 | } 56 | 57 | /** 58 | * @return string 59 | */ 60 | public function getOs(): ?string 61 | { 62 | return $this->os; 63 | } 64 | 65 | public function toText(): string 66 | { 67 | return sprintf('"%s" "%s"', $this->cpu ?? '', $this->os ?? ''); 68 | } 69 | 70 | public function toWire(): string 71 | { 72 | return $this->toText(); 73 | } 74 | 75 | public function fromWire(string $rdata, int &$offset = 0, ?int $rdLength = null): void 76 | { 77 | $this->fromText(substr($rdata, $offset, $rdLength ?? strlen($rdata))); 78 | $offset += $rdLength; 79 | } 80 | 81 | public function fromText(string $text): void 82 | { 83 | $string = new StringIterator($text); 84 | $this->setCpu(self::extractText($string)); 85 | $this->setOs(self::extractText($string)); 86 | } 87 | 88 | private static function extractText(StringIterator $string): string 89 | { 90 | $txt = new StringIterator(); 91 | 92 | if ($string->is(Tokens::DOUBLE_QUOTES)) { 93 | TXT::handleTxt($string, $txt); 94 | $string->next(); 95 | } else { 96 | while ($string->isNot(Tokens::SPACE) && $string->valid()) { 97 | $txt->append($string->current()); 98 | $string->next(); 99 | } 100 | } 101 | 102 | if ($string->is(Tokens::SPACE)) { 103 | $string->next(); 104 | } 105 | 106 | return (string) $txt; 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /lib/Rdata/UnknownType.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace Badcow\DNS\Rdata; 15 | 16 | use Badcow\DNS\Parser\ParseException; 17 | use Badcow\DNS\Parser\Tokens; 18 | 19 | /** 20 | * {@link https://tools.ietf.org/html/rfc3597}. 21 | */ 22 | class UnknownType implements RdataInterface 23 | { 24 | /** 25 | * @var int 26 | */ 27 | private $typeCode; 28 | 29 | /** 30 | * @var string|null 31 | */ 32 | private $data; 33 | 34 | public function setTypeCode(int $typeCode): void 35 | { 36 | $this->typeCode = $typeCode; 37 | } 38 | 39 | /** 40 | * @return string 41 | */ 42 | public function getData(): ?string 43 | { 44 | return $this->data; 45 | } 46 | 47 | /** 48 | * @param string $data 49 | */ 50 | public function setData(?string $data): void 51 | { 52 | $this->data = $data; 53 | } 54 | 55 | public function getType(): string 56 | { 57 | return 'TYPE'.$this->typeCode; 58 | } 59 | 60 | public function getTypeCode(): int 61 | { 62 | return $this->typeCode; 63 | } 64 | 65 | public function toText(): string 66 | { 67 | if (null === $this->data) { 68 | return '\# 0'; 69 | } 70 | 71 | return sprintf('\# %d %s', strlen($this->data), bin2hex($this->data)); 72 | } 73 | 74 | public function toWire(): string 75 | { 76 | return $this->data ?? ''; 77 | } 78 | 79 | /** 80 | * @throws ParseException 81 | */ 82 | public function fromText(string $text): void 83 | { 84 | if (1 !== preg_match('/^\\\#\s+(\d+)(\s[a-f0-9\s]+)?$/i', $text, $matches)) { 85 | throw new ParseException('Could not parse rdata of unknown type. Malformed string.'); 86 | } 87 | 88 | if ('0' === $matches[1]) { 89 | return; 90 | } 91 | 92 | $hexVal = str_replace(Tokens::SPACE, '', $matches[2]); 93 | 94 | if (false === $data = hex2bin($hexVal)) { 95 | throw new ParseException(sprintf('Could not parse hexadecimal data "%s".', $hexVal)); 96 | } 97 | $this->setData($data); 98 | } 99 | 100 | public function fromWire(string $rdata, int &$offset = 0, ?int $rdLength = null): void 101 | { 102 | $rdLength = $rdLength ?? strlen($rdata); 103 | $this->setData(substr($rdata, $offset, $rdLength)); 104 | $offset += $rdLength; 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /tests/Edns/Option/COOKIETest.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace Badcow\DNS\Tests\Edns\Option; 15 | 16 | use Badcow\DNS\Edns\Option\COOKIE; 17 | use Badcow\DNS\Edns\Option\DecodeException; 18 | use PHPUnit\Framework\TestCase; 19 | 20 | class COOKIETest extends TestCase 21 | { 22 | /** 23 | * @var COOKIE 24 | */ 25 | private $option; 26 | 27 | public function setUp(): void 28 | { 29 | $this->option = new COOKIE(); 30 | } 31 | 32 | public function testGetterSetters(): void 33 | { 34 | $this->assertEquals('COOKIE', $this->option->getName()); 35 | } 36 | 37 | public function testSetClientCookie(): void 38 | { 39 | $this->expectException(\InvalidArgumentException::class); 40 | $this->option->setClientCookie(str_repeat('a', 9)); 41 | } 42 | 43 | public function testSetServerCookie(): void 44 | { 45 | $this->expectException(\InvalidArgumentException::class); 46 | $this->option->setServerCookie(str_repeat('b', 7)); 47 | } 48 | 49 | public function testToWire(): void 50 | { 51 | $noCookie = new COOKIE(); 52 | $this->assertEquals('', $noCookie->toWire()); 53 | 54 | $justClientCookie = new COOKIE(); 55 | $justClientCookie->setClientCookie('aaaaaaaa'); 56 | $this->assertEquals('aaaaaaaa', $justClientCookie->toWire()); 57 | 58 | $bothCookies = new COOKIE(); 59 | $bothCookies->setClientCookie('aaaaaaaa'); 60 | $bothCookies->setServerCookie('bbbbbbbbb'); 61 | $this->assertEquals('aaaaaaaabbbbbbbbb', $bothCookies->toWire()); 62 | } 63 | 64 | public function testFromWire1(): void 65 | { 66 | $this->expectException(DecodeException::class); 67 | $wire = ''; 68 | $noCookie = new COOKIE(); 69 | $noCookie->fromWire($wire); 70 | } 71 | 72 | public function testFromWire2(): void 73 | { 74 | $wire = 'aaaaaaaa'; 75 | $justClientCookie = new COOKIE(); 76 | $justClientCookie->fromWire($wire); 77 | $this->assertEquals('aaaaaaaa', $justClientCookie->getClientCookie()); 78 | $this->assertNull($justClientCookie->getServerCookie()); 79 | } 80 | 81 | public function testFromWire3(): void 82 | { 83 | $wire = 'aaaaaaaabbbbbbbbb'; 84 | $bothCookies = new COOKIE(); 85 | $bothCookies->fromWire($wire); 86 | $this->assertEquals('aaaaaaaa', $bothCookies->getClientCookie()); 87 | $this->assertEquals('bbbbbbbbb', $bothCookies->getServerCookie()); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /lib/Rcode.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace Badcow\DNS; 15 | 16 | class Rcode 17 | { 18 | /** 19 | * No Error [RFC1035]. 20 | */ 21 | public const NOERROR = 0; 22 | 23 | /** 24 | * Format Error [RFC1035]. 25 | */ 26 | public const FORMERR = 1; 27 | 28 | /** 29 | * Server Failure [RFC1035]. 30 | */ 31 | public const SERVFAIL = 2; 32 | 33 | /** 34 | * Non-Existent Domain [RFC1035]. 35 | */ 36 | public const NXDOMAIN = 3; 37 | 38 | /** 39 | * Not Implemented [RFC1035]. 40 | */ 41 | public const NOTIMP = 4; 42 | 43 | /** 44 | * Query Refused [RFC1035]. 45 | */ 46 | public const REFUSED = 5; 47 | 48 | /** 49 | * Name Exists when it should not [RFC2136][RFC6672]. 50 | */ 51 | public const YXDOMAIN = 6; 52 | 53 | /** 54 | * RR Set Exists when it should not [RFC2136]. 55 | */ 56 | public const YXRRSET = 7; 57 | 58 | /** 59 | * RR Set that should exist does not [RFC2136]. 60 | */ 61 | public const NXRRSET = 8; 62 | 63 | /** 64 | * Server Not Authoritative for zone [RFC2136]. 65 | * Not Authorized [RFC2845]. 66 | */ 67 | public const NOTAUTH = 9; 68 | 69 | /** 70 | * Name not contained in zone [RFC2136]. 71 | */ 72 | public const NOTZONE = 10; 73 | 74 | /** 75 | * DSO-TYPE Not Implemented [RFC8490]. 76 | */ 77 | public const DSOTYPENI = 11; 78 | 79 | /** 80 | * Bad OPT Version [RFC6891]. 81 | */ 82 | public const BADVERS = 16; 83 | 84 | /** 85 | * TSIG Signature Failure [RFC2845]. 86 | */ 87 | public const BADSIG = 16; 88 | 89 | /** 90 | * Key not recognized [RFC2845]. 91 | */ 92 | public const BADKEY = 17; 93 | 94 | /** 95 | * Signature out of time window [RFC2845]. 96 | */ 97 | public const BADTIME = 18; 98 | 99 | /** 100 | * Bad TKEY Mode [RFC2930]. 101 | */ 102 | public const BADMODE = 19; 103 | 104 | /** 105 | * Duplicate key name [RFC2930]. 106 | */ 107 | public const BADNAME = 20; 108 | 109 | /** 110 | * Algorithm not supported [RFC2930]. 111 | */ 112 | public const BADALG = 21; 113 | 114 | /** 115 | * Bad Truncation [RFC4635]. 116 | */ 117 | public const BADTRUNC = 22; 118 | 119 | /** 120 | * Bad/missing Server Cookie [RFC7873]. 121 | */ 122 | public const BADCOOKIE = 23; 123 | } 124 | -------------------------------------------------------------------------------- /lib/Parser/TimeFormat.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace Badcow\DNS\Parser; 15 | 16 | class TimeFormat 17 | { 18 | public const TIME_FORMAT_REGEX = '/^(?:(?\d+)w)?(?:(?\d+)d)?(?:(?\d+)h)?(?:(?\d+)m)?(?:(?\d+)s)?$/i'; 19 | 20 | /** 21 | * Maximum time is the the lesser of 0xffffffff or the PHP maximum integer. 22 | * 23 | * @var int|null 24 | */ 25 | public static $maxTime; 26 | 27 | public const TIME_MULTIPLIERS = [ 28 | 'w' => 604800, 29 | 'd' => 86400, 30 | 'h' => 3600, 31 | 'm' => 60, 32 | 's' => 1, 33 | ]; 34 | 35 | /** 36 | * Check if given token looks like time format. 37 | * 38 | * @param string $value the time value to be evaluated 39 | * 40 | * @return bool true if $value is a valid time format 41 | */ 42 | public static function isTimeFormat(string $value): bool 43 | { 44 | return is_numeric($value) || 1 === \preg_match(self::TIME_FORMAT_REGEX, $value); 45 | } 46 | 47 | /** 48 | * Convert human readable time format to seconds. 49 | * 50 | * @param string|int $value time value to be converted to seconds 51 | * 52 | * @return int the time value in seconds 53 | */ 54 | public static function toSeconds($value): int 55 | { 56 | if (!isset(static::$maxTime)) { 57 | static::$maxTime = min(0xFFFFFFFF, PHP_INT_MAX); 58 | } 59 | 60 | if (is_numeric($value)) { 61 | return (int) $value; 62 | } 63 | 64 | if (1 === preg_match_all(self::TIME_FORMAT_REGEX, $value, $matches)) { 65 | $sec = (int) $matches['w'][0] * 604800 + 66 | (int) $matches['d'][0] * 86400 + 67 | (int) $matches['h'][0] * 3600 + 68 | (int) $matches['m'][0] * 60 + 69 | (int) $matches['s'][0]; 70 | 71 | return $sec < static::$maxTime ? $sec : 0; 72 | } 73 | 74 | return 0; 75 | } 76 | 77 | /** 78 | * Convert number of seconds to human readable format. 79 | * 80 | * @param int $seconds the time in seconds to be converted to human-readable string 81 | * 82 | * @return string a human-readable representation of the $seconds parameter 83 | */ 84 | public static function toHumanReadable(int $seconds): string 85 | { 86 | $humanReadable = ''; 87 | foreach (self::TIME_MULTIPLIERS as $suffix => $multiplier) { 88 | $humanReadable .= ($t = floor($seconds / $multiplier)) > 0 ? $t.$suffix : ''; 89 | $seconds -= $t * $multiplier; 90 | } 91 | 92 | return $humanReadable; 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /tests/Rdata/PtrTest.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace Badcow\DNS\Tests\Rdata; 15 | 16 | use Badcow\DNS\Rdata\PTR; 17 | use PHPUnit\Framework\TestCase; 18 | 19 | class PtrTest extends TestCase 20 | { 21 | public function provider_expandIPv6(): array 22 | { 23 | return [ 24 | ['0000:0000:0000:0000:0000:0000:0000:0001', '::1'], 25 | ['2001:0db8:0000:0000:0000:ff00:0042:8329', '2001:db8::ff00:42:8329'], 26 | ['2001:0000:0000:acad:0000:0000:0000:0001', '2001:0:0:acad::1'], 27 | ['0000:0000:0000:0000:0000:0000:0000:0000', '::'], 28 | ['2001:0000:0000:ab80:2390:0000:0000:000a', '2001::ab80:2390:0:0:a'], 29 | ['0000:0000:aaaa:0000:0000:aaaa:0000:0000', '::aaaa:0:0:aaaa:0:0'], 30 | ['0001:0000:0000:0000:0000:0000:0000:0000', '1::'], 31 | ]; 32 | } 33 | 34 | public function provider_contractIPv6(): array 35 | { 36 | return array_merge($this->provider_expandIPv6(), [ 37 | ['2001:db8:0:0:f:0:0:0', '2001:db8:0:0:f::'], 38 | ['2001:db8::ff00:42:8329', '2001:db8::ff00:42:8329'], 39 | ['2001:db8:a:bac:8099:d:f:9', '2001:db8:a:bac:8099:d:f:9'], 40 | ]); 41 | } 42 | 43 | /** 44 | * @dataProvider provider_expandIPv6 45 | */ 46 | public function testExpandIpv6(string $expectation, string $ip): void 47 | { 48 | $this->assertEquals($expectation, PTR::expandIpv6($ip)); 49 | } 50 | 51 | /** 52 | * @dataProvider provider_contractIPv6 53 | */ 54 | public function testContractIpv6(string $ip, string $expectation): void 55 | { 56 | $this->assertEquals($expectation, PTR::contractIpv6($ip)); 57 | } 58 | 59 | public function testContractIpv6ThrowsException(): void 60 | { 61 | $this->expectException(\InvalidArgumentException::class); 62 | $this->expectExceptionMessage('"127.0.0.1" is not a valid IPv6 address.'); 63 | 64 | PTR::contractIpv6('127.0.0.1'); 65 | } 66 | 67 | public function testReverseIpv4(): void 68 | { 69 | $case_1 = '192.168.1.213'; 70 | $exp_1 = '213.1.168.192.in-addr.arpa.'; 71 | 72 | $this->assertEquals($exp_1, PTR::reverseIpv4($case_1)); 73 | } 74 | 75 | public function testReverseIpv6(): void 76 | { 77 | $case_1 = '2001:db8::567:89ab'; 78 | $case_2 = '8007:ea:19'; 79 | 80 | $exp_1 = 'b.a.9.8.7.6.5.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.8.b.d.0.1.0.0.2.ip6.arpa.'; 81 | $exp_2 = '9.1.0.0.a.e.0.0.7.0.0.8.ip6.arpa.'; 82 | 83 | $this->assertEquals($exp_1, PTR::reverseIpv6($case_1)); 84 | $this->assertEquals($exp_2, PTR::reverseIpv6($case_2)); 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /tests/Rdata/KxTest.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace Badcow\DNS\Tests\Rdata; 15 | 16 | use Badcow\DNS\Rdata\Factory; 17 | use Badcow\DNS\Rdata\KX; 18 | use PHPUnit\Framework\TestCase; 19 | 20 | class KxTest extends TestCase 21 | { 22 | public function testSetters(): void 23 | { 24 | $target = 'foo.example.com.'; 25 | $preference = 10; 26 | $kx = new KX(); 27 | $kx->setExchanger($target); 28 | $kx->setPreference($preference); 29 | 30 | $this->assertEquals($target, $kx->getExchanger()); 31 | $this->assertEquals($preference, $kx->getPreference()); 32 | } 33 | 34 | public function testOutput(): void 35 | { 36 | $target = 'foo.example.com.'; 37 | $kx = new KX(); 38 | $kx->SetExchanger($target); 39 | $kx->setPreference(42); 40 | 41 | $this->assertEquals('42 foo.example.com.', $kx->toText()); 42 | } 43 | 44 | public function testOutputThrowsExceptionWhenMissingPreference(): void 45 | { 46 | $kx = new KX(); 47 | $kx->setExchanger('mail.google.com.'); 48 | 49 | $this->expectException(\InvalidArgumentException::class); 50 | $this->expectExceptionMessage('No preference has been set on KX object.'); 51 | $kx->toText(); 52 | } 53 | 54 | public function testOutputThrowsExceptionWhenMissingExchanger(): void 55 | { 56 | $kx = new KX(); 57 | $kx->setPreference(15); 58 | 59 | $this->expectException(\InvalidArgumentException::class); 60 | $this->expectExceptionMessage('No exchanger has been set on KX object.'); 61 | $kx->toText(); 62 | } 63 | 64 | public function testFactory(): void 65 | { 66 | $kx = Factory::KX(15, 'mx.example.com.'); 67 | $this->assertInstanceOf(KX::class, $kx); 68 | $this->assertEquals('15 mx.example.com.', $kx->toText()); 69 | } 70 | 71 | public function testFromText(): void 72 | { 73 | $text = '10 mail.example.com.'; 74 | /** @var KX $kx */ 75 | $kx = new KX(); 76 | $kx->fromText($text); 77 | 78 | $this->assertEquals(10, $kx->getPreference()); 79 | $this->assertEquals('mail.example.com.', $kx->getExchanger()); 80 | } 81 | 82 | public function testWire(): void 83 | { 84 | $kx = new KX(); 85 | $kx->setExchanger('mail.example.com.'); 86 | $kx->setPreference(10); 87 | 88 | $expectation = pack('n', 10).chr(4).'mail'.chr(7).'example'.chr(3).'com'.chr(0); 89 | 90 | $this->assertEquals($expectation, $kx->toWire()); 91 | $fromWire = new KX(); 92 | $fromWire->fromWire($expectation); 93 | $this->assertEquals($kx, $fromWire); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /lib/Rdata/KX.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace Badcow\DNS\Rdata; 15 | 16 | use Badcow\DNS\Message; 17 | 18 | /** 19 | * {@link https://tools.ietf.org/html/rfc2230}. 20 | */ 21 | class KX implements RdataInterface 22 | { 23 | use RdataTrait; 24 | 25 | public const TYPE = 'KX'; 26 | public const TYPE_CODE = 36; 27 | 28 | /** 29 | * @var int 30 | */ 31 | private $preference; 32 | 33 | /** 34 | * @var string 35 | */ 36 | private $exchanger; 37 | 38 | public function setExchanger(string $exchanger): void 39 | { 40 | $this->exchanger = $exchanger; 41 | } 42 | 43 | public function getExchanger(): string 44 | { 45 | return $this->exchanger; 46 | } 47 | 48 | public function setPreference(int $preference): void 49 | { 50 | $this->preference = $preference; 51 | } 52 | 53 | public function getPreference(): int 54 | { 55 | return $this->preference; 56 | } 57 | 58 | /** 59 | * @throws \InvalidArgumentException throws exception if preference or exchanger have not been set 60 | */ 61 | public function toText(): string 62 | { 63 | if (null === $this->preference) { 64 | throw new \InvalidArgumentException('No preference has been set on KX object.'); 65 | } 66 | if (null === $this->exchanger) { 67 | throw new \InvalidArgumentException('No exchanger has been set on KX object.'); 68 | } 69 | 70 | return $this->preference.' '.$this->exchanger; 71 | } 72 | 73 | public function toWire(): string 74 | { 75 | if (null === $this->preference) { 76 | throw new \InvalidArgumentException('No preference has been set on KX object.'); 77 | } 78 | if (null === $this->exchanger) { 79 | throw new \InvalidArgumentException('No exchanger has been set on KX object.'); 80 | } 81 | 82 | return pack('n', $this->preference).Message::encodeName($this->exchanger); 83 | } 84 | 85 | public function fromText(string $text): void 86 | { 87 | $rdata = explode(' ', $text); 88 | $this->setPreference((int) $rdata[0]); 89 | $this->setExchanger($rdata[1]); 90 | } 91 | 92 | public function fromWire(string $rdata, int &$offset = 0, ?int $rdLength = null): void 93 | { 94 | if (false === $preference = unpack('n', $rdata, $offset)) { 95 | throw new DecodeException(static::TYPE, $rdata); 96 | } 97 | $this->setPreference($preference[1]); 98 | $offset += 2; 99 | $this->setExchanger(Message::decodeName($rdata, $offset)); 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /lib/Rdata/MX.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace Badcow\DNS\Rdata; 15 | 16 | use Badcow\DNS\Message; 17 | 18 | /** 19 | * @see https://tools.ietf.org/html/rfc1035#section-3.3.9 20 | */ 21 | class MX implements RdataInterface 22 | { 23 | use RdataTrait; 24 | 25 | public const TYPE = 'MX'; 26 | public const TYPE_CODE = 15; 27 | 28 | /** 29 | * @var int|null 30 | */ 31 | private $preference; 32 | 33 | /** 34 | * @var string|null 35 | */ 36 | private $exchange; 37 | 38 | public function setExchange(string $exchange): void 39 | { 40 | $this->exchange = $exchange; 41 | } 42 | 43 | public function getExchange(): ?string 44 | { 45 | return $this->exchange; 46 | } 47 | 48 | public function setPreference(int $preference): void 49 | { 50 | $this->preference = $preference; 51 | } 52 | 53 | public function getPreference(): ?int 54 | { 55 | return $this->preference; 56 | } 57 | 58 | /** 59 | * @throws \InvalidArgumentException throws exception if preference or exchange have not been set 60 | */ 61 | public function toText(): string 62 | { 63 | if (null === $this->preference) { 64 | throw new \InvalidArgumentException('No preference has been set on MX object.'); 65 | } 66 | 67 | if (null === $this->exchange) { 68 | throw new \InvalidArgumentException('No exchange has been set on MX object.'); 69 | } 70 | 71 | return $this->preference.' '.$this->exchange; 72 | } 73 | 74 | public function toWire(): string 75 | { 76 | if (null === $this->preference) { 77 | throw new \InvalidArgumentException('No preference has been set on MX object.'); 78 | } 79 | 80 | if (null === $this->exchange) { 81 | throw new \InvalidArgumentException('No exchange has been set on MX object.'); 82 | } 83 | 84 | return pack('n', $this->preference).Message::encodeName($this->exchange); 85 | } 86 | 87 | public function fromText(string $text): void 88 | { 89 | $rdata = explode(' ', $text); 90 | $this->setPreference((int) $rdata[0]); 91 | $this->setExchange($rdata[1]); 92 | } 93 | 94 | public function fromWire(string $rdata, int &$offset = 0, ?int $rdLength = null): void 95 | { 96 | if (false === $preference = unpack('n', $rdata, $offset)) { 97 | throw new DecodeException(static::TYPE, $rdata); 98 | } 99 | $this->setPreference($preference[1]); 100 | $offset += 2; 101 | $this->setExchange(Message::decodeName($rdata, $offset)); 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /tests/Rdata/SrvTest.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace Badcow\DNS\Tests\Rdata; 15 | 16 | use Badcow\DNS\Rdata\Factory; 17 | use Badcow\DNS\Rdata\SRV; 18 | use PHPUnit\Framework\TestCase; 19 | 20 | class SrvTest extends TestCase 21 | { 22 | public function testOutput(): void 23 | { 24 | $srv = Factory::SRV(10, 20, 666, 'doom.example.com.'); 25 | 26 | $expectation = '10 20 666 doom.example.com.'; 27 | 28 | $this->assertEquals($expectation, $srv->toText()); 29 | $this->assertEquals(10, $srv->getPriority()); 30 | $this->assertEquals(20, $srv->getWeight()); 31 | $this->assertEquals(666, $srv->getPort()); 32 | } 33 | 34 | /** 35 | * @throws \InvalidArgumentException 36 | */ 37 | public function testPortException(): void 38 | { 39 | $this->expectException(\InvalidArgumentException::class); 40 | $this->expectExceptionMessage('Port must be an unsigned integer on the range [0-65535]'); 41 | 42 | $srv = new SRV(); 43 | $srv->setPort(65536); 44 | } 45 | 46 | /** 47 | * @throws \InvalidArgumentException 48 | */ 49 | public function testPriorityException(): void 50 | { 51 | $this->expectException(\InvalidArgumentException::class); 52 | $this->expectExceptionMessage('Priority must be an unsigned integer on the range [0-65535]'); 53 | 54 | $srv = new SRV(); 55 | $srv->setPriority(65536); 56 | } 57 | 58 | /** 59 | * @throws \InvalidArgumentException 60 | */ 61 | public function testWeightException(): void 62 | { 63 | $this->expectException(\InvalidArgumentException::class); 64 | $this->expectExceptionMessage('Weight must be an unsigned integer on the range [0-65535]'); 65 | 66 | $srv = new SRV(); 67 | $srv->setWeight(65536); 68 | } 69 | 70 | public function testFromText(): void 71 | { 72 | $text = '0 1 80 www.example.com.'; 73 | $srv = new SRV(); 74 | $srv->setPriority(0); 75 | $srv->setWeight(1); 76 | $srv->setPort(80); 77 | $srv->setTarget('www.example.com.'); 78 | 79 | $fromText = new SRV(); 80 | $fromText->fromText($text); 81 | $this->assertEquals($srv, $fromText); 82 | } 83 | 84 | public function testWire(): void 85 | { 86 | $expectation = pack('nnn', 0, 1, 80).chr(3).'www'.chr(7).'example'.chr(3).'com'.chr(0); 87 | $srv = new SRV(); 88 | $srv->setPriority(0); 89 | $srv->setWeight(1); 90 | $srv->setPort(80); 91 | $srv->setTarget('www.example.com.'); 92 | 93 | $this->assertEquals($expectation, $srv->toWire()); 94 | $fromWire = new SRV(); 95 | $fromWire->fromWire($expectation); 96 | $this->assertEquals($srv, $fromWire); 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /lib/Rdata/CSYNC.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace Badcow\DNS\Rdata; 15 | 16 | use Badcow\DNS\Parser\Tokens; 17 | 18 | /** 19 | * {@link https://tools.ietf.org/html/rfc7477}. 20 | */ 21 | class CSYNC implements RdataInterface 22 | { 23 | use RdataTrait; 24 | 25 | public const TYPE = 'CSYNC'; 26 | public const TYPE_CODE = 62; 27 | 28 | /** 29 | * @var int 30 | */ 31 | private $soaSerial; 32 | 33 | /** 34 | * @var int 35 | */ 36 | private $flags; 37 | 38 | /** 39 | * @var array 40 | */ 41 | private $types = []; 42 | 43 | public function addType(string $type): void 44 | { 45 | $this->types[] = $type; 46 | } 47 | 48 | /** 49 | * Clears the types from the RDATA. 50 | */ 51 | public function clearTypes(): void 52 | { 53 | $this->types = []; 54 | } 55 | 56 | public function getTypes(): array 57 | { 58 | return $this->types; 59 | } 60 | 61 | public function getSoaSerial(): int 62 | { 63 | return $this->soaSerial; 64 | } 65 | 66 | public function setSoaSerial(int $soaSerial): void 67 | { 68 | $this->soaSerial = $soaSerial; 69 | } 70 | 71 | public function getFlags(): int 72 | { 73 | return $this->flags; 74 | } 75 | 76 | public function setFlags(int $flags): void 77 | { 78 | $this->flags = $flags; 79 | } 80 | 81 | public function toText(): string 82 | { 83 | return sprintf('%d %d %s', $this->soaSerial, $this->flags, implode(Tokens::SPACE, $this->types)); 84 | } 85 | 86 | /** 87 | * @throws UnsupportedTypeException 88 | */ 89 | public function toWire(): string 90 | { 91 | return pack('Nn', $this->soaSerial, $this->flags).NSEC::renderBitmap($this->types); 92 | } 93 | 94 | public function fromText(string $text): void 95 | { 96 | $rdata = explode(Tokens::SPACE, $text); 97 | $this->setSoaSerial((int) array_shift($rdata)); 98 | $this->setFlags((int) array_shift($rdata)); 99 | array_map([$this, 'addType'], $rdata); 100 | } 101 | 102 | /** 103 | * @throws UnsupportedTypeException|DecodeException 104 | */ 105 | public function fromWire(string $rdata, int &$offset = 0, ?int $rdLength = null): void 106 | { 107 | if (false === $integers = unpack('Nserial/nflags', $rdata, $offset)) { 108 | throw new DecodeException(static::TYPE, $rdata); 109 | } 110 | $offset += 6; 111 | $types = NSEC::parseBitmap($rdata, $offset); 112 | 113 | $this->setSoaSerial((int) $integers['serial']); 114 | $this->setFlags((int) $integers['flags']); 115 | array_map([$this, 'addType'], $types); 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /tests/Rdata/NsecTest.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace Badcow\DNS\Tests\Rdata; 15 | 16 | use Badcow\DNS\Rdata\A; 17 | use Badcow\DNS\Rdata\Factory; 18 | use Badcow\DNS\Rdata\MX; 19 | use Badcow\DNS\Rdata\NS; 20 | use Badcow\DNS\Rdata\NSEC; 21 | use Badcow\DNS\Rdata\PTR; 22 | use Badcow\DNS\Rdata\RRSIG; 23 | use PHPUnit\Framework\TestCase; 24 | 25 | class NsecTest extends TestCase 26 | { 27 | public function testOutput(): void 28 | { 29 | $expectation = 'host.example.com. A MX RRSIG NSEC'; 30 | 31 | $nsec = new NSEC(); 32 | $nsec->setNextDomainName('host.example.com.'); 33 | $nsec->addType(A::TYPE); 34 | $nsec->addType(MX::TYPE); 35 | $nsec->addType(RRSIG::TYPE); 36 | $nsec->addType(NSEC::TYPE); 37 | 38 | $this->assertEquals($expectation, $nsec->toText()); 39 | } 40 | 41 | public function testFactory(): void 42 | { 43 | $nextDomain = 'host.example.com.'; 44 | $bitMaps = [A::TYPE, MX::TYPE, RRSIG::TYPE, NSEC::TYPE]; 45 | $nsec = Factory::NSEC($nextDomain, $bitMaps); 46 | 47 | $this->assertEquals($nextDomain, $nsec->getNextDomainName()); 48 | $this->assertEquals($bitMaps, $nsec->getTypes()); 49 | } 50 | 51 | public function testClearTypeMap(): void 52 | { 53 | $nsec = new NSEC(); 54 | $nsec->addType(NS::TYPE); 55 | $nsec->addType(PTR::TYPE); 56 | 57 | $this->assertEquals([NS::TYPE, PTR::TYPE], $nsec->getTypes()); 58 | $nsec->clearTypes(); 59 | $this->assertEquals([], $nsec->getTypes()); 60 | } 61 | 62 | public function testFromText(): void 63 | { 64 | $text = 'host.example.com. A MX RRSIG NSEC TYPE1234'; 65 | /** @var NSEC $nsec */ 66 | $nsec = new NSEC(); 67 | $nsec->fromText($text); 68 | 69 | $this->assertEquals('host.example.com.', $nsec->getNextDomainName()); 70 | $this->assertEquals(['A', 'MX', 'RRSIG', 'NSEC', 'TYPE1234'], $nsec->getTypes()); 71 | $this->assertEquals($text, $nsec->toText()); 72 | } 73 | 74 | public function testWire(): void 75 | { 76 | $hexMatrix = [ 77 | 0x04, ord('h'), ord('o'), ord('s'), ord('t'), 78 | 0x07, ord('e'), ord('x'), ord('a'), ord('m'), ord('p'), ord('l'), ord('e'), 79 | 0x03, ord('c'), ord('o'), ord('m'), 0x00, 80 | 0x00, 0x06, 0x40, 0x01, 0x00, 0x00, 0x00, 0x03, 81 | 0x01, 0x01, 0x40, 82 | 0x80, 0x01, 0x40, 83 | ]; 84 | 85 | $expectation = pack('C*', ...$hexMatrix); 86 | 87 | $text = 'host.example.com. A MX RRSIG NSEC CAA DLV'; 88 | /** @var NSEC $nsec */ 89 | $nsec = new NSEC(); 90 | $nsec->fromText($text); 91 | 92 | $this->assertEquals($expectation, $nsec->toWire()); 93 | $fromWire = new NSEC(); 94 | $fromWire->fromWire($expectation); 95 | $this->assertEquals($nsec, $fromWire); 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /tests/Rdata/DsTest.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace Badcow\DNS\Tests\Rdata; 15 | 16 | use Badcow\DNS\Algorithms; 17 | use Badcow\DNS\Rdata\DNSKEY; 18 | use Badcow\DNS\Rdata\DS; 19 | use Badcow\DNS\Rdata\Factory; 20 | use PHPUnit\Framework\TestCase; 21 | 22 | class DsTest extends TestCase 23 | { 24 | private static $digest = '2BB183AF5F22588179A53B0A98631FAD1A292118'; 25 | 26 | public function testOutput(): void 27 | { 28 | $expectation = '60485 5 1 '.self::$digest; 29 | 30 | $ds = new DS(); 31 | $ds->setKeyTag(60485); 32 | $ds->setAlgorithm(Algorithms::RSASHA1); 33 | $ds->setDigestType(DS::DIGEST_SHA1); 34 | $ds->setDigest(hex2bin(self::$digest)); 35 | 36 | $this->assertEquals($expectation, $ds->toText()); 37 | } 38 | 39 | public function testFactory(): void 40 | { 41 | $keyTag = 60485; 42 | $ds = Factory::DS($keyTag, Algorithms::RSASHA1, hex2bin(self::$digest), DS::DIGEST_SHA1); 43 | 44 | $this->assertEquals($keyTag, $ds->getKeyTag()); 45 | $this->assertEquals(Algorithms::RSASHA1, $ds->getAlgorithm()); 46 | $this->assertEquals(hex2bin(self::$digest), $ds->getDigest()); 47 | $this->assertEquals(DS::DIGEST_SHA1, $ds->getDigestType()); 48 | } 49 | 50 | public function testFromText(): void 51 | { 52 | $expectation = new DS(); 53 | $expectation->setKeyTag(60485); 54 | $expectation->setAlgorithm(Algorithms::RSASHA1); 55 | $expectation->setDigestType(DS::DIGEST_SHA1); 56 | $expectation->setDigest(hex2bin(self::$digest)); 57 | 58 | $fromText = new DS(); 59 | $fromText->fromText('60485 5 1 '.self::$digest); 60 | $this->assertEquals($expectation, $fromText); 61 | } 62 | 63 | public function testWire(): void 64 | { 65 | $ds = new DS(); 66 | $ds->setKeyTag(60485); 67 | $ds->setAlgorithm(Algorithms::RSASHA1); 68 | $ds->setDigestType(DS::DIGEST_SHA1); 69 | $ds->setDigest(hex2bin(self::$digest)); 70 | $wireFormat = $ds->toWire(); 71 | 72 | $fromWire = new DS(); 73 | $fromWire->fromWire($wireFormat); 74 | 75 | $this->assertEquals($ds, $fromWire); 76 | } 77 | 78 | public function testCalculateDigest(): void 79 | { 80 | $algorithm = Algorithms::RSASHA1; 81 | $dnskey = new DNSKEY(); 82 | $dnskey->setPublicKey(base64_decode('AQOeiiR0GOMYkDshWoSKz9XzfwJr1AYtsmx3TGkJaNXVbfi/2pHm822aJ5iI9BMzNXxeYCmZDRD99WYwYqUSdjMmmAphXdvxegXd/M5+X7OrzKBaMbCVdFLUUh6DhweJBjEVv5f2wwjM9XzcnOf+EPbtG9DMBmADjFDc2w/rljwvFw==')); 83 | $dnskey->setAlgorithm($algorithm); 84 | $dnskey->setFlags(256); 85 | 86 | $ds = new DS(); 87 | $ds->setAlgorithm($algorithm); 88 | $ds->setKeyTag(60485); 89 | $ds->calculateDigest('DSKEY.example.com.', $dnskey); 90 | 91 | $this->assertEquals('60485 5 1 2BB183AF5F22588179A53B0A98631FAD1A292118', $ds->toText()); 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /tests/Rdata/KeyTest.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace Badcow\DNS\Tests\Rdata; 15 | 16 | use Badcow\DNS\Algorithms; 17 | use Badcow\DNS\Rdata\Factory; 18 | use Badcow\DNS\Rdata\KEY; 19 | use PHPUnit\Framework\TestCase; 20 | 21 | class KeyTest extends TestCase 22 | { 23 | /** 24 | * @var string 25 | */ 26 | private static $publicKey = 'AQPSKmynfzW4kyBv015MUG2DeIQ3Cbl+BBZH4b/0PY1kxkmvHjcZc8nokfzj31GajIQKY+5CptLr3buXA10hWqTkF7H6RfoRqXQeogmMHfpftf6zMv1LyBUgia7za6ZEzOJBOztyvhjL742iU/TpPSEDhm2SNKLijfUppn1UaNvv4w=='; 27 | 28 | public function testOutput(): void 29 | { 30 | $expectation = '256 3 5 '.self::$publicKey; 31 | 32 | $key = new KEY(); 33 | $key->setFlags(256); 34 | $key->setProtocol(3); 35 | $key->setAlgorithm(Algorithms::RSASHA1); 36 | $key->setPublicKey(base64_decode(self::$publicKey)); 37 | 38 | $this->assertEquals($expectation, $key->toText()); 39 | } 40 | 41 | public function testFactory(): void 42 | { 43 | $key = Factory::KEY(256, 3, Algorithms::RSASHA1, base64_decode(self::$publicKey)); 44 | $output = '256 3 5 '.self::$publicKey; 45 | 46 | $this->assertEquals(256, $key->getFlags()); 47 | $this->assertEquals(5, $key->getAlgorithm()); 48 | $this->assertEquals(base64_decode(self::$publicKey), $key->getPublicKey()); 49 | $this->assertEquals(3, $key->getProtocol()); 50 | $this->assertEquals($output, $key->toText()); 51 | } 52 | 53 | public function testFromText(): void 54 | { 55 | $rdata = '256 3 5 AQPSKmynfzW4kyBv015MUG2DeIQ3 Cbl+BBZH4b/0PY1kxkmvHjcZc8no kfzj31GajIQKY+5CptLr3buXA10h WqTkF7H6RfoRqXQeogmMHfpftf6z Mv1LyBUgia7za6ZEzOJBOztyvhjL 742iU/TpPSEDhm2SNKLijfUppn1U aNvv4w=='; 56 | $key = new KEY(); 57 | $key->setFlags(256); 58 | $key->setProtocol(3); 59 | $key->setAlgorithm(Algorithms::RSASHA1); 60 | $key->setPublicKey(base64_decode(self::$publicKey)); 61 | 62 | $fromText = new KEY(); 63 | $fromText->fromText($rdata); 64 | $this->assertEquals($key, $fromText); 65 | } 66 | 67 | public function testWire(): void 68 | { 69 | $wireFormat = pack('nCC', 256, 3, 5).base64_decode(self::$publicKey); 70 | 71 | $key = new KEY(); 72 | $key->setFlags(256); 73 | $key->setProtocol(3); 74 | $key->setAlgorithm(Algorithms::RSASHA1); 75 | $key->setPublicKey(base64_decode("AQPSKmynfzW4kyBv015MUG2DeIQ3Cbl+BBZH4b/\r\n0PY1kxkmvHjcZc8nokfzj31GajIQKY+5CptLr3buXA10hWqTkF7H6RfoRqXQe ogmMHfpftf6zMv1LyBUgia7za6ZEzOJBOztyvhjL742iU\n/TpPSEDhm2SNKLijfUppn1UaNvv4w==")); 76 | 77 | $this->assertEquals($wireFormat, $key->toWire()); 78 | 79 | $rdLength = strlen($wireFormat); 80 | $wireFormat = 'abcde'.$wireFormat.'fghijk'; 81 | $offset = 5; 82 | 83 | $fromWire = new KEY(); 84 | $fromWire->fromWire($wireFormat, $offset, $rdLength); 85 | $this->assertEquals($key, $fromWire); 86 | $this->assertEquals(5 + $rdLength, $offset); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /tests/Rdata/Nsec3paramTest.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace Badcow\DNS\Tests\Rdata; 15 | 16 | use Badcow\DNS\Algorithms; 17 | use Badcow\DNS\Rdata\Factory; 18 | use Badcow\DNS\Rdata\NSEC3PARAM; 19 | use PHPUnit\Framework\TestCase; 20 | 21 | class Nsec3paramTest extends TestCase 22 | { 23 | public function testGetType(): void 24 | { 25 | $nsec3param = new NSEC3PARAM(); 26 | $this->assertEquals('NSEC3PARAM', $nsec3param->getType()); 27 | } 28 | 29 | public function testGetTypeCode(): void 30 | { 31 | $nsec3param = new NSEC3PARAM(); 32 | $this->assertEquals(51, $nsec3param->getTypeCode()); 33 | } 34 | 35 | public function testToText(): void 36 | { 37 | $nsec3param = new NSEC3PARAM(); 38 | $nsec3param->setHashAlgorithm(Algorithms::RSAMD5); 39 | $nsec3param->setSalt('d9143ec07c5977ae'); 40 | $nsec3param->setIterations(55); 41 | $nsec3param->setFlags(0); 42 | 43 | $expectation = '1 0 55 d9143ec07c5977ae'; 44 | $this->assertEquals($expectation, $nsec3param->toText()); 45 | } 46 | 47 | public function testToWire(): void 48 | { 49 | $nsec3param = new NSEC3PARAM(); 50 | $nsec3param->setHashAlgorithm(Algorithms::RSAMD5); 51 | $nsec3param->setSalt('d9143ec07c5977ae'); 52 | $nsec3param->setIterations(55); 53 | $nsec3param->setFlags(0); 54 | 55 | $expectation = chr(1).chr(0).pack('n', 55).chr(8).hex2bin('d9143ec07c5977ae'); 56 | 57 | $this->assertEquals($expectation, $nsec3param->toWire()); 58 | } 59 | 60 | public function testFromText(): void 61 | { 62 | $expectation = new NSEC3PARAM(); 63 | $expectation->setHashAlgorithm(Algorithms::RSAMD5); 64 | $expectation->setSalt('d9143ec07c5977ae'); 65 | $expectation->setIterations(55); 66 | $expectation->setFlags(0); 67 | 68 | $fromText = new NSEC3PARAM(); 69 | $fromText->fromText('1 0 55 d9143ec07c5977ae'); 70 | $this->assertEquals($expectation, $fromText); 71 | } 72 | 73 | public function testFromWire(): void 74 | { 75 | $expectation = new NSEC3PARAM(); 76 | $expectation->setHashAlgorithm(Algorithms::RSAMD5); 77 | $expectation->setSalt('d9143ec07c5977ae'); 78 | $expectation->setIterations(55); 79 | $expectation->setFlags(0); 80 | 81 | $wireFormat = chr(1).chr(0).pack('n', 55).chr(8).hex2bin('d9143ec07c5977ae'); 82 | 83 | $fromWire = new NSEC3PARAM(); 84 | $fromWire->fromWire($wireFormat); 85 | $this->assertEquals($expectation, $fromWire); 86 | } 87 | 88 | public function testFactory(): void 89 | { 90 | $nsec3param = Factory::NSEC3PARAM(Algorithms::RSAMD5, 0, 55, 'd9143ec07c5977ae'); 91 | 92 | $this->assertEquals(1, $nsec3param->getHashAlgorithm()); 93 | $this->assertEquals(0, $nsec3param->getFlags()); 94 | $this->assertEquals(55, $nsec3param->getIterations()); 95 | $this->assertEquals('d9143ec07c5977ae', $nsec3param->getSalt()); 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /tests/Parser/ParseUnknownTypesTest.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace Badcow\DNS\Tests\Parser; 15 | 16 | use Badcow\DNS\Classes; 17 | use Badcow\DNS\Parser\ParseException; 18 | use Badcow\DNS\Parser\Parser; 19 | use Badcow\DNS\Rdata\A; 20 | use Badcow\DNS\Rdata\PolymorphicRdata; 21 | use Badcow\DNS\Rdata\UnknownType; 22 | use Badcow\DNS\ResourceRecord; 23 | use PHPUnit\Framework\TestCase; 24 | 25 | class ParseUnknownTypesTest extends TestCase 26 | { 27 | /** 28 | * @throws ParseException 29 | */ 30 | public function testUnknownType(): void 31 | { 32 | $text = 'dickens.example.com. CLASS45 1800 TYPE1859 \# 20 412054616c65206f662054776f20436974696573'; 33 | $binData = hex2bin('412054616c65206f662054776f20436974696573'); 34 | $zone = Parser::parse('example.com.', $text); 35 | $this->assertCount(1, $zone); 36 | /** @var ResourceRecord $rr */ 37 | $rr = $zone[0]; 38 | 39 | $this->assertEquals('dickens.example.com.', $rr->getName()); 40 | $this->assertEquals('CLASS45', $rr->getClass()); 41 | $this->assertEquals(45, $rr->getClassId()); 42 | $this->assertEquals('TYPE1859', $rr->getType()); 43 | $this->assertEquals(1800, $rr->getTtl()); 44 | $this->assertInstanceOf(UnknownType::class, $rr->getRdata()); 45 | $this->assertEquals($binData, $rr->getRdata()->getData()); 46 | $this->assertEquals(1859, $rr->getRdata()->getTypeCode()); 47 | } 48 | 49 | /** 50 | * @throws ParseException 51 | */ 52 | public function testPolymorphicType(): void 53 | { 54 | $text = 'dickens.example.com. IN 1800 RESERVED "A Tale of Two Cities"'; 55 | $zone = Parser::parse('example.com.', $text); 56 | $this->assertCount(1, $zone); 57 | /** @var ResourceRecord $rr */ 58 | $rr = $zone[0]; 59 | 60 | $this->assertEquals('dickens.example.com.', $rr->getName()); 61 | $this->assertEquals('IN', $rr->getClass()); 62 | $this->assertEquals(1, $rr->getClassId()); 63 | $this->assertEquals('RESERVED', $rr->getType()); 64 | $this->assertEquals(0xFFFF, $rr->getRdata()->getTypeCode()); 65 | $this->assertEquals(1800, $rr->getTtl()); 66 | $this->assertInstanceOf(PolymorphicRdata::class, $rr->getRdata()); 67 | $this->assertEquals('"A Tale of Two Cities"', $rr->getRdata()->getData()); 68 | } 69 | 70 | public function testSupportedTypeInUnknownFormatIsOutputtedAsCorrectType(): void 71 | { 72 | // files.example.com. IN 3600 IN A 192.168.1.100 73 | $record = 'files.example.com. IN 3600 TYPE1 \# 4 c0 a8 01 64'; 74 | $zone = Parser::parse('example.com.', $record); 75 | $this->assertCount(1, $zone); 76 | /** @var ResourceRecord $rr */ 77 | $rr = $zone[0]; 78 | 79 | $this->assertEquals('files.example.com.', $rr->getName()); 80 | $this->assertEquals(Classes::INTERNET, $rr->getClass()); 81 | $this->assertEquals(3600, $rr->getTtl()); 82 | $this->assertInstanceOf(A::class, $rr->getRdata()); 83 | $this->assertEquals('192.168.1.100', $rr->getRdata()->getAddress()); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /docs/Zone.md: -------------------------------------------------------------------------------- 1 | The DNS Zone Class 2 | ================== 3 | > A DNS zone is any distinct, contiguous portion of the domain name space in the Domain Name System (DNS) for which administrative responsibility has been delegated to a single manager. 4 | > 5 | >\- _[Wikipedia: DNS Zone](https://en.wikipedia.org/wiki/DNS_zone)_ 6 | 7 | The `Badcow\DNS\Zone` class is simply a collection of `Badcow\DNS\ResourceRecord` objects common to a single DNS Zone. 8 | The `Zone` object has three properties: 9 | - `name`: the name of the zone (e.g. "example.com.") 10 | - `defaultTtl`: The default time-to-live. If there is no TTL defined on the constituent Resource Records, then they 11 | default to this property. 12 | - `resourceRecords`: An array of `ResourceRecord` objects. 13 | 14 | These properties can be optionally set in the `Zone` constructor: 15 | ```php 16 | $resourceRecords = array(...); 17 | $zone = new Zone('example.com.' 10800, $resourceRecords); 18 | ``` 19 | 20 | ## Property Accessors 21 | The zone name, default TTL, and array of ResourceRecords can be accessed with simple getters: 22 | ```php 23 | echo $zone->getName(); 24 | echo $zone->getDefaultTtl(); 25 | print_r($zone->getResourceRecords()); 26 | ``` 27 | 28 | The `Zone` object implements `\ArrayAccess` so the `ResourceRecord` collection can be accessed like any other array: 29 | ```php 30 | $rr_1 = $zone[0]; 31 | $rr_2 = $zone[1]; 32 | ``` 33 | Similarly, `ResourceRecord` objects can be set and unset like any array: 34 | ```php 35 | unset($zone[2]); 36 | $zone[3] = $rr_4; 37 | ``` 38 | 39 | ## Property Assigners 40 | The zone name and default TTL can be assigned with simple setters: 41 | ```php 42 | $zone->setName('test.com.'); //Note: this is a fully qualified domain name! 43 | $zone->setDefaultTtl(3600); 44 | ``` 45 | There are three methods used for setting Resource Records: 46 | - `$zone->fromArray($rr_array);` This takes an array of `ResourceRecord` objects and adds them to the Zone. 47 | - `$zone->fromList($rr1, $rr2, $rr3, ...);` This takes a list of `ResourceRecord` objects and adds each of them to the Zone. 48 | - `$zone->addResourceRecord($rr);` This adds a single `ResourceRecord` to the Zone. 49 | 50 | **Note:** None of these methods overwrite the existing Resource Records in the Zone, they all append objects. 51 | 52 | ### Removing Resource Records 53 | Resource Records can be removed from the Zone using the `remove` method: 54 | ```php 55 | $zone = new Zone(); 56 | $resourceRecord = new Badcow\DNS\ResourceRecord(); 57 | $zone->addResourceRecord($resourceRecord); 58 | echo count($zone); //Echos "1". 59 | $zone->remove($resourceRecord); 60 | echo count($zone); //Echos "0". 61 | ``` 62 | 63 | ## Iteration 64 | `Zone` implements `\IteratorAggregate` for iterating over each `ResourceRecord`: 65 | ```php 66 | foreach ($zone as $i => $resourceRecord) { 67 | echo $resourceRecord->getName()."\n"; 68 | } 69 | ``` 70 | 71 | ## Countable 72 | `Zone` implements `\Countable`: 73 | ```php 74 | count($zone); //Returns the number of ResourceRecords 75 | $zone->count(); //Returns the number of ResourceRecords 76 | ``` 77 | 78 | ## Other Methods: 79 | - `Zone::getClass(): string` - Returns the first non-null Resource Record class (IN, CH, CS, or HS). If all Reource Record 80 | classes are null, it will return IN by default. 81 | - `Zone::contains(ResourceRecord $resourceRecord): bool` - Whether the Zone contains a particular Resource Record. 82 | - `Zone::isEmpty(): bool` - Whether or not the Zone's Resource Record array is empty. -------------------------------------------------------------------------------- /lib/Rdata/OPT.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace Badcow\DNS\Rdata; 15 | 16 | use Badcow\DNS\Edns\Option\Factory; 17 | use Badcow\DNS\Edns\Option\OptionInterface; 18 | use Badcow\DNS\Edns\Option\UnknownOption; 19 | use Badcow\DNS\Edns\Option\UnsupportedOptionException; 20 | 21 | /** 22 | * @see https://datatracker.ietf.org/doc/html/rfc6891#section-6.1.3 23 | */ 24 | class OPT implements RdataInterface 25 | { 26 | use RdataTrait; 27 | 28 | public const TYPE = 'OPT'; 29 | public const TYPE_CODE = 41; 30 | 31 | /** 32 | * @var OptionInterface[] 33 | */ 34 | protected $options = []; 35 | 36 | public function toText(): string 37 | { 38 | return ''; 39 | } 40 | 41 | public function fromText(string $text): void 42 | { 43 | throw new \Exception('Badcow\DNS\Rdata\OPT::fromText() cannot be used to hydrate this object.'); 44 | } 45 | 46 | /** 47 | * @param OptionInterface[]|null $options 48 | */ 49 | public function setOptions(?array $options): void 50 | { 51 | $this->options = []; 52 | 53 | if (null === $options) { 54 | return; 55 | } 56 | 57 | foreach ($options as $option) { 58 | $this->addOption($option); 59 | } 60 | } 61 | 62 | public function addOption(OptionInterface $option): void 63 | { 64 | $this->options[] = $option; 65 | } 66 | 67 | /** 68 | * @return OptionInterface[] 69 | */ 70 | public function getOptions(): array 71 | { 72 | return $this->options; 73 | } 74 | 75 | /** 76 | * @throws \InvalidArgumentException 77 | */ 78 | public function toWire(): string 79 | { 80 | $encoded = ''; 81 | if (!$this->options) { 82 | return $encoded; 83 | } 84 | foreach ($this->options as $option) { 85 | $optionValue = $option->toWire(); 86 | $encoded .= pack('nn', $option->getCode(), strlen($optionValue)); 87 | $encoded .= $optionValue; 88 | } 89 | 90 | return $encoded; 91 | } 92 | 93 | /** 94 | * @throws DecodeException 95 | */ 96 | public function fromWire(string $rdata, int &$offset = 0, ?int $rdLength = null): void 97 | { 98 | $rdLength = $rdLength ?? strlen($rdata); 99 | 100 | $endOffset = $offset + $rdLength; 101 | do { 102 | $integers = @unpack('ncode/nlength', $rdata, $offset); 103 | if (false === $integers) { 104 | throw new DecodeException(static::TYPE, $rdata); 105 | } 106 | $offset += 4; 107 | try { 108 | $option = Factory::newOptionFromId($integers['code']); 109 | } catch (UnsupportedOptionException $e) { 110 | $option = new UnknownOption(); 111 | $option->setOptionCode($integers['code']); 112 | } 113 | $option->fromWire($rdata, $offset, $integers['length']); 114 | $this->options[] = $option; 115 | } while ($offset < $endOffset); 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /tests/Rdata/TxtTest.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace Badcow\DNS\Tests\Rdata; 15 | 16 | use Badcow\DNS\Rdata\TXT; 17 | use PHPUnit\Framework\TestCase; 18 | 19 | class TxtTest extends TestCase 20 | { 21 | public function testSetText(): void 22 | { 23 | $text = 'This is some text. It\'s a nice piece of text.'; 24 | $txt = new TXT(); 25 | $txt->setText($text); 26 | 27 | $this->assertEquals($text, $txt->getText()); 28 | } 29 | 30 | public function dp_testToText(): array 31 | { 32 | return [ 33 | //'what is tested' => [$text, $expectation] 34 | 'quotes are escaped' => ['"This is some quoted text". It\'s a nice piece of text.', '"\"This is some quoted text\". It\'s a nice piece of text."'], 35 | 'spaces are wraped in quotes' => [ 36 | 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Duis vel lorem in massa elementum blandit nec sed massa. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec eu purus id arcu venenatis elementum in quis enim. Aenean at urna varius sapien dapibus.', 37 | '"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Duis vel lorem in massa elementum blandit nec sed massa. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec eu purus id arcu venenatis elementum in quis enim. Aenean at urna varius sapie" "n dapibus."', 38 | ], 39 | ]; 40 | } 41 | 42 | /** 43 | * @dataProvider dp_testToText 44 | * 45 | * @param string $text the input text value 46 | * @param string $expectation The expected output of TXT::toText() 47 | */ 48 | public function testToText(string $text, string $expectation): void 49 | { 50 | $txt = new TXT(); 51 | $txt->setText($text); 52 | 53 | $this->assertEquals($expectation, $txt->toText()); 54 | } 55 | 56 | public function dp_testFromTxt(): array 57 | { 58 | return [ 59 | //'what is tested' => [$text, $expectation] 60 | 'chunked text literal' => ['"Some text;" " another some text"', 'Some text; another some text'], 61 | 'string literal' => ['foobar', 'foobar'], 62 | 'text with space without quotes' => ['foo bar', 'foo'], 63 | 'trailing whitespace' => ["\t\t\tfoobar", 'foobar'], 64 | 'integer literal' => ['3600', '3600'], 65 | 'double escape sequence' => ['"double escape \\\\010"', 'double escape \010'], 66 | ]; 67 | } 68 | 69 | /** 70 | * @dataProvider dp_testFromTxt 71 | */ 72 | public function testFromTxt(string $text, string $expectation): void 73 | { 74 | $txt = new TXT(); 75 | $txt->fromText($text); 76 | $this->assertEquals($expectation, $txt->getText()); 77 | } 78 | 79 | public function testWire(): void 80 | { 81 | $expectation = 'This is some text. It\'s a nice piece of text.'; 82 | $txt = new TXT(); 83 | $txt->setText($expectation); 84 | 85 | $this->assertEquals($expectation, $txt->toWire()); 86 | $fromWire = new TXT(); 87 | $fromWire->fromWire($expectation); 88 | $this->assertEquals($txt, $fromWire); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /tests/Parser/ReverseRecordTest.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace Badcow\DNS\Tests\Parser; 15 | 16 | use Badcow\DNS\Classes; 17 | use Badcow\DNS\Parser\ParseException; 18 | use Badcow\DNS\Parser\Parser; 19 | use Badcow\DNS\Rdata\PTR; 20 | use PHPUnit\Framework\TestCase; 21 | 22 | class ReverseRecordTest extends TestCase 23 | { 24 | /** 25 | * @throws ParseException 26 | */ 27 | public function testReverseRecord(): void 28 | { 29 | $ptr = '1 1080 IN PTR gw01.core.acme.com.'; 30 | $zone = Parser::parse('50.100.200.in-addr.arpa.', $ptr); 31 | $rr = $zone->getResourceRecords()[0]; 32 | 33 | $this->assertEquals('1', $rr->getName()); 34 | $this->assertEquals(Classes::INTERNET, $rr->getClass()); 35 | $this->assertEquals(PTR::TYPE, $rr->getType()); 36 | $this->assertEquals('gw01.core.acme.com.', $rr->getRdata()->getTarget()); 37 | } 38 | 39 | /** 40 | * @throws ParseException|\Exception 41 | */ 42 | public function testParseReverseRecordFile(): void 43 | { 44 | $file = NormaliserTest::readFile(__DIR__.'/Resources/50.100.200.in-addr.arpa.db'); 45 | $zone = Parser::parse('50.100.200.in-addr.arpa.', $file); 46 | 47 | $parentRecords = ParserTest::findRecord('@', $zone); 48 | $_1Records = ParserTest::findRecord('1', $zone); 49 | $_50Records = ParserTest::findRecord('50', $zone); 50 | $_150Records = ParserTest::findRecord('150', $zone); 51 | $_170Records = ParserTest::findRecord('170', $zone); 52 | 53 | $this->assertCount(11, $zone); 54 | $this->assertCount(3, $parentRecords); 55 | $this->assertCount(2, $_1Records); 56 | $this->assertCount(1, $_50Records); 57 | $this->assertCount(1, $_150Records); 58 | 59 | $_1 = $_1Records[0]; 60 | $_50 = $_50Records[0]; 61 | $_150 = $_150Records[0]; 62 | $_170 = $_170Records[0]; 63 | 64 | $this->assertEquals('1', $_1->getName()); 65 | $this->assertEquals(1080, $_1->getTtl()); 66 | $this->assertEquals(Classes::INTERNET, $_1->getClass()); 67 | $this->assertEquals(PTR::TYPE, $_1->getType()); 68 | $this->assertEquals('gw01.core.acme.com.', $_1->getRdata()->getTarget()); 69 | 70 | $this->assertEquals('50', $_50->getName()); 71 | $this->assertEquals(1080, $_50->getTtl()); 72 | $this->assertEquals(Classes::INTERNET, $_50->getClass()); 73 | $this->assertEquals(PTR::TYPE, $_50->getType()); 74 | $this->assertEquals('mx1.acme.com.', $_50->getRdata()->getTarget()); 75 | 76 | $this->assertEquals('150', $_150->getName()); 77 | $this->assertEquals(200, $_150->getTtl()); 78 | $this->assertEquals(Classes::INTERNET, $_150->getClass()); 79 | $this->assertEquals(PTR::TYPE, $_150->getType()); 80 | $this->assertEquals('smtp.example.com.', $_150->getRdata()->getTarget()); 81 | 82 | $this->assertEquals('170', $_170->getName()); 83 | $this->assertEquals(150, $_170->getTtl()); 84 | $this->assertEquals(Classes::INTERNET, $_170->getClass()); 85 | $this->assertEquals(PTR::TYPE, $_170->getType()); 86 | $this->assertEquals('netscape.com.', $_170->getRdata()->getTarget()); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /tests/Rdata/ATest.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace Badcow\DNS\Tests\Rdata; 15 | 16 | use Badcow\DNS\Rdata\A; 17 | use Badcow\DNS\Rdata\DecodeException; 18 | use PHPUnit\Framework\TestCase; 19 | 20 | class ATest extends TestCase 21 | { 22 | /** 23 | * @var A 24 | */ 25 | private $aRdata; 26 | 27 | public function setUp(): void 28 | { 29 | $this->aRdata = new A(); 30 | } 31 | 32 | public function testGetType(): void 33 | { 34 | $this->assertEquals('A', $this->aRdata->getType()); 35 | } 36 | 37 | public function testSetAddress(): void 38 | { 39 | $address = '192.168.1.1'; 40 | $this->aRdata->setAddress($address); 41 | 42 | $this->assertEquals($address, $this->aRdata->getAddress()); 43 | 44 | $this->expectException(\InvalidArgumentException::class); 45 | $this->expectExceptionMessage('The address "abc" is not a valid IPv4 address.'); 46 | $this->aRdata->setAddress('abc'); 47 | } 48 | 49 | public function testOutput(): void 50 | { 51 | $address = '192.168.1.1'; 52 | $this->aRdata->setAddress($address); 53 | 54 | $this->assertEquals($address, $this->aRdata->toText()); 55 | $this->assertEquals($address, $this->aRdata->toText()); 56 | } 57 | 58 | public function testFromText(): void 59 | { 60 | $text = '200.100.50.1'; 61 | /** @var A $a */ 62 | $a = new A(); 63 | $a->fromText($text); 64 | 65 | $this->assertEquals($text, $a->getAddress()); 66 | } 67 | 68 | /** 69 | * @throws DecodeException 70 | */ 71 | public function testWire(): void 72 | { 73 | $address = '200.100.50.1'; 74 | $expectation = inet_pton($address); 75 | /** @var A $a */ 76 | $a = new A(); 77 | $a->fromWire($expectation); 78 | 79 | $this->assertEquals($expectation, $a->toWire()); 80 | $this->assertEquals($address, $a->getAddress()); 81 | } 82 | 83 | public function testToWireThrowsExceptionIfAddressIsMalformed(): void 84 | { 85 | $a_prime = new class() extends A { 86 | public function __construct() 87 | { 88 | $this->address = 'abc'; 89 | } 90 | }; 91 | 92 | $this->expectException(\InvalidArgumentException::class); 93 | $this->expectExceptionMessage('The IP address "abc" cannot be encoded. Check that it is a valid IP address.'); 94 | $a_prime->toWire(); 95 | } 96 | 97 | /** 98 | * @throws DecodeException 99 | */ 100 | public function testFromWire(): void 101 | { 102 | $wire = pack('C6', 0x07, 0xC0, 0xFF, 0x01, 0x01, 0x07); //⍾192.255.1.1⍾ 103 | $offset = 1; 104 | /** @var A $a */ 105 | $a = new A(); 106 | $a->fromWire($wire, $offset); 107 | 108 | $this->assertEquals('192.255.1.1', $a->getAddress()); 109 | $this->assertEquals(chr(0x07), $wire[$offset]); 110 | 111 | $wire = pack('C3', 0x61, 0x62, 0x63); 112 | $a = new A(); 113 | $this->expectException(DecodeException::class); 114 | $a->fromWire($wire); 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /tests/Rdata/DnskeyTest.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace Badcow\DNS\Tests\Rdata; 15 | 16 | use Badcow\DNS\Algorithms; 17 | use Badcow\DNS\Rdata\DNSKEY; 18 | use Badcow\DNS\Rdata\Factory; 19 | use PHPUnit\Framework\TestCase; 20 | 21 | class DnskeyTest extends TestCase 22 | { 23 | /** 24 | * @var string 25 | */ 26 | private static $publicKey = 'AQPSKmynfzW4kyBv015MUG2DeIQ3Cbl+BBZH4b/0PY1kxkmvHjcZc8nokfzj31GajIQKY+5CptLr3buXA10hWqTkF7H6RfoRqXQeogmMHfpftf6zMv1LyBUgia7za6ZEzOJBOztyvhjL742iU/TpPSEDhm2SNKLijfUppn1UaNvv4w=='; 27 | 28 | public function testOutput(): void 29 | { 30 | $expectation = '256 3 5 '.self::$publicKey; 31 | 32 | $dnskey = new DNSKEY(); 33 | $dnskey->setFlags(256); 34 | $dnskey->setAlgorithm(Algorithms::RSASHA1); 35 | $dnskey->setPublicKey(base64_decode(self::$publicKey)); 36 | 37 | $this->assertEquals($expectation, $dnskey->toText()); 38 | } 39 | 40 | public function testSetProtocolThrowsException(): void 41 | { 42 | $dnskey = new DNSKEY(); 43 | $this->expectException(\InvalidArgumentException::class); 44 | $dnskey->setProtocol(2); 45 | } 46 | 47 | public function testFactory(): void 48 | { 49 | $dnskey = Factory::DNSKEY(256, Algorithms::RSASHA1, base64_decode(self::$publicKey)); 50 | $output = '256 3 5 '.self::$publicKey; 51 | 52 | $this->assertEquals(256, $dnskey->getFlags()); 53 | $this->assertEquals(5, $dnskey->getAlgorithm()); 54 | $this->assertEquals(base64_decode(self::$publicKey), $dnskey->getPublicKey()); 55 | $this->assertEquals(3, $dnskey->getProtocol()); 56 | $this->assertEquals($output, $dnskey->toText()); 57 | } 58 | 59 | public function testFromText(): void 60 | { 61 | $rdata = '256 3 5 AQPSKmynfzW4kyBv015MUG2DeIQ3 Cbl+BBZH4b/0PY1kxkmvHjcZc8no kfzj31GajIQKY+5CptLr3buXA10h WqTkF7H6RfoRqXQeogmMHfpftf6z Mv1LyBUgia7za6ZEzOJBOztyvhjL 742iU/TpPSEDhm2SNKLijfUppn1U aNvv4w=='; 62 | $dnskey = new DNSKEY(); 63 | $dnskey->setFlags(256); 64 | $dnskey->setProtocol(3); 65 | $dnskey->setAlgorithm(Algorithms::RSASHA1); 66 | $dnskey->setPublicKey(base64_decode(self::$publicKey)); 67 | 68 | $fromText = new DNSKEY(); 69 | $fromText->fromText($rdata); 70 | $this->assertEquals($dnskey, $fromText); 71 | } 72 | 73 | public function testWire(): void 74 | { 75 | $wireFormat = pack('nCC', 256, 3, 5).base64_decode(self::$publicKey); 76 | 77 | $dnskey = new DNSKEY(); 78 | $dnskey->setFlags(256); 79 | $dnskey->setAlgorithm(Algorithms::RSASHA1); 80 | $dnskey->setPublicKey(base64_decode("AQPSKmynfzW4kyBv015MUG2DeIQ3Cbl+BBZH4b/\r\n0PY1kxkmvHjcZc8nokfzj31GajIQKY+5CptLr3buXA10hWqTkF7H6RfoRqXQe ogmMHfpftf6zMv1LyBUgia7za6ZEzOJBOztyvhjL742iU\n/TpPSEDhm2SNKLijfUppn1UaNvv4w==")); 81 | 82 | $this->assertEquals($wireFormat, $dnskey->toWire()); 83 | 84 | $rdLength = strlen($wireFormat); 85 | $wireFormat = 'abcde'.$wireFormat.'fghijk'; 86 | $offset = 5; 87 | 88 | $fromWire = new DNSKEY(); 89 | $fromWire->fromWire($wireFormat, $offset, $rdLength); 90 | 91 | $this->assertEquals($dnskey, $fromWire); 92 | $this->assertEquals(5 + $rdLength, $offset); 93 | } 94 | } 95 | --------------------------------------------------------------------------------