├── .coveralls.yml ├── .gitignore ├── .php_cs.dist ├── .scrutinizer.yml ├── .travis.yml ├── CHANGELOG-1.x.md ├── LICENSE.md ├── README.md ├── bin ├── PhpDnsConsole.php ├── PhpDnsInstaller.php └── PhpDnsServer.php ├── composer.json ├── console.box.json ├── docs ├── Basic-Usage.md ├── Event-Dispatcher.md ├── Json-Resolver.md └── XML-Resolver.md ├── example ├── example.com.db ├── example.com.json ├── example.com.xml ├── example.com.yml ├── example.php └── record.json ├── installer.box.json ├── php-dns-server.xsd ├── phpunit ├── phpunit.xml.dist ├── server.box.json ├── src ├── ClassEnum.php ├── Config │ ├── FileConfig.php │ └── RecursiveArrayObject.php ├── Console │ ├── CommandServer.php │ └── Commands │ │ └── VersionCommand.php ├── Decoder.php ├── Encoder.php ├── Event │ ├── Events.php │ ├── MessageEvent.php │ ├── QueryReceiveEvent.php │ ├── QueryResponseEvent.php │ ├── ServerExceptionEvent.php │ ├── ServerStartEvent.php │ └── Subscriber │ │ ├── EchoLogger.php │ │ ├── LoggerSubscriber.php │ │ └── ServerTerminator.php ├── Exception │ ├── ConfigFileNotFoundException.php │ ├── InvalidZoneFileException.php │ └── ZoneFileNotFoundException.php ├── Filesystem │ └── FilesystemManager.php ├── Header.php ├── Message.php ├── RdataDecoder.php ├── RdataEncoder.php ├── RecordTypeEnum.php ├── Resolver │ ├── AbstractResolver.php │ ├── ArrayRdata.php │ ├── BindResolver.php │ ├── JsonFileSystemResolver.php │ ├── JsonResolver.php │ ├── ResolverInterface.php │ ├── StackableResolver.php │ ├── SystemResolver.php │ ├── XmlResolver.php │ └── YamlResolver.php ├── ResourceRecord.php ├── Server.php └── UnsupportedTypeException.php ├── tests ├── ClassEnumTest.php ├── DecoderTest.php ├── DummyResolver.php ├── EncoderTest.php ├── MockSocket.php ├── RecordTypeEnumTest.php ├── Resolver │ ├── AbstractResolverTest.php │ ├── BindResolverTest.php │ ├── JsonResolverTest.php │ ├── StackableResolverTest.php │ ├── SystemResolverTest.php │ ├── XmlResolverTest.php │ └── YamlResolverTest.php ├── Resources │ ├── example.com-2.db │ ├── example.com.db │ ├── example.com.json │ ├── example.com.xml │ ├── example.com.yml │ ├── invalid_dns_records.json │ ├── records.yml │ ├── test.com.xml │ ├── test2.com.db │ ├── test2.com.xml │ └── test_records.json └── ServerTest.php └── vendor-bin └── box └── composer.json /.coveralls.yml: -------------------------------------------------------------------------------- 1 | service_name: travis-ci 2 | coverage_clover: clover.xml 3 | json_path: coveralls-upload.json -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | composer.lock 2 | .project 3 | /vendor/ 4 | .README.md.html 5 | /.idea 6 | clover.xml 7 | /nbproject 8 | phpunit.xml 9 | .php_cs.cache -------------------------------------------------------------------------------- /.php_cs.dist: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | $header = <<<'EOF' 13 | This file is part of PHP DNS Server. 14 | 15 | (c) Yif Swery 16 | 17 | For the full copyright and license information, please view the LICENSE 18 | file that was distributed with this source code. 19 | EOF; 20 | 21 | $finder = PhpCsFixer\Finder::create() 22 | ->in(__DIR__.'/src') 23 | ->in(__DIR__.'/tests') 24 | ->exclude(__DIR__.'/tests/Resources') 25 | ; 26 | 27 | $config = PhpCsFixer\Config::create() 28 | ->setRiskyAllowed(true) 29 | ->setRules([ 30 | '@Symfony' => true, 31 | 'header_comment' => ['header' => $header], 32 | 'array_syntax' => ['syntax' => 'short'], 33 | 'no_superfluous_phpdoc_tags' => false, 34 | ]) 35 | ->setFinder($finder) 36 | ; 37 | 38 | return $config; 39 | -------------------------------------------------------------------------------- /.scrutinizer.yml: -------------------------------------------------------------------------------- 1 | filter: 2 | excluded_paths: [src/Tests/*] 3 | checks: 4 | php: 5 | code_rating: true 6 | remove_extra_empty_lines: true 7 | remove_php_closing_tag: true 8 | remove_trailing_whitespace: true 9 | fix_use_statements: 10 | remove_unused: true 11 | preserve_multiple: false 12 | preserve_blanklines: true 13 | order_alphabetically: true 14 | fix_php_opening_tag: true 15 | fix_linefeed: true 16 | fix_line_ending: true 17 | fix_identation_4spaces: true 18 | fix_doc_comments: true 19 | tools: 20 | external_code_coverage: true 21 | php_analyzer: true 22 | php_code_coverage: false 23 | php_code_sniffer: 24 | config: 25 | standard: PSR2 26 | filter: 27 | paths: ['src'] 28 | php_loc: 29 | enabled: true 30 | excluded_dirs: ['vendor', 'example'] 31 | php_cpd: 32 | enabled: true 33 | excluded_dirs: ['vendor', 'example'] -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | 3 | php: 4 | - "7.2" 5 | - "7.3" 6 | - "7.4" 7 | 8 | before_script: 9 | - composer install 10 | 11 | script: vendor/bin/phpunit --coverage-clover=coverage.clover 12 | 13 | after_script: 14 | - php vendor/bin/php-coveralls -v 15 | - wget https://scrutinizer-ci.com/ocular.phar 16 | - php ocular.phar code-coverage:upload --format=php-clover coverage.clover 17 | 18 | -------------------------------------------------------------------------------- /CHANGELOG-1.x.md: -------------------------------------------------------------------------------- 1 | CHANGELOG for 1.4 2 | ================= 3 | ## v1.4.1 4 | * [PR #102](https://github.com/yswery/PHP-DNS-SERVER/pull/102) Require `symfony/property-access` version `4.3` instead of `5.0`. 5 | ## v1.4.0 6 | * [PR #90](https://github.com/yswery/PHP-DNS-SERVER/pull/90) Added the beginning of a cli interface for PhpDnsServer, and a filesystem config for loading all .json files from a zones directory. 7 | * [PR #93](https://github.com/yswery/PHP-DNS-SERVER/pull/93) Change private `Server` properties to protected. 8 | * [PR #98](https://github.com/yswery/PHP-DNS-SERVER/pull/98) Drop support for PHP 7.1. 9 | * [PR #99](https://github.com/yswery/PHP-DNS-SERVER/pull/99) New `BindResolver` class to add support for Bind9/named style records. 10 | * [PR #100](https://github.com/yswery/PHP-DNS-SERVER/pull/100) New logger subscriber that listens too all events. 11 | 12 | CHANGELOG for 1.3 13 | ================= 14 | * New specialised classes for encoding and decoding Rdata: `RdataEncoder` & `RdataDecoder`. 15 | * Upgrade to React Socket v1.2 and greater. 16 | * Add `ServerTerminator` subscriber. 17 | 18 | CHANGELOG for 1.2 19 | ================= 20 | * New event `SERVER_START_FAIL` triggered when the server fails to start. 21 | * RData encoding and decoding methods separated into their own classess: `RdataEncoder` and `RdataDecoder`. 22 | * It is now optional to inject an `EventDispatcher` into `Server` instance. 23 | 24 | CHANGELOG for 1.1 25 | ================= 26 | * Normalised RDATA naming conventions to be consistent with RFC1035. 27 | * Tests moved into the main src directory. 28 | * Updated PHPUnit to latest version (v7.3.*). 29 | * Implemented PSR Logger. 30 | * The message, header and resource records are now represented by objects. 31 | * Ability to respond to server events via event subscriber component. 32 | * Optionally store dns records in Yaml or XML format. 33 | * Implemented Symfony Event dispatcher. 34 | * Resolvers support wildcard domains. 35 | * Additional record processing happens automatically for SRV, NS and MX records. 36 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016-2018 Yif Swery 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/yswery/PHP-DNS-SERVER.svg)](https://travis-ci.org/yswery/PHP-DNS-SERVER) 2 | [![Coverage Status](https://coveralls.io/repos/yswery/PHP-DNS-SERVER/badge.png)](https://coveralls.io/github/yswery/PHP-DNS-SERVER) 3 | [![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/samuelwilliams/PHP-DNS-SERVER/badges/quality-score.png)](https://scrutinizer-ci.com/g/samuelwilliams/PHP-DNS-SERVER/) 4 | 5 | 6 | # PHP DNS Server 7 | 8 | This is an Authoritative DNS Server written in pure PHP. 9 | It will listen to DNS request on the default port (Default: port 53) and give answers about any domain that it has DNS records for. 10 | This class can be used to give DNS responses dynamically based on your pre-existing PHP code. 11 | 12 | ## Requirements 13 | 14 | * `PHP 7.2+` 15 | * Needs either `sockets` or `socket_create` PHP extension loaded (which they are by default) 16 | 17 | ## Example 18 | 19 | Here is an example of DNS server usage: 20 | ```php 21 | require_once __DIR__.'/../vendor/autoload.php'; 22 | 23 | // JsonResolver created and provided with path to file with json dns records 24 | $jsonResolver = new yswery\DNS\Resolver\JsonResolver([ 25 | '/path/to/zones/example.com.json', 26 | '/path/to/zone/test.com.json', 27 | ]); 28 | 29 | // System resolver acting as a fallback to the JsonResolver 30 | $systemResolver = new yswery\DNS\Resolver\SystemResolver(); 31 | 32 | // StackableResolver will try each resolver in order and return the first match 33 | $stackableResolver = new yswery\DNS\Resolver\StackableResolver([$jsonResolver, $systemResolver]); 34 | 35 | // Create the eventDispatcher and add the event subscribers 36 | $eventDispatcher = new \Symfony\Component\EventDispatcher\EventDispatcher(); 37 | $eventDispatcher->addSubscriber(new \yswery\DNS\Event\Subscriber\EchoLogger()); 38 | 39 | // Create a new instance of Server class 40 | $server = new yswery\DNS\Server($stackableResolver, $eventDispatcher); 41 | 42 | // Start DNS server 43 | $server->start(); 44 | 45 | ``` 46 | ### Running example 47 | 48 | * Run `composer install` to install dependencies 49 | * Run `php example/example.php` to run the server 50 | 51 | Query server using `dig` command to ensure proper functioning 52 | ```bash 53 | $ dig @127.0.0.1 test.com A +short 54 | 111.111.111.111 55 | 56 | $ dig @127.0.0.1 test.com TXT +short 57 | "Some text." 58 | 59 | $ dig @127.0.0.1 test2.com A +short 60 | 111.111.111.111 61 | 112.112.112.112 62 | ``` 63 | ## Zone File Storage 64 | PHP DNS Server supports four zone file formats out-of-the-box: Bind, JSON, XML, and YAML; each file format 65 | is supported by a specialised `Resolver` class: `BindResolver`, `JsonResolver`, `XmlResolver`, and `YamlResolver`, 66 | respectively. Example files are in the `example/` directory. 67 | 68 | ### JSON zone example 69 | ```json 70 | { 71 | "domain": "example.com.", 72 | "default-ttl": 7200, 73 | "resource-records": [ 74 | { 75 | "name": "@", 76 | "ttl": 10800, 77 | "type": "SOA", 78 | "class": "IN", 79 | "mname": "example.com.", 80 | "rname": "postmaster", 81 | "serial": 2, 82 | "refresh": 3600, 83 | "retry": 7200, 84 | "expire": 10800, 85 | "minimum": 3600 86 | }, { 87 | "type": "A", 88 | "address": "12.34.56.78" 89 | },{ 90 | "type": "A", 91 | "address": "90.12.34.56" 92 | }, { 93 | "type": "AAAA", 94 | "address": "2001:acad:ad::32" 95 | }, { 96 | "name": "www", 97 | "type": "cname", 98 | "target": "@" 99 | }, { 100 | "name": "@", 101 | "type": "MX", 102 | "preference": 15, 103 | "exchange": "mail" 104 | }, { 105 | "name": "*.subdomain", 106 | "ttl": 3600, 107 | "type": "A", 108 | "address": "192.168.1.42" 109 | } 110 | ] 111 | } 112 | ``` 113 | 114 | ## Running Tests 115 | 116 | Unit tests using PHPUnit are provided. A simple script is located in the root. 117 | 118 | * run `composer install` to install PHPUnit and dependencies 119 | * run `vendor/bin/phpunit` from the root to run the tests 120 | 121 | ## Building .phar 122 | * run `composer run-script build-server` to build the phpdnsserver.phar file, outputs in the bin folder. 123 | * run `composer run-script build-console` to build the phpdnscli.phar file, outputs in the bin folder. 124 | * run `composer run-script build-installer` to build the installer. Windows support for the installer is currently limited. 125 | 126 | ## Running the .phar files 127 | To run the new .phar files, download them from the release and move them to the desired folder. 128 | * `phpdnsserver.phar` to run the phpdnsserver, uses the new filesystem by default 129 | * `phpdnscli.phar` to run cli commands 130 | * `phpdnsinstaller.phar` as root to create required folders and default config. 131 | 132 | ## Supported command line switches 133 | * `--bind:b` - bind to a specific ip. Uses `0.0.0.0` by default 134 | * `--port:p` - bind to a specific port. Uses port `53` by default 135 | * `--config:c` - specify the config file. Uses `phpdns.json` on windows 136 | and `/etc/phpdns.json` on unix systems by default 137 | * `--storage:s` - specify the path to the storage for zones, and logs. Uses `/etc/phpdnsserver` on unix, 138 | and current working directory on windows. 139 | 140 | ## Supported Record Types 141 | 142 | * A 143 | * NS 144 | * CNAME 145 | * SOA 146 | * PTR 147 | * MX 148 | * TXT 149 | * AAAA 150 | * AXFR 151 | * ANY 152 | * SRV 153 | 154 | ## License 155 | 156 | The MIT License (MIT) 157 | 158 | Copyright (c) 2016-2017 Yif Swery 159 | 160 | Permission is hereby granted, free of charge, to any person obtaining a copy of 161 | this software and associated documentation files (the "Software"), to deal in 162 | the Software without restriction, including without limitation the rights to 163 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 164 | the Software, and to permit persons to whom the Software is furnished to do so, 165 | subject to the following conditions: 166 | 167 | The above copyright notice and this permission notice shall be included in all 168 | copies or substantial portions of the Software. 169 | 170 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 171 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 172 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 173 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 174 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 175 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 176 | -------------------------------------------------------------------------------- /bin/PhpDnsConsole.php: -------------------------------------------------------------------------------- 1 | addSubscriber(new \yswery\DNS\Event\Subscriber\EchoLogger()); 8 | 9 | // Create a new instance of Server class 10 | $server = new yswery\DNS\Console\CommandServer(); 11 | 12 | // Start DNS server 13 | $server->run(); -------------------------------------------------------------------------------- /bin/PhpDnsInstaller.php: -------------------------------------------------------------------------------- 1 | '0.0.0.0', 24 | 'port' => 53, 25 | 'storage' => '/etc/phpdnsserver', 26 | 'backend' => 'file' 27 | ]; 28 | 29 | $filesystem = new \Symfony\Component\Filesystem\Filesystem; 30 | 31 | try { 32 | echo "Creating required directories and config files...\n"; 33 | 34 | $filesystem->mkdir('/etc/phpdnsserver'); 35 | $filesystem->mkdir('/etc/phpdnsserver/zones'); 36 | $filesystem->mkdir('/etc/phpdnsserver/logs'); 37 | 38 | // create default config 39 | file_put_contents('/etc/phpdns.json', json_encode(getcwd())); 40 | } catch (\Symfony\Component\Filesystem\Exception\IOException $e){ 41 | die("An error occurred during installation\n".$e->getMessage()); 42 | } 43 | 44 | } else { 45 | // create the default config file 46 | $defaultConfig = [ 47 | 'host' => '0.0.0.0', 48 | 'port' => 53, 49 | 'backend' => 'file' 50 | ]; 51 | 52 | $filesystem = new \Symfony\Component\Filesystem\Filesystem; 53 | 54 | try { 55 | echo "Creating required directories and config files...\n"; 56 | 57 | $filesystem->mkdir(getcwd().'\\zones'); 58 | $filesystem->mkdir(getcwd().'\\logs'); 59 | 60 | // create default config 61 | file_put_contents(getcwd().'\\phpdns.json', json_encode(getcwd())); 62 | } catch (\Symfony\Component\Filesystem\Exception\IOException $e){ 63 | die("An error occurred during installation\n".$e->getMessage()); 64 | } 65 | } -------------------------------------------------------------------------------- /bin/PhpDnsServer.php: -------------------------------------------------------------------------------- 1 | opt('bind:b', 'Bind to a specific ip interface', false) 12 | ->opt('port:p', 'specify the port to bind to', false) 13 | ->opt('config:c', 'specify the path to the phpdns.json file', false) 14 | ->opt('storage:s', 'specify the location to zone storage folder', false); 15 | 16 | $args = $cli->parse($argv, true); 17 | 18 | // defaults 19 | $host = $args->getOpt('bind', '0.0.0.0'); 20 | $port = $args->getOpt('port', 53); 21 | 22 | // figure out config location 23 | if (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') { 24 | // default to current working directory on windows 25 | $configFile = $args->getOpt('config', getcwd() . '/phpdns.json'); 26 | $storageDirectory = $args->getOpt('storage', getcwd()); 27 | } else { 28 | // default to /etc/phpdns.json and /etc/phpdnsserver if not on windows 29 | $configFile = $args->getOpt('config', '/etc/phpdns.json'); 30 | $storageDirectory = $args->getOpt('storage', '/etc/phpdnserver'); 31 | } 32 | 33 | // initialize the configuration 34 | $config = new yswery\DNS\Config\FileConfig($configFile); 35 | try { 36 | $config->load(); 37 | 38 | if ($config->has('host')) { 39 | $host = $config->get('host'); 40 | } 41 | 42 | if ($config->has('port')) { 43 | $port = $config->get('port'); 44 | } 45 | 46 | if ($config->has('storage')) { 47 | $storageDirectory = $config->get('storage'); 48 | } 49 | } catch (\yswery\DNS\Exception\ConfigFileNotFoundException $e) { 50 | echo $e->getMessage(); 51 | } 52 | 53 | // Create the eventDispatcher and add the event subscribers 54 | $eventDispatcher = new \Symfony\Component\EventDispatcher\EventDispatcher(); 55 | $eventDispatcher->addSubscriber(new \yswery\DNS\Event\Subscriber\EchoLogger()); 56 | 57 | try { 58 | // Create a new instance of Server class 59 | $server = new yswery\DNS\Server(null, $eventDispatcher, $config, $storageDirectory, true, $host, $port); 60 | 61 | // Start DNS server 62 | $server->run(); 63 | } catch (\Exception $e) { 64 | echo $e->getMessage(); 65 | } 66 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "yswery/dns", 3 | "description": "A DNS server implementation in pure PHP.", 4 | "require-dev": { 5 | "phpunit/phpunit": "~7.3", 6 | "php-coveralls/php-coveralls": "~2.1", 7 | "bamarni/composer-bin-plugin": "^1.3", 8 | "humbug/box": ">=3.6", 9 | "friendsofphp/php-cs-fixer": "^2.16" 10 | }, 11 | "license": "MIT", 12 | "authors": [ 13 | { 14 | "name": "Yif Swery", 15 | "email": "yiftachswr@gmail.com" 16 | }, 17 | { 18 | "name": "Gary Saunders", 19 | "email": "gary@codenamegary.com" 20 | }, 21 | { 22 | "name": "Ivan Stanojevic", 23 | "email": "ivanstan@gmail.com" 24 | }, 25 | { 26 | "name": "Samuel Williams", 27 | "email": "sam@badcow.co" 28 | } 29 | ], 30 | "require": { 31 | "php": "~7.2", 32 | "ext-json": "*", 33 | "ext-SimpleXML": "*", 34 | "badcow/dns": "^3.4", 35 | "psr/log": "^1.0", 36 | "react/socket": "~1.2", 37 | "react/datagram": "^1.4", 38 | "symfony/console": "^4.3", 39 | "symfony/event-dispatcher": "^4.3", 40 | "symfony/filesystem": "^4.3", 41 | "symfony/property-access": "^4.3", 42 | "vanilla/garden-cli": "^2.2" 43 | }, 44 | "autoload": { 45 | "psr-4": { 46 | "yswery\\DNS\\": "src/" 47 | } 48 | }, 49 | "autoload-dev": { 50 | "psr-4": { 51 | "yswery\\DNS\\Tests\\": "tests/" 52 | } 53 | }, 54 | "scripts": { 55 | "build-server": "box compile -c server.box.json", 56 | "build-console": "box compile -c console.box.json", 57 | "build-installer": "box compile -c installer.box.json" 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /console.box.json: -------------------------------------------------------------------------------- 1 | { 2 | "chmod": "0755", 3 | "main": "bin/PhpDnsConsole.php", 4 | "output": "bin/phpdnscli.phar", 5 | "directories": ["src"], 6 | "finder": [ 7 | { 8 | "name": "*.php", 9 | "exclude": ["test", "tests"], 10 | "in": "vendor" 11 | } 12 | ], "stub": true 13 | } -------------------------------------------------------------------------------- /docs/Basic-Usage.md: -------------------------------------------------------------------------------- 1 | Basic Usage 2 | =========== 3 | 4 | ```php 5 | // JsonResolver created and provided with path to file with json dns records 6 | $jsonResolver = new yswery\DNS\Resolver\JsonResolver(['/path/to/example.com.json', '/path/to/test.com.json']); 7 | 8 | // Create a new instance of Server class 9 | $server = new yswery\DNS\Server($jsonResolver); 10 | 11 | // Start DNS server 12 | $server->start(); 13 | ``` 14 | -------------------------------------------------------------------------------- /docs/Event-Dispatcher.md: -------------------------------------------------------------------------------- 1 | # Event Subscriber 2 | 3 | Server class supports registering event subscribers that can respond to various events emitted during 4 | application life cycle. The Symfony Event Dispatcher component has been implemented to handle the events. 5 | 6 | Following examples show how to implement and register such subscriber. 7 | 8 | ```php 9 | use Symfony\Component\EventDispatcher\EventSubscriberInterface; 10 | 11 | class ExampleEventSubscriber implements EventSubscriberInterface 12 | { 13 | public static function getSubscribedEvents(): array 14 | { 15 | return [ 16 | Event::SERVER_START => 'onServerStart', 17 | Event::MESSAGE => 'onMessage', 18 | ]; 19 | } 20 | 21 | public function onServerStart(ServerStartEvent $event): void 22 | { 23 | // ToDo: implement method 24 | } 25 | 26 | public function onMessage(MessageEvent $event): void 27 | { 28 | // ToDo: implement method 29 | } 30 | } 31 | ``` 32 | List of all possible events can be found in `yswery\DNS\Event\Events` abstract class. 33 | You need to create the EventDispatcher and add your subscriber classes or event methods. 34 | The EventDispatcher is parsed to the Server constructor. 35 | 36 | ```php 37 | $eventDispatcher = new \Symfony\Component\EventDispatcher\EventDispatcher(); 38 | $eventDispatcher->addSubscriber(new ExampleEventSubscriber()); 39 | $server = new Server(new JsonResolver('./record.json'), $eventDispatcher); 40 | ``` 41 | 42 | ## Supported events 43 | 44 | * `Events::SERVER_START` - Server is started and listening for queries. 45 | * `Events::SERVER_START_FAIL` - The server failed to start at all. 46 | * `Events::SERVER_EXCEPTION` - Exception is thrown when processing and responding to query. 47 | * `Events::MESSAGE` - Message is received from client in raw format. 48 | * `Events::QUERY_RECEIVE` - Query is parsed to dns message class. 49 | * `Events::QUERY_RESPONSE` - Message is resolved and sent to client. 50 | 51 | ## Example 52 | ### Adding a kill switch 53 | Say you wanted to be able to stop the server if you send it a particular query. In the example below, 54 | if the server receives a query for `STOP.DNS.`, then it will trigger an `exit()`. 55 | 56 | ```php 57 | $dispatcher->addListener(Events::QUERY_RECEIVE, function (QueryReceiveEvent $event) { 58 | foreach ($event->getMessage()->getQuestions() as $query) { 59 | if ('STOP.DNS.' === $query->getName()) { 60 | exit; 61 | } 62 | } 63 | }); 64 | 65 | $server = new Server($resolver, $dispatcher); 66 | $server->start(); 67 | ``` 68 | 69 | ```BASH 70 | > nslookup STOP.DNS. 127.0.0.1 71 | ``` -------------------------------------------------------------------------------- /docs/Json-Resolver.md: -------------------------------------------------------------------------------- 1 | # Enhanced JSON Resolver 2 | The `EnhancedJsonResolver` treats records differently to its predecessor. Each JSON file is more akin to 3 | a BIND zone in how it structured. 4 | 5 | ## File structure 6 | The object MUST declare the `domain`, `default-ttl` and array of `resource-records` objects. 7 | Each Resource Record object can have the following properties: 8 | * **name** - optional string, if none or an `@` is specified, the name will default to the zone name. 9 | This does not need to be a fully qualified name, as the parent will be automatically appended. 10 | * **ttl** - optional int, if none is specified it will default to the `default-ttl`. 11 | * **type** - string, the RDATA type. This must be specified. 12 | * **address** - string (A and AAAA records only) 13 | * **target** - string (NS, CNAME and PTR records) 14 | * **mname** (string), **rname** (string), **serial** (int), **refresh** (int), **retry** (int),\ 15 | **expire** (int) **minimum** (int) - SOA records 16 | * **preference** (int) and **exchange** (string) - MX records 17 | 18 | ### Example 19 | 20 | ```json 21 | { 22 | "domain": "example.com.", 23 | "default-ttl": 7200, 24 | "resource-records": [ 25 | { 26 | "name": "@", 27 | "ttl": 10800, 28 | "type": "SOA", 29 | "class": "IN", 30 | "mname": "example.com.", 31 | "rname": "postmaster", 32 | "serial": 2, 33 | "refresh": 3600, 34 | "retry": 7200, 35 | "expire": 10800, 36 | "minimum": 3600 37 | }, { 38 | "type": "A", 39 | "address": "12.34.56.78" 40 | },{ 41 | "type": "A", 42 | "address": "90.12.34.56" 43 | }, { 44 | "type": "AAAA", 45 | "address": "2001:acad:ad::32" 46 | }, { 47 | "name": "www", 48 | "type": "cname", 49 | "target": "@" 50 | }, { 51 | "name": "@", 52 | "type": "MX", 53 | "preference": 15, 54 | "exchange": "mail" 55 | }, { 56 | "name": "*.subdomain", 57 | "type": "A", 58 | "address": "192.168.1.42" 59 | } 60 | ] 61 | } 62 | ``` 63 | 64 | ## Wildcard Domains 65 | Wildcard domains are supported, in the example above, a query for `foobar.subdomain.example.com.` will 66 | return `192.168.1.42`. 67 | 68 | ## Processing Records 69 | ```php 70 | $files = [__DIR__.'/example.com.json', __DIR__.'/test.com.json']; 71 | $resolver = new yswery\DNS\Resolver\EnhancedJsonResolver($files); 72 | $resolver->getAnswer(/*some query*/); 73 | ``` 74 | 75 | ## Backward Compatibility 76 | The JSON Resolver can handle the older format JSON zone records (example below). These are loaded 77 | the same way as the new file format. 78 | 79 | ```json 80 | { 81 | "test.com": { 82 | "A": "111.111.111.111", 83 | "MX": [ 84 | { 85 | "exchange": "mail-gw1.test.com", 86 | "preference": 10 87 | }, 88 | { 89 | "exchange": "mail-gw2.test.com", 90 | "preference": 20 91 | } 92 | ], 93 | "NS": [ 94 | "ns1.test.com", 95 | "ns2.test.com" 96 | ], 97 | "TXT": "Some text.", 98 | "AAAA": "DEAD:01::BEEF", 99 | "CNAME": "www2.test.com", 100 | "SOA": [ 101 | { 102 | "mname": "ns1.test.com", 103 | "rname": "admin.test.com", 104 | "serial": "2014111100", 105 | "retry": "7200", 106 | "refresh": "1800", 107 | "expire": "8600", 108 | "minimum": "300" 109 | } 110 | ] 111 | }, 112 | "test2.com": { 113 | "A": [ 114 | "111.111.111.111", 115 | "112.112.112.112" 116 | ], 117 | "MX": [ 118 | { 119 | "preference": 20, 120 | "exchange": "mail-gw1.test2.com." 121 | }, 122 | { 123 | "preference": 30, 124 | "exchange": "mail-gw2.test2.com." 125 | } 126 | ] 127 | } 128 | } 129 | 130 | ``` -------------------------------------------------------------------------------- /docs/XML-Resolver.md: -------------------------------------------------------------------------------- 1 | # XML Resolver 2 | The `XMLResolver` provides the capability to store DNS records as XML files. 3 | 4 | The XSD file is in the root of the project directory. 5 | 6 | ## File structure 7 | The object MUST declare the ``, `` and `` tags. 8 | Each `` within `` can have the following properties: 9 | * **name** - optional string, if none or an `@` is specified, the name will default to the zone name. 10 | This does not need to be a fully qualified name, as the parent will be automatically appended. 11 | * **ttl** - optional int, if none is specified it will default to the ``. 12 | * **type** - required string, the RDATA type. This MUST be specified. 13 | * **rdata** - required 14 | 15 | ### Example 16 | 17 | ```xml 18 | 19 | 23 | example.com. 24 | 7200 25 | 26 | 27 | 28 | @ 29 | 10800 30 | SOA 31 | IN 32 | 33 | example.com. 34 | postmaster 35 | 2 36 | 3600 37 | 7200 38 | 10800 39 | 3600 40 | 41 | 42 | 43 | 44 | A 45 | 46 |
12.34.56.78
47 |
48 |
49 | 50 | 51 | A 52 | 53 |
90.12.34.56
54 |
55 |
56 | 57 | 58 | AAAA 59 | 60 |
2001:acad:ad::32
61 |
62 |
63 | 64 | 65 | www 66 | cname 67 | 68 | @ 69 | 70 | 71 | 72 | 73 | @ 74 | MX 75 | 76 | 15 77 | mail 78 | 79 | 80 | 81 | 82 | *.subdomain 83 | A 84 | 85 |
192.168.1.42
86 |
87 |
88 |
89 |
90 | ``` 91 | 92 | ## Wildcard Domains 93 | Wildcard domains are supported, in the example above, a query for `foobar.subdomain.example.com.` will 94 | return `192.168.1.42`. 95 | 96 | ## Processing Records 97 | ```php 98 | $files = [__DIR__.'/example.com.xml', __DIR__.'/test.com.xml']; 99 | $resolver = new yswery\DNS\Resolver\XmlResolver($files); 100 | $resolver->getAnswer(/*some query*/); 101 | ``` -------------------------------------------------------------------------------- /example/example.com.db: -------------------------------------------------------------------------------- 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 | -------------------------------------------------------------------------------- /example/example.com.json: -------------------------------------------------------------------------------- 1 | { 2 | "domain": "example.com.", 3 | "default-ttl": 7200, 4 | "resource-records": [ 5 | { 6 | "name": "@", 7 | "ttl": 10800, 8 | "type": "SOA", 9 | "class": "IN", 10 | "mname": "example.com.", 11 | "rname": "postmaster", 12 | "serial": 2, 13 | "refresh": 3600, 14 | "retry": 7200, 15 | "expire": 10800, 16 | "minimum": 3600 17 | }, { 18 | "type": "A", 19 | "address": "12.34.56.78" 20 | },{ 21 | "type": "A", 22 | "address": "90.12.34.56" 23 | }, { 24 | "type": "AAAA", 25 | "address": "2001:acad:ad::32" 26 | }, { 27 | "name": "www", 28 | "type": "cname", 29 | "target": "@" 30 | }, { 31 | "name": "@", 32 | "type": "MX", 33 | "preference": 15, 34 | "exchange": "mail" 35 | }, { 36 | "name": "*.subdomain", 37 | "type": "A", 38 | "address": "192.168.1.42" 39 | } 40 | ] 41 | } -------------------------------------------------------------------------------- /example/example.com.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | example.com. 7 | 7200 8 | 9 | 10 | 11 | @ 12 | 10800 13 | SOA 14 | IN 15 | 16 | example.com. 17 | postmaster 18 | 2 19 | 3600 20 | 7200 21 | 10800 22 | 3600 23 | 24 | 25 | 26 | 27 | A 28 | 29 |
12.34.56.78
30 |
31 |
32 | 33 | 34 | A 35 | 36 |
90.12.34.56
37 |
38 |
39 | 40 | 41 | AAAA 42 | 43 |
2001:acad:ad::32
44 |
45 |
46 | 47 | 48 | www 49 | cname 50 | 51 | @ 52 | 53 | 54 | 55 | 56 | @ 57 | MX 58 | 59 | 15 60 | mail 61 | 62 | 63 | 64 | 65 | *.subdomain 66 | A 67 | 68 |
192.168.1.42
69 |
70 |
71 |
72 |
73 | -------------------------------------------------------------------------------- /example/example.com.yml: -------------------------------------------------------------------------------- 1 | domain: example.com. 2 | default-ttl: 7200 3 | resource-records: 4 | - name: '@' 5 | ttl: 10800 6 | type: SOA 7 | class: IN 8 | mname: example.com. 9 | rname: postmaster 10 | serial: 2 11 | refresh: 3600 12 | retry: 7200 13 | expire: 10800 14 | minimum: 3600 15 | 16 | - type: A 17 | address: 12.34.56.78 18 | 19 | - type: A 20 | address: 90.12.34.56 21 | 22 | - type: AAAA 23 | address: 2001:acad:ad::32 24 | 25 | - name: www 26 | type: cname 27 | target: '@' 28 | 29 | - name: '@' 30 | type: MX 31 | preference: 15 32 | exchange: mail 33 | 34 | - name: '*.subdomain' 35 | type: A 36 | address: 192.168.1.42 -------------------------------------------------------------------------------- /example/example.php: -------------------------------------------------------------------------------- 1 | addSubscriber(new \yswery\DNS\Event\Subscriber\EchoLogger()); 17 | $eventDispatcher->addSubscriber(new \yswery\DNS\Event\Subscriber\ServerTerminator()); 18 | 19 | // Create a new instance of Server class 20 | $server = new yswery\DNS\Server($stackableResolver, $eventDispatcher); 21 | 22 | // Start DNS server 23 | $server->start(); 24 | -------------------------------------------------------------------------------- /example/record.json: -------------------------------------------------------------------------------- 1 | { 2 | "test.com": { 3 | "A": "111.111.111.111", 4 | "MX": [ 5 | { 6 | "exchange": "mail-gw1.test.com", 7 | "preference": 10 8 | }, 9 | { 10 | "exchange": "mail-gw2.test.com", 11 | "preference": 20 12 | } 13 | ], 14 | "NS": [ 15 | "ns1.test.com", 16 | "ns2.test.com" 17 | ], 18 | "TXT": "Some text.", 19 | "AAAA": "DEAD:01::BEEF", 20 | "CNAME": "www2.test.com", 21 | "SOA": [ 22 | { 23 | "mname": "ns1.test.com", 24 | "rname": "admin.test.com", 25 | "serial": "2014111100", 26 | "retry": "7200", 27 | "refresh": "1800", 28 | "expire": "8600", 29 | "minimum": "300" 30 | } 31 | ] 32 | }, 33 | "test2.com": { 34 | "A": [ 35 | "111.111.111.111", 36 | "112.112.112.112" 37 | ], 38 | "MX": [ 39 | { 40 | "preference": 20, 41 | "exchange": "mail-gw1.test2.com." 42 | }, 43 | { 44 | "preference": 30, 45 | "exchange": "mail-gw2.test2.com." 46 | } 47 | ] 48 | }, 49 | "112.111.112.111.in-addr.arpa.": { 50 | "PTR": { 51 | "target": "test2.com" 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /installer.box.json: -------------------------------------------------------------------------------- 1 | { 2 | "chmod": "0755", 3 | "main": "bin/PhpDnsInstaller.php", 4 | "output": "bin/phpdnsinstaller.phar", 5 | "finder": [ 6 | { 7 | "name": "*.php", 8 | "exclude": ["test", "tests"], 9 | "in": "vendor" 10 | } 11 | ], 12 | "stub": true 13 | } -------------------------------------------------------------------------------- /php-dns-server.xsd: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /phpunit: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | ./vendor/bin/phpunit -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 15 | 16 | 17 | ./tests/ 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | ./src/ 26 | 27 | 28 | -------------------------------------------------------------------------------- /server.box.json: -------------------------------------------------------------------------------- 1 | { 2 | "chmod": "0755", 3 | "main": "bin/PhpDnsServer.php", 4 | "output": "bin/phpdnsserver.phar", 5 | "directories": ["src"], 6 | "finder": [ 7 | { 8 | "name": "*.php", 9 | "exclude": ["test", "tests"], 10 | "in": "vendor" 11 | } 12 | ], "stub": true 13 | } -------------------------------------------------------------------------------- /src/ClassEnum.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 | */ 11 | 12 | namespace yswery\DNS; 13 | 14 | class ClassEnum 15 | { 16 | public const INTERNET = 1; 17 | public const CSNET = 2; 18 | public const CHAOS = 3; 19 | public const HESIOD = 4; 20 | 21 | /** 22 | * @var array 23 | */ 24 | public static $classes = [ 25 | self::INTERNET => 'IN', 26 | self::CSNET => 'CS', 27 | self::CHAOS => 'CHAOS', 28 | self::HESIOD => 'HS', 29 | ]; 30 | 31 | /** 32 | * Determine if a class is valid. 33 | * 34 | * @param string $class 35 | * 36 | * @return bool 37 | */ 38 | public static function isValid($class): bool 39 | { 40 | return array_key_exists($class, self::$classes); 41 | } 42 | 43 | /** 44 | * @param int $class 45 | * 46 | * @return mixed 47 | * 48 | * @throws \InvalidArgumentException 49 | */ 50 | public static function getName(int $class): string 51 | { 52 | if (!static::isValid($class)) { 53 | throw new \InvalidArgumentException(sprintf('No class matching integer "%s"', $class)); 54 | } 55 | 56 | return self::$classes[$class]; 57 | } 58 | 59 | /** 60 | * @param string $name 61 | * 62 | * @return int 63 | */ 64 | public static function getClassFromName(string $name): int 65 | { 66 | $class = array_search(strtoupper($name), self::$classes, true); 67 | 68 | if (false === $class || !is_int($class)) { 69 | throw new \InvalidArgumentException(sprintf('Class: "%s" is not defined.', $name)); 70 | } 71 | 72 | return $class; 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/Config/FileConfig.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 | */ 11 | 12 | namespace yswery\DNS\Config; 13 | 14 | use yswery\DNS\Exception\ConfigFileNotFoundException; 15 | 16 | class FileConfig 17 | { 18 | /** 19 | * @var string 20 | */ 21 | protected $configFile; 22 | 23 | /** 24 | * @var object 25 | */ 26 | protected $config; 27 | 28 | public function __construct(string $configFile) 29 | { 30 | $this->configFile = $configFile; 31 | } 32 | 33 | /** 34 | * @throws ConfigFileNotFoundException 35 | */ 36 | public function load() 37 | { 38 | // make sure the file exists before loading the config 39 | if (file_exists($this->configFile)) { 40 | $data = file_get_contents($this->configFile); 41 | $this->config = json_decode($data); 42 | } else { 43 | throw new ConfigFileNotFoundException('Config file not found.'); 44 | } 45 | } 46 | 47 | public function save() 48 | { 49 | file_put_contents($this->configFile, json_encode($this->config)); 50 | } 51 | 52 | public function get($key, $default = null) 53 | { 54 | if (isset($this->config->{$key})) { 55 | return $this->config->{$key}; 56 | } 57 | 58 | return $default; 59 | } 60 | 61 | public function set(array $data) 62 | { 63 | $configObject = new RecursiveArrayObject($this->config); 64 | $configArray = $configObject->getArrayCopy(); 65 | 66 | //$origional = json_decode(json_encode($this->config), true); 67 | $new = array_merge($configArray, $data); 68 | 69 | $this->config = json_decode(json_encode($new), false); 70 | } 71 | 72 | public function has($key) 73 | { 74 | return isset($this->config->{$key}); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/Config/RecursiveArrayObject.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 | */ 11 | 12 | namespace yswery\DNS\Config; 13 | 14 | use ArrayObject; 15 | 16 | class RecursiveArrayObject extends ArrayObject 17 | { 18 | public function getArrayCopy() 19 | { 20 | $resultArray = parent::getArrayCopy(); 21 | foreach ($resultArray as $key => $val) { 22 | if (!is_object($val)) { 23 | continue; 24 | } 25 | $o = new RecursiveArrayObject($val); 26 | $resultArray[$key] = $o->getArrayCopy(); 27 | } 28 | 29 | return $resultArray; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Console/CommandServer.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 | */ 11 | 12 | namespace yswery\DNS\Console; 13 | 14 | use Symfony\Component\Console\Application; 15 | use yswery\DNS\Console\Commands\VersionCommand; 16 | use yswery\DNS\Server; 17 | 18 | class CommandServer extends Application 19 | { 20 | public function __construct() 21 | { 22 | parent::__construct('PhpDnsServer', Server::VERSION); 23 | 24 | $this->registerCommands(); 25 | } 26 | 27 | protected function registerCommands() 28 | { 29 | $this->add(new VersionCommand()); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Console/Commands/VersionCommand.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 | */ 11 | 12 | namespace yswery\DNS\Console\Commands; 13 | 14 | use Symfony\Component\Console\Command\Command; 15 | use Symfony\Component\Console\Input\InputInterface; 16 | use Symfony\Component\Console\Output\OutputInterface; 17 | use yswery\DNS\Server; 18 | 19 | class VersionCommand extends Command 20 | { 21 | protected static $defaultName = 'version'; 22 | 23 | protected function configure() 24 | { 25 | $this->setDescription('Shows the current PhpDnsServer version') 26 | ->setHelp('Shows the current PhpDnsServer version'); 27 | } 28 | 29 | protected function execute(InputInterface $input, OutputInterface $output) 30 | { 31 | $output->writeln('PowerDnsServer version '.Server::VERSION); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Decoder.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 | */ 11 | 12 | namespace yswery\DNS; 13 | 14 | class Decoder 15 | { 16 | /** 17 | * @param string $message 18 | * 19 | * @return Message 20 | */ 21 | public static function decodeMessage(string $message): Message 22 | { 23 | $offset = 0; 24 | $header = self::decodeHeader($message, $offset); 25 | $messageObject = new Message($header); 26 | $messageObject->setQuestions(self::decodeResourceRecords($message, $header->getQuestionCount(), $offset, true)); 27 | $messageObject->setAnswers(self::decodeResourceRecords($message, $header->getAnswerCount(), $offset)); 28 | $messageObject->setAuthoritatives(self::decodeResourceRecords($message, $header->getNameServerCount(), $offset)); 29 | $messageObject->setAdditionals(self::decodeResourceRecords($message, $header->getAdditionalRecordsCount(), $offset)); 30 | 31 | return $messageObject; 32 | } 33 | 34 | /** 35 | * @param string $string 36 | * @param int $offset 37 | * 38 | * @return string 39 | */ 40 | public static function decodeDomainName(string $string, int &$offset = 0): string 41 | { 42 | $len = ord($string[$offset]); 43 | ++$offset; 44 | 45 | if (0 === $len) { 46 | return '.'; 47 | } 48 | 49 | $domainName = ''; 50 | while (0 !== $len) { 51 | $domainName .= substr($string, $offset, $len).'.'; 52 | $offset += $len; 53 | $len = ord($string[$offset]); 54 | ++$offset; 55 | } 56 | 57 | return $domainName; 58 | } 59 | 60 | /** 61 | * @param string $pkt 62 | * @param int $offset 63 | * @param int $count The number of resource records to decode 64 | * @param bool $isQuestion Is the resource record from the question section 65 | * 66 | * @return ResourceRecord[] 67 | */ 68 | public static function decodeResourceRecords(string $pkt, int $count = 1, int &$offset = 0, bool $isQuestion = false): array 69 | { 70 | $resourceRecords = []; 71 | 72 | for ($i = 0; $i < $count; ++$i) { 73 | ($rr = new ResourceRecord()) 74 | ->setQuestion($isQuestion) 75 | ->setName(self::decodeDomainName($pkt, $offset)); 76 | 77 | if ($rr->isQuestion()) { 78 | $values = unpack('ntype/nclass', substr($pkt, $offset, 4)); 79 | $rr->setType($values['type'])->setClass($values['class']); 80 | $offset += 4; 81 | } else { 82 | $values = unpack('ntype/nclass/Nttl/ndlength', substr($pkt, $offset, 10)); 83 | $rr->setType($values['type'])->setClass($values['class'])->setTtl($values['ttl']); 84 | $offset += 10; 85 | 86 | //Ignore unsupported types. 87 | try { 88 | $rr->setRdata(RdataDecoder::decodeRdata($rr->getType(), substr($pkt, $offset, $values['dlength']))); 89 | } catch (UnsupportedTypeException $e) { 90 | $offset += $values['dlength']; 91 | continue; 92 | } 93 | $offset += $values['dlength']; 94 | } 95 | 96 | $resourceRecords[] = $rr; 97 | } 98 | 99 | return $resourceRecords; 100 | } 101 | 102 | /** 103 | * @param string $pkt 104 | * @param int $offset 105 | * 106 | * @return Header 107 | */ 108 | public static function decodeHeader(string $pkt, int &$offset = 0): Header 109 | { 110 | $data = unpack('nid/nflags/nqdcount/nancount/nnscount/narcount', $pkt); 111 | $flags = self::decodeFlags($data['flags']); 112 | $offset += 12; 113 | 114 | return (new Header()) 115 | ->setId($data['id']) 116 | ->setResponse($flags['qr']) 117 | ->setOpcode($flags['opcode']) 118 | ->setAuthoritative($flags['aa']) 119 | ->setTruncated($flags['tc']) 120 | ->setRecursionDesired($flags['rd']) 121 | ->setRecursionAvailable($flags['ra']) 122 | ->setZ($flags['z']) 123 | ->setRcode($flags['rcode']) 124 | ->setQuestionCount($data['qdcount']) 125 | ->setAnswerCount($data['ancount']) 126 | ->setNameServerCount($data['nscount']) 127 | ->setAdditionalRecordsCount($data['arcount']); 128 | } 129 | 130 | /** 131 | * @param string $flags 132 | * 133 | * @return array 134 | */ 135 | private static function decodeFlags($flags): array 136 | { 137 | return [ 138 | 'qr' => $flags >> 15 & 0x1, 139 | 'opcode' => $flags >> 11 & 0xf, 140 | 'aa' => $flags >> 10 & 0x1, 141 | 'tc' => $flags >> 9 & 0x1, 142 | 'rd' => $flags >> 8 & 0x1, 143 | 'ra' => $flags >> 7 & 0x1, 144 | 'z' => $flags >> 4 & 0x7, 145 | 'rcode' => $flags & 0xf, 146 | ]; 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /src/Encoder.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 | */ 11 | 12 | namespace yswery\DNS; 13 | 14 | class Encoder 15 | { 16 | /** 17 | * @param Message $message 18 | * 19 | * @return string 20 | * 21 | * @throws UnsupportedTypeException 22 | */ 23 | public static function encodeMessage(Message $message): string 24 | { 25 | return 26 | self::encodeHeader($message->getHeader()). 27 | self::encodeResourceRecords($message->getQuestions()). 28 | self::encodeResourceRecords($message->getAnswers()). 29 | self::encodeResourceRecords($message->getAuthoritatives()). 30 | self::encodeResourceRecords($message->getAdditionals()); 31 | } 32 | 33 | /** 34 | * Encode a domain name as a sequence of labels. 35 | * 36 | * @param $domain 37 | * 38 | * @return string 39 | */ 40 | public static function encodeDomainName($domain): string 41 | { 42 | if ('.' === $domain) { 43 | return chr(0); 44 | } 45 | 46 | $domain = rtrim($domain, '.').'.'; 47 | $res = ''; 48 | 49 | foreach (explode('.', $domain) as $label) { 50 | $res .= chr(strlen($label)).$label; 51 | } 52 | 53 | return $res; 54 | } 55 | 56 | /** 57 | * @param ResourceRecord[] $resourceRecords 58 | * 59 | * @return string 60 | * 61 | * @throws UnsupportedTypeException 62 | */ 63 | public static function encodeResourceRecords(array $resourceRecords): string 64 | { 65 | $records = array_map('self::encodeResourceRecord', $resourceRecords); 66 | 67 | return implode('', $records); 68 | } 69 | 70 | /** 71 | * @param ResourceRecord $rr 72 | * 73 | * @return string 74 | * 75 | * @throws UnsupportedTypeException 76 | */ 77 | public static function encodeResourceRecord(ResourceRecord $rr): string 78 | { 79 | $encoded = self::encodeDomainName($rr->getName()); 80 | if ($rr->isQuestion()) { 81 | return $encoded.pack('nn', $rr->getType(), $rr->getClass()); 82 | } 83 | 84 | $data = RdataEncoder::encodeRdata($rr->getType(), $rr->getRdata()); 85 | $encoded .= pack('nnNn', $rr->getType(), $rr->getClass(), $rr->getTtl(), strlen($data)); 86 | 87 | return $encoded.$data; 88 | } 89 | 90 | /** 91 | * @param Header $header 92 | * 93 | * @return string 94 | */ 95 | public static function encodeHeader(Header $header): string 96 | { 97 | return pack( 98 | 'nnnnnn', 99 | $header->getId(), 100 | self::encodeFlags($header), 101 | $header->getQuestionCount(), 102 | $header->getAnswerCount(), 103 | $header->getNameServerCount(), 104 | $header->getAdditionalRecordsCount() 105 | ); 106 | } 107 | 108 | /** 109 | * Encode the bit field of the Header between "ID" and "QDCOUNT". 110 | * 111 | * @param Header $header 112 | * 113 | * @return int 114 | */ 115 | private static function encodeFlags(Header $header): int 116 | { 117 | return 0x0 | 118 | ($header->isResponse() & 0x1) << 15 | 119 | ($header->getOpcode() & 0xf) << 11 | 120 | ($header->isAuthoritative() & 0x1) << 10 | 121 | ($header->isTruncated() & 0x1) << 9 | 122 | ($header->isRecursionDesired() & 0x1) << 8 | 123 | ($header->isRecursionAvailable() & 0x1) << 7 | 124 | ($header->getZ() & 0x7) << 4 | 125 | ($header->getRcode() & 0xf); 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /src/Event/Events.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 | */ 11 | 12 | namespace yswery\DNS\Event; 13 | 14 | class Events 15 | { 16 | /** 17 | * Message is received from client in raw format. 18 | * 19 | * @Event("yswery\DNS\Event\MessageEvent") 20 | */ 21 | public const MESSAGE = 'dns.message'; 22 | 23 | /** 24 | * Query is parsed to DNS Message class. 25 | * 26 | * @Event("yswery\DNS\Event\QueryReceiveEvent") 27 | */ 28 | public const QUERY_RECEIVE = 'dns.query_receive'; 29 | 30 | /** 31 | * Query is resolved and sent to the client. 32 | * 33 | * @Event("yswery\DNS\Event\QueryResponseEvent") 34 | */ 35 | public const QUERY_RESPONSE = 'dns.query_response'; 36 | 37 | /** 38 | * Exception is thrown when processing and responding to query. 39 | * 40 | * @Event("yswery\DNS\Event\ServerExceptionEvent") 41 | */ 42 | public const SERVER_EXCEPTION = 'dns.server_exception'; 43 | 44 | /** 45 | * Server is started and listening for queries. 46 | * 47 | * @Event("yswery\DNS\Event\ServerStartEvent") 48 | */ 49 | public const SERVER_START = 'dns.server_start'; 50 | 51 | /** 52 | * Exception is thrown when there is any error starting the server. 53 | * 54 | * @Event("yswery\DNS\Event\ServerExceptionEvent") 55 | */ 56 | public const SERVER_START_FAIL = 'dns.server_start_fail'; 57 | } 58 | -------------------------------------------------------------------------------- /src/Event/MessageEvent.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 | */ 11 | 12 | namespace yswery\DNS\Event; 13 | 14 | use React\Datagram\Socket; 15 | use React\Datagram\SocketInterface; 16 | 17 | class MessageEvent extends ServerStartEvent 18 | { 19 | /** 20 | * @var string 21 | */ 22 | private $remote; 23 | 24 | /** 25 | * @var string 26 | */ 27 | private $message; 28 | 29 | /** 30 | * MessageEvent constructor. 31 | * 32 | * @param SocketInterface $socket 33 | * @param string $remote 34 | * @param string $message 35 | */ 36 | public function __construct(SocketInterface $socket, string $remote, string $message) 37 | { 38 | parent::__construct($socket); 39 | $this->remote = $remote; 40 | $this->message = $message; 41 | } 42 | 43 | /** 44 | * @return string 45 | */ 46 | public function getRemote(): string 47 | { 48 | return $this->remote; 49 | } 50 | 51 | /** 52 | * @return string 53 | */ 54 | public function getMessage(): string 55 | { 56 | return $this->message; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/Event/QueryReceiveEvent.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 | */ 11 | 12 | namespace yswery\DNS\Event; 13 | 14 | use Symfony\Component\EventDispatcher\Event; 15 | use yswery\DNS\Message; 16 | 17 | class QueryReceiveEvent extends Event 18 | { 19 | /** 20 | * @var Message 21 | */ 22 | private $message; 23 | 24 | /** 25 | * QueryReceiveEvent constructor. 26 | * 27 | * @param Message $message 28 | */ 29 | public function __construct(Message $message) 30 | { 31 | $this->message = $message; 32 | } 33 | 34 | /** 35 | * @return Message 36 | */ 37 | public function getMessage(): Message 38 | { 39 | return $this->message; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Event/QueryResponseEvent.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 | */ 11 | 12 | namespace yswery\DNS\Event; 13 | 14 | class QueryResponseEvent extends QueryReceiveEvent 15 | { 16 | } 17 | -------------------------------------------------------------------------------- /src/Event/ServerExceptionEvent.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 | */ 11 | 12 | namespace yswery\DNS\Event; 13 | 14 | use Symfony\Component\EventDispatcher\Event; 15 | 16 | class ServerExceptionEvent extends Event 17 | { 18 | /** 19 | * @var \Exception 20 | */ 21 | private $exception; 22 | 23 | /** 24 | * ExceptionEvent constructor. 25 | * 26 | * @param \Exception $exception 27 | */ 28 | public function __construct(\Exception $exception) 29 | { 30 | $this->exception = $exception; 31 | } 32 | 33 | /** 34 | * @return \Exception 35 | */ 36 | public function getException(): \Exception 37 | { 38 | return $this->exception; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Event/ServerStartEvent.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 | */ 11 | 12 | namespace yswery\DNS\Event; 13 | 14 | use React\Datagram\SocketInterface; 15 | use Symfony\Component\EventDispatcher\Event; 16 | 17 | class ServerStartEvent extends Event 18 | { 19 | /** 20 | * @var SocketInterface 21 | */ 22 | private $socket; 23 | 24 | /** 25 | * ServerStartEvent constructor. 26 | * 27 | * @param SocketInterface $socket 28 | */ 29 | public function __construct(SocketInterface $socket) 30 | { 31 | $this->socket = $socket; 32 | } 33 | 34 | /** 35 | * @return SocketInterface 36 | */ 37 | public function getSocket(): SocketInterface 38 | { 39 | return $this->socket; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Event/Subscriber/EchoLogger.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 | */ 11 | 12 | namespace yswery\DNS\Event\Subscriber; 13 | 14 | use Psr\Log\LoggerInterface; 15 | use Psr\Log\LoggerTrait; 16 | 17 | class EchoLogger extends LoggerSubscriber implements LoggerInterface 18 | { 19 | use LoggerTrait; 20 | 21 | public function __construct() 22 | { 23 | $this->setLogger($this); 24 | } 25 | 26 | public function log($level, $message, array $context = []) 27 | { 28 | echo sprintf('[%s] %s: %s'.PHP_EOL, date('c'), $level, $message); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Event/Subscriber/LoggerSubscriber.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 | */ 11 | 12 | namespace yswery\DNS\Event\Subscriber; 13 | 14 | use Psr\Log\LoggerAwareInterface; 15 | use Psr\Log\LoggerAwareTrait; 16 | use Psr\Log\LogLevel; 17 | use Symfony\Component\EventDispatcher\EventSubscriberInterface; 18 | use yswery\DNS\Event\Events; 19 | use yswery\DNS\Event\QueryReceiveEvent; 20 | use yswery\DNS\Event\QueryResponseEvent; 21 | use yswery\DNS\Event\ServerExceptionEvent; 22 | use yswery\DNS\Event\ServerStartEvent; 23 | 24 | class LoggerSubscriber implements EventSubscriberInterface, LoggerAwareInterface 25 | { 26 | use LoggerAwareTrait; 27 | 28 | public static function getSubscribedEvents(): array 29 | { 30 | return [ 31 | Events::SERVER_START => 'onServerStart', 32 | Events::SERVER_START_FAIL => 'onException', 33 | Events::SERVER_EXCEPTION => 'onException', 34 | Events::QUERY_RECEIVE => 'onQueryReceive', 35 | Events::QUERY_RESPONSE => 'onQueryResponse', 36 | ]; 37 | } 38 | 39 | public function onServerStart(ServerStartEvent $event): void 40 | { 41 | $this->logger->log(LogLevel::INFO, 'Server started.'); 42 | $this->logger->log(LogLevel::INFO, sprintf('Listening on %s', $event->getSocket()->getLocalAddress())); 43 | } 44 | 45 | public function onException(ServerExceptionEvent $event): void 46 | { 47 | $this->logger->log(LogLevel::ERROR, $event->getException()->getMessage()); 48 | } 49 | 50 | public function onQueryReceive(QueryReceiveEvent $event): void 51 | { 52 | foreach ($event->getMessage()->getQuestions() as $question) { 53 | $this->logger->log(LogLevel::INFO, 'Query: '.$question); 54 | } 55 | } 56 | 57 | public function onQueryResponse(QueryResponseEvent $event): void 58 | { 59 | foreach ($event->getMessage()->getAnswers() as $answer) { 60 | $this->logger->log(LogLevel::INFO, 'Answer: '.$answer); 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/Event/Subscriber/ServerTerminator.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 | */ 11 | 12 | namespace yswery\DNS\Event\Subscriber; 13 | 14 | use Symfony\Component\EventDispatcher\EventSubscriberInterface; 15 | use yswery\DNS\Event\Events; 16 | use yswery\DNS\Event\ServerExceptionEvent; 17 | 18 | class ServerTerminator implements EventSubscriberInterface 19 | { 20 | public static function getSubscribedEvents(): array 21 | { 22 | return [ 23 | Events::SERVER_START_FAIL => 'onException', 24 | ]; 25 | } 26 | 27 | public function onException(ServerExceptionEvent $event): void 28 | { 29 | exit(1); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Exception/ConfigFileNotFoundException.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 | */ 11 | 12 | namespace yswery\DNS\Exception; 13 | 14 | use Exception; 15 | 16 | class ConfigFileNotFoundException extends Exception 17 | { 18 | } 19 | -------------------------------------------------------------------------------- /src/Exception/InvalidZoneFileException.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 | */ 11 | 12 | namespace yswery\DNS\Exception; 13 | 14 | use RuntimeException; 15 | 16 | class InvalidZoneFileException extends RuntimeException 17 | { 18 | } 19 | -------------------------------------------------------------------------------- /src/Exception/ZoneFileNotFoundException.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 | */ 11 | 12 | namespace yswery\DNS\Exception; 13 | 14 | use RuntimeException; 15 | 16 | class ZoneFileNotFoundException extends RuntimeException 17 | { 18 | } 19 | -------------------------------------------------------------------------------- /src/Filesystem/FilesystemManager.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 | */ 11 | 12 | namespace yswery\DNS\Filesystem; 13 | 14 | use Symfony\Component\Filesystem\Exception\IOExceptionInterface; 15 | use Symfony\Component\Filesystem\Filesystem; 16 | use yswery\DNS\Resolver\JsonFileSystemResolver; 17 | 18 | class FilesystemManager 19 | { 20 | /** 21 | * @var Filesystem 22 | */ 23 | protected $filesystem; 24 | 25 | /** 26 | * @var string 27 | */ 28 | protected $basePath; 29 | 30 | /** 31 | * @var string 32 | */ 33 | protected $zonePath; 34 | 35 | public function __construct($basePath = null, $zonePath = null) 36 | { 37 | if ($basePath) { 38 | $this->setBasePath($basePath); 39 | } 40 | 41 | if ($zonePath) { 42 | $this->setZonePath($zonePath); 43 | } 44 | 45 | $this->registerFilesystem(); 46 | } 47 | 48 | protected function registerFilesystem() 49 | { 50 | $this->filesystem = new Filesystem(); 51 | 52 | // make sure our directories exist 53 | if (!$this->filesystem->exists($this->zonePath())) { 54 | try { 55 | $this->filesystem->mkdir($this->zonePath(), 0700); 56 | } catch (IOExceptionInterface $e) { 57 | // todo: implement logging functions 58 | } 59 | } 60 | } 61 | 62 | public function setBasePath($basePath) 63 | { 64 | $this->basePath = rtrim($basePath, '\/'); 65 | 66 | return $this; 67 | } 68 | 69 | public function basePath() 70 | { 71 | return $this->basePath; 72 | } 73 | 74 | public function setZonePath($zonePath) 75 | { 76 | $this->zonePath = rtrim($zonePath, '\/'); 77 | 78 | return $this; 79 | } 80 | 81 | public function zonePath() 82 | { 83 | return $this->zonePath ?: $this->basePath.DIRECTORY_SEPARATOR.'zones'; 84 | } 85 | 86 | /** 87 | * @param string $zone 88 | * 89 | * @return JsonFileSystemResolver 90 | * 91 | * @throws \yswery\DNS\UnsupportedTypeException 92 | */ 93 | public function getZone(string $zone) 94 | { 95 | $zoneFile = $this->basePath().DIRECTORY_SEPARATOR.$zone.'.json'; 96 | 97 | return new JsonFileSystemResolver($zoneFile); 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/Header.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 | */ 11 | 12 | namespace yswery\DNS; 13 | 14 | class Header 15 | { 16 | const OPCODE_STANDARD_QUERY = 0; 17 | 18 | const OPCODE_INVERSE_QUERY = 1; 19 | 20 | const OPCODE_STATUS_REQUEST = 2; 21 | 22 | const RCODE_NO_ERROR = 0; 23 | 24 | const RCODE_FORMAT_ERROR = 1; 25 | 26 | const RCODE_SERVER_FAILURE = 2; 27 | 28 | const RCODE_NAME_ERROR = 3; 29 | 30 | const RCODE_NOT_IMPLEMENTED = 4; 31 | 32 | const RCODE_REFUSED = 5; 33 | 34 | /** 35 | * ID. 36 | * 37 | * @var int 38 | */ 39 | private $id; 40 | 41 | /** 42 | * QR. 43 | * 44 | * @var bool 45 | */ 46 | private $response; 47 | 48 | /** 49 | * OPCODE. 50 | * 51 | * @var int 52 | */ 53 | private $opcode; 54 | 55 | /** 56 | * AA. 57 | * 58 | * @var bool 59 | */ 60 | private $authoritative; 61 | 62 | /** 63 | * TC. 64 | * 65 | * @var bool 66 | */ 67 | private $truncated; 68 | 69 | /** 70 | * RD. 71 | * 72 | * @var bool 73 | */ 74 | private $recursionDesired; 75 | 76 | /** 77 | * RA. 78 | * 79 | * @var bool 80 | */ 81 | private $recursionAvailable; 82 | 83 | /** 84 | * A. 85 | * 86 | * @var int 87 | */ 88 | private $z = 0; 89 | 90 | /** 91 | * RCODE. 92 | * 93 | * @var int 94 | */ 95 | private $rcode; 96 | 97 | /** 98 | * QDCOUNT. 99 | * 100 | * @var int 101 | */ 102 | private $questionCount; 103 | 104 | /** 105 | * ANCOUNT. 106 | * 107 | * @var int 108 | */ 109 | private $answerCount; 110 | 111 | /** 112 | * NSCOUNT. 113 | * 114 | * @var int 115 | */ 116 | private $nameServerCount; 117 | 118 | /** 119 | * ARCOUNT. 120 | * 121 | * @var int 122 | */ 123 | private $additionalRecordsCount; 124 | 125 | /** 126 | * @return int 127 | */ 128 | public function getId() 129 | { 130 | return $this->id; 131 | } 132 | 133 | /** 134 | * @param $id 135 | * 136 | * @return $this 137 | */ 138 | public function setId($id) 139 | { 140 | $this->id = (int) $id; 141 | 142 | return $this; 143 | } 144 | 145 | /** 146 | * @return bool 147 | */ 148 | public function isQuery() 149 | { 150 | return !$this->response; 151 | } 152 | 153 | /** 154 | * @return bool 155 | */ 156 | public function isResponse() 157 | { 158 | return $this->response; 159 | } 160 | 161 | /** 162 | * @param $response 163 | * 164 | * @return $this 165 | */ 166 | public function setResponse($response) 167 | { 168 | $this->response = (bool) $response; 169 | 170 | return $this; 171 | } 172 | 173 | /** 174 | * @param $query 175 | * 176 | * @return $this 177 | */ 178 | public function setQuery($query) 179 | { 180 | $this->response = !((bool) $query); 181 | 182 | return $this; 183 | } 184 | 185 | /** 186 | * @return int 187 | */ 188 | public function getOpcode() 189 | { 190 | return $this->opcode; 191 | } 192 | 193 | /** 194 | * @param $opcode 195 | * 196 | * @return $this 197 | */ 198 | public function setOpcode($opcode) 199 | { 200 | $this->opcode = (int) $opcode; 201 | 202 | return $this; 203 | } 204 | 205 | /** 206 | * @return bool 207 | */ 208 | public function isAuthoritative() 209 | { 210 | return $this->authoritative; 211 | } 212 | 213 | /** 214 | * @param $authoritative 215 | * 216 | * @return $this 217 | */ 218 | public function setAuthoritative($authoritative) 219 | { 220 | $this->authoritative = (bool) $authoritative; 221 | 222 | return $this; 223 | } 224 | 225 | /** 226 | * @return bool 227 | */ 228 | public function isTruncated() 229 | { 230 | return $this->truncated; 231 | } 232 | 233 | /** 234 | * @param $truncated 235 | * 236 | * @return $this 237 | */ 238 | public function setTruncated($truncated) 239 | { 240 | $this->truncated = (bool) $truncated; 241 | 242 | return $this; 243 | } 244 | 245 | /** 246 | * @return bool 247 | */ 248 | public function isRecursionDesired() 249 | { 250 | return $this->recursionDesired; 251 | } 252 | 253 | /** 254 | * @param $recursionDesired 255 | * 256 | * @return $this 257 | */ 258 | public function setRecursionDesired($recursionDesired) 259 | { 260 | $this->recursionDesired = (bool) $recursionDesired; 261 | 262 | return $this; 263 | } 264 | 265 | /** 266 | * @return bool 267 | */ 268 | public function isRecursionAvailable() 269 | { 270 | return $this->recursionAvailable; 271 | } 272 | 273 | /** 274 | * @param $recursionAvailable 275 | * 276 | * @return $this 277 | */ 278 | public function setRecursionAvailable($recursionAvailable) 279 | { 280 | $this->recursionAvailable = (bool) $recursionAvailable; 281 | 282 | return $this; 283 | } 284 | 285 | /** 286 | * @return int 287 | */ 288 | public function getZ() 289 | { 290 | return $this->z; 291 | } 292 | 293 | /** 294 | * @param $z 295 | * 296 | * @return $this 297 | */ 298 | public function setZ($z) 299 | { 300 | $this->z = (int) $z; 301 | 302 | return $this; 303 | } 304 | 305 | /** 306 | * @return int 307 | */ 308 | public function getRcode() 309 | { 310 | return $this->rcode; 311 | } 312 | 313 | /** 314 | * @param $rcode 315 | * 316 | * @return $this 317 | */ 318 | public function setRcode($rcode) 319 | { 320 | $this->rcode = (int) $rcode; 321 | 322 | return $this; 323 | } 324 | 325 | /** 326 | * @return int 327 | */ 328 | public function getQuestionCount() 329 | { 330 | return $this->questionCount; 331 | } 332 | 333 | /** 334 | * @param $questionCount 335 | * 336 | * @return $this 337 | */ 338 | public function setQuestionCount($questionCount) 339 | { 340 | $this->questionCount = (int) $questionCount; 341 | 342 | return $this; 343 | } 344 | 345 | /** 346 | * @return int 347 | */ 348 | public function getAnswerCount() 349 | { 350 | return $this->answerCount; 351 | } 352 | 353 | /** 354 | * @param $answerCount 355 | * 356 | * @return $this 357 | */ 358 | public function setAnswerCount($answerCount) 359 | { 360 | $this->answerCount = (int) $answerCount; 361 | 362 | return $this; 363 | } 364 | 365 | /** 366 | * @return int 367 | */ 368 | public function getNameServerCount() 369 | { 370 | return $this->nameServerCount; 371 | } 372 | 373 | /** 374 | * @param $nameServerCount 375 | * 376 | * @return $this 377 | */ 378 | public function setNameServerCount($nameServerCount) 379 | { 380 | $this->nameServerCount = (int) $nameServerCount; 381 | 382 | return $this; 383 | } 384 | 385 | /** 386 | * @return int 387 | */ 388 | public function getAdditionalRecordsCount() 389 | { 390 | return $this->additionalRecordsCount; 391 | } 392 | 393 | /** 394 | * @param $additionalRecordsCount 395 | * 396 | * @return $this 397 | */ 398 | public function setAdditionalRecordsCount($additionalRecordsCount) 399 | { 400 | $this->additionalRecordsCount = (int) $additionalRecordsCount; 401 | 402 | return $this; 403 | } 404 | } 405 | -------------------------------------------------------------------------------- /src/Message.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 | */ 11 | 12 | namespace yswery\DNS; 13 | 14 | class Message 15 | { 16 | /** 17 | * @var Header 18 | */ 19 | private $header; 20 | 21 | /** 22 | * @var ResourceRecord[] 23 | */ 24 | private $questions = []; 25 | 26 | /** 27 | * @var ResourceRecord[] 28 | */ 29 | private $answers = []; 30 | 31 | /** 32 | * @var ResourceRecord[] 33 | */ 34 | private $authoritatives = []; 35 | 36 | /** 37 | * @var ResourceRecord[] 38 | */ 39 | private $additionals = []; 40 | 41 | /** 42 | * Message constructor. 43 | * 44 | * @param Header|null $header 45 | */ 46 | public function __construct(Header $header = null) 47 | { 48 | if (null === $header) { 49 | $header = (new Header()) 50 | ->setQuestionCount(0) 51 | ->setAnswerCount(0) 52 | ->setNameServerCount(0) 53 | ->setAdditionalRecordsCount(0); 54 | } 55 | $this->setHeader($header); 56 | } 57 | 58 | /** 59 | * @return Header 60 | */ 61 | public function getHeader(): Header 62 | { 63 | return $this->header; 64 | } 65 | 66 | /** 67 | * @param Header $header 68 | * 69 | * @return Message 70 | */ 71 | public function setHeader(Header $header): Message 72 | { 73 | $this->header = $header; 74 | 75 | return $this; 76 | } 77 | 78 | /** 79 | * @return ResourceRecord[] 80 | */ 81 | public function getQuestions(): array 82 | { 83 | return $this->questions; 84 | } 85 | 86 | /** 87 | * @param ResourceRecord $resourceRecord 88 | * 89 | * @throws \InvalidArgumentException 90 | * 91 | * @return Message 92 | */ 93 | public function addQuestion(ResourceRecord $resourceRecord): Message 94 | { 95 | if (!$resourceRecord->isQuestion()) { 96 | throw new \InvalidArgumentException('Resource Record provided is not a question.'); 97 | } 98 | 99 | $this->questions[] = $resourceRecord; 100 | $this->header->setQuestionCount(count($this->questions)); 101 | 102 | return $this; 103 | } 104 | 105 | /** 106 | * @return ResourceRecord[] 107 | */ 108 | public function getAnswers(): array 109 | { 110 | return $this->answers; 111 | } 112 | 113 | /** 114 | * @param ResourceRecord $resourceRecord 115 | * 116 | * @return Message 117 | */ 118 | public function addAnswer(ResourceRecord $resourceRecord): Message 119 | { 120 | $this->answers[] = $resourceRecord; 121 | $this->header->setAnswerCount(count($this->answers)); 122 | 123 | return $this; 124 | } 125 | 126 | /** 127 | * @return ResourceRecord[] 128 | */ 129 | public function getAuthoritatives(): array 130 | { 131 | return $this->authoritatives; 132 | } 133 | 134 | /** 135 | * @param ResourceRecord $resourceRecord 136 | * 137 | * @return Message 138 | */ 139 | public function addAuthoritative(ResourceRecord $resourceRecord): Message 140 | { 141 | $this->authoritatives[] = $resourceRecord; 142 | $this->header->setNameServerCount(count($this->authoritatives)); 143 | 144 | return $this; 145 | } 146 | 147 | /** 148 | * @return ResourceRecord[] 149 | */ 150 | public function getAdditionals(): array 151 | { 152 | return $this->additionals; 153 | } 154 | 155 | /** 156 | * @param ResourceRecord $resourceRecord 157 | * 158 | * @return Message 159 | */ 160 | public function addAdditional(ResourceRecord $resourceRecord): Message 161 | { 162 | $this->additionals[] = $resourceRecord; 163 | $this->header->setAdditionalRecordsCount(count($this->additionals)); 164 | 165 | return $this; 166 | } 167 | 168 | /** 169 | * @param array $resourceRecords 170 | * 171 | * @return Message 172 | */ 173 | public function setQuestions(array $resourceRecords): Message 174 | { 175 | $this->questions = []; 176 | foreach ($resourceRecords as $resourceRecord) { 177 | $this->addQuestion($resourceRecord); 178 | } 179 | 180 | return $this; 181 | } 182 | 183 | /** 184 | * @param array $resourceRecords 185 | * 186 | * @return Message 187 | */ 188 | public function setAnswers(array $resourceRecords): Message 189 | { 190 | $this->answers = $resourceRecords; 191 | $this->header->setAnswerCount(count($this->answers)); 192 | 193 | return $this; 194 | } 195 | 196 | /** 197 | * @param array $resourceRecords 198 | * 199 | * @return Message 200 | */ 201 | public function setAuthoritatives(array $resourceRecords): Message 202 | { 203 | $this->authoritatives = $resourceRecords; 204 | $this->header->setNameServerCount(count($this->authoritatives)); 205 | 206 | return $this; 207 | } 208 | 209 | /** 210 | * @param array $resourceRecords 211 | * 212 | * @return Message 213 | */ 214 | public function setAdditionals(array $resourceRecords): Message 215 | { 216 | $this->additionals = $resourceRecords; 217 | $this->header->setAdditionalRecordsCount(count($this->additionals)); 218 | 219 | return $this; 220 | } 221 | } 222 | -------------------------------------------------------------------------------- /src/RdataDecoder.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 | */ 11 | 12 | namespace yswery\DNS; 13 | 14 | class RdataDecoder 15 | { 16 | /** 17 | * Maps an RData type to its decoder method. 18 | * 19 | * @var array 20 | */ 21 | private static $methodMap = [ 22 | RecordTypeEnum::TYPE_A => 'a', 23 | RecordTypeEnum::TYPE_AAAA => 'a', 24 | RecordTypeEnum::TYPE_CNAME => 'cname', 25 | RecordTypeEnum::TYPE_DNAME => 'cname', 26 | RecordTypeEnum::TYPE_NS => 'cname', 27 | RecordTypeEnum::TYPE_PTR => 'cname', 28 | RecordTypeEnum::TYPE_SOA => 'soa', 29 | RecordTypeEnum::TYPE_MX => 'mx', 30 | RecordTypeEnum::TYPE_TXT => 'txt', 31 | RecordTypeEnum::TYPE_SRV => 'srv', 32 | ]; 33 | 34 | /** 35 | * @param int $type 36 | * @param string $rdata 37 | * 38 | * @return array|string 39 | * 40 | * @throws UnsupportedTypeException 41 | */ 42 | public static function decodeRdata(int $type, string $rdata) 43 | { 44 | if (!array_key_exists($type, self::$methodMap)) { 45 | throw new UnsupportedTypeException(sprintf('Record type "%s" is not a supported type.', RecordTypeEnum::getName($type))); 46 | } 47 | 48 | return call_user_func(['self', self::$methodMap[$type]], $rdata); 49 | } 50 | 51 | /** 52 | * Used for A and AAAA records. 53 | * 54 | * @param string $rdata 55 | * 56 | * @return string 57 | */ 58 | public static function a(string $rdata): string 59 | { 60 | return inet_ntop($rdata); 61 | } 62 | 63 | /** 64 | * Used for CNAME, DNAME, NS, and PTR records. 65 | * 66 | * @param string $rdata 67 | * 68 | * @return string 69 | */ 70 | public static function cname(string $rdata): string 71 | { 72 | return Decoder::decodeDomainName($rdata); 73 | } 74 | 75 | /** 76 | * Exclusively for SOA records. 77 | * 78 | * @param string $rdata 79 | * 80 | * @return array 81 | */ 82 | public static function soa(string $rdata): array 83 | { 84 | $offset = 0; 85 | 86 | return array_merge( 87 | [ 88 | 'mname' => Decoder::decodeDomainName($rdata, $offset), 89 | 'rname' => Decoder::decodeDomainName($rdata, $offset), 90 | ], 91 | unpack('Nserial/Nrefresh/Nretry/Nexpire/Nminimum', substr($rdata, $offset)) 92 | ); 93 | } 94 | 95 | /** 96 | * Exclusively for MX records. 97 | * 98 | * @param string $rdata 99 | * 100 | * @return array 101 | */ 102 | public static function mx(string $rdata): array 103 | { 104 | return [ 105 | 'preference' => unpack('npreference', $rdata)['preference'], 106 | 'exchange' => Decoder::decodeDomainName(substr($rdata, 2)), 107 | ]; 108 | } 109 | 110 | /** 111 | * Exclusively for TXT records. 112 | * 113 | * @param string $rdata 114 | * 115 | * @return string 116 | */ 117 | public static function txt(string $rdata): string 118 | { 119 | $len = ord($rdata[0]); 120 | if ((strlen($rdata) + 1) < $len) { 121 | return ''; 122 | } 123 | 124 | return substr($rdata, 1, $len); 125 | } 126 | 127 | /** 128 | * Exclusively for SRV records. 129 | * 130 | * @param string $rdata 131 | * 132 | * @return array 133 | */ 134 | public static function srv(string $rdata): array 135 | { 136 | $offset = 6; 137 | $values = unpack('npriority/nweight/nport', $rdata); 138 | $values['target'] = Decoder::decodeDomainName($rdata, $offset); 139 | 140 | return $values; 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /src/RdataEncoder.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 | */ 11 | 12 | namespace yswery\DNS; 13 | 14 | use yswery\DNS\Resolver\ArrayRdata; 15 | 16 | class RdataEncoder 17 | { 18 | private static $methodMap = [ 19 | RecordTypeEnum::TYPE_A => 'a', 20 | RecordTypeEnum::TYPE_AAAA => 'a', 21 | RecordTypeEnum::TYPE_CNAME => 'cname', 22 | RecordTypeEnum::TYPE_DNAME => 'cname', 23 | RecordTypeEnum::TYPE_NS => 'cname', 24 | RecordTypeEnum::TYPE_PTR => 'cname', 25 | RecordTypeEnum::TYPE_SOA => 'soa', 26 | RecordTypeEnum::TYPE_MX => 'mx', 27 | RecordTypeEnum::TYPE_TXT => 'txt', 28 | RecordTypeEnum::TYPE_SRV => 'srv', 29 | ]; 30 | 31 | /** 32 | * @param int $type 33 | * @param string|array $rdata 34 | * 35 | * @return string 36 | * 37 | * @throws UnsupportedTypeException|\InvalidArgumentException 38 | */ 39 | public static function encodeRdata(int $type, $rdata): string 40 | { 41 | if ($rdata instanceof ArrayRdata) { 42 | return $rdata->getBadcowRdata()->toWire(); 43 | } 44 | 45 | if (!array_key_exists($type, self::$methodMap)) { 46 | throw new UnsupportedTypeException(sprintf('Record type "%s" is not a supported type.', RecordTypeEnum::getName($type))); 47 | } 48 | 49 | return call_user_func(['self', self::$methodMap[$type]], $rdata); 50 | } 51 | 52 | /** 53 | * Used for A and AAAA records. 54 | * 55 | * @param string $rdata 56 | * 57 | * @return string 58 | */ 59 | public static function a(string $rdata): string 60 | { 61 | if (!filter_var($rdata, FILTER_VALIDATE_IP)) { 62 | throw new \InvalidArgumentException(sprintf('The IP address "%s" is invalid.', $rdata)); 63 | } 64 | 65 | return inet_pton($rdata); 66 | } 67 | 68 | /** 69 | * Used for CNAME, DNAME, NS, and PTR records. 70 | * 71 | * @param string $rdata 72 | * 73 | * @return string 74 | */ 75 | public static function cname(string $rdata): string 76 | { 77 | return Encoder::encodeDomainName($rdata); 78 | } 79 | 80 | /** 81 | * Exclusively for SOA records. 82 | * 83 | * @param array $rdata 84 | * 85 | * @return string 86 | */ 87 | public static function soa(array $rdata): string 88 | { 89 | return 90 | Encoder::encodeDomainName($rdata['mname']). 91 | Encoder::encodeDomainName($rdata['rname']). 92 | pack( 93 | 'NNNNN', 94 | $rdata['serial'], 95 | $rdata['refresh'], 96 | $rdata['retry'], 97 | $rdata['expire'], 98 | $rdata['minimum'] 99 | ); 100 | } 101 | 102 | /** 103 | * Exclusively for MX records. 104 | * 105 | * @param array $rdata 106 | * 107 | * @return string 108 | */ 109 | public static function mx(array $rdata): string 110 | { 111 | return pack('n', (int) $rdata['preference']).Encoder::encodeDomainName($rdata['exchange']); 112 | } 113 | 114 | /** 115 | * Exclusively for TXT records. 116 | * 117 | * @param string $rdata 118 | * 119 | * @return string 120 | */ 121 | public static function txt(string $rdata): string 122 | { 123 | $rdata = substr($rdata, 0, 255); 124 | 125 | return chr(strlen($rdata)).$rdata; 126 | } 127 | 128 | /** 129 | * Exclusively for SRV records. 130 | * 131 | * @param array $rdata 132 | * 133 | * @return string 134 | */ 135 | public static function srv(array $rdata): string 136 | { 137 | return pack('nnn', (int) $rdata['priority'], (int) $rdata['weight'], (int) $rdata['port']). 138 | Encoder::encodeDomainName($rdata['target']); 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /src/RecordTypeEnum.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 | */ 11 | 12 | namespace yswery\DNS; 13 | 14 | class RecordTypeEnum 15 | { 16 | /** 17 | * @var array 18 | */ 19 | private static $names = [ 20 | self::TYPE_A => 'A', 21 | self::TYPE_NS => 'NS', 22 | self::TYPE_CNAME => 'CNAME', 23 | self::TYPE_SOA => 'SOA', 24 | self::TYPE_PTR => 'PTR', 25 | self::TYPE_MX => 'MX', 26 | self::TYPE_TXT => 'TXT', 27 | self::TYPE_AAAA => 'AAAA', 28 | self::TYPE_OPT => 'OPT', 29 | self::TYPE_AXFR => 'AXFR', 30 | self::TYPE_ANY => 'ANY', 31 | self::TYPE_AFSDB => 'AFSDB', 32 | self::TYPE_APL => 'APL', 33 | self::TYPE_CAA => 'CAA', 34 | self::TYPE_CDNSKEY => 'CDNSKEY', 35 | self::TYPE_CDS => 'CDS', 36 | self::TYPE_CERT => 'CERT', 37 | self::TYPE_DHCID => 'DHCID', 38 | self::TYPE_DLV => 'DLV', 39 | self::TYPE_DNSKEY => 'DNSKEY', 40 | self::TYPE_DS => 'DS', 41 | self::TYPE_IPSECKEY => 'IPSECKEY', 42 | self::TYPE_KEY => 'KEY', 43 | self::TYPE_KX => 'KX', 44 | self::TYPE_LOC => 'LOC', 45 | self::TYPE_NAPTR => 'NAPTR', 46 | self::TYPE_NSEC => 'NSEC', 47 | self::TYPE_NSEC3 => 'NSEC3', 48 | self::TYPE_NSEC3PARAM => 'NSEC3PARAM', 49 | self::TYPE_RRSIG => 'RRSIG', 50 | self::TYPE_RP => 'RP', 51 | self::TYPE_SIG => 'SIG', 52 | self::TYPE_SRV => 'SRV', 53 | self::TYPE_SSHFP => 'SSHFP', 54 | self::TYPE_TA => 'TA', 55 | self::TYPE_TKEY => 'TKEY', 56 | self::TYPE_TLSA => 'TLSA', 57 | self::TYPE_TSIG => 'TSIG', 58 | self::TYPE_URI => 'URI', 59 | self::TYPE_DNAME => 'DNAME', 60 | ]; 61 | 62 | public const TYPE_A = 1; 63 | public const TYPE_NS = 2; 64 | public const TYPE_CNAME = 5; 65 | public const TYPE_SOA = 6; 66 | public const TYPE_PTR = 12; 67 | public const TYPE_MX = 15; 68 | public const TYPE_TXT = 16; 69 | public const TYPE_AAAA = 28; 70 | public const TYPE_OPT = 41; 71 | public const TYPE_AXFR = 252; 72 | public const TYPE_ANY = 255; 73 | public const TYPE_AFSDB = 18; 74 | public const TYPE_APL = 42; 75 | public const TYPE_CAA = 257; 76 | public const TYPE_CDNSKEY = 60; 77 | public const TYPE_CDS = 59; 78 | public const TYPE_CERT = 37; 79 | public const TYPE_DHCID = 49; 80 | public const TYPE_DLV = 32769; 81 | public const TYPE_DNSKEY = 48; 82 | public const TYPE_DS = 43; 83 | public const TYPE_IPSECKEY = 45; 84 | public const TYPE_KEY = 25; 85 | public const TYPE_KX = 36; 86 | public const TYPE_LOC = 29; 87 | public const TYPE_NAPTR = 35; 88 | public const TYPE_NSEC = 47; 89 | public const TYPE_NSEC3 = 50; 90 | public const TYPE_NSEC3PARAM = 51; 91 | public const TYPE_RRSIG = 46; 92 | public const TYPE_RP = 17; 93 | public const TYPE_SIG = 24; 94 | public const TYPE_SRV = 33; 95 | public const TYPE_SSHFP = 44; 96 | public const TYPE_TA = 32768; 97 | public const TYPE_TKEY = 249; 98 | public const TYPE_TLSA = 52; 99 | public const TYPE_TSIG = 250; 100 | public const TYPE_URI = 256; 101 | public const TYPE_DNAME = 39; 102 | 103 | /** 104 | * @param int $type 105 | * 106 | * @return bool 107 | */ 108 | public static function isValid(int $type): bool 109 | { 110 | return array_key_exists($type, self::$names); 111 | } 112 | 113 | /** 114 | * Get the name of an RDATA type. E.g. RecordTypeEnum::getName(6) return 'SOA'. 115 | * 116 | * @param int $type The index of the type 117 | * 118 | * @return string 119 | * 120 | * @throws \InvalidArgumentException 121 | */ 122 | public static function getName(int $type): string 123 | { 124 | if (!self::isValid($type)) { 125 | throw new \InvalidArgumentException(sprintf('The integer "%d" does not correspond to a valid type', $type)); 126 | } 127 | 128 | return self::$names[$type]; 129 | } 130 | 131 | /** 132 | * Return the integer value of an RDATA type. E.g. getTypeFromName('MX') returns 15. 133 | * 134 | * @param string $name The name of the record type, e.g. = 'A' or 'MX' or 'SOA' 135 | * 136 | * @return int 137 | * 138 | * @throws \InvalidArgumentException 139 | */ 140 | public static function getTypeFromName(string $name): int 141 | { 142 | $type = array_search(strtoupper(trim($name)), self::$names); 143 | if (false === $type || !is_int($type)) { 144 | throw new \InvalidArgumentException(sprintf('RData type "%s" is not defined.', $name)); 145 | } 146 | 147 | return $type; 148 | } 149 | 150 | /** 151 | * @return array An array of all valid RDATA types 152 | */ 153 | public static function getTypes(): array 154 | { 155 | return self::$names; 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /src/Resolver/AbstractResolver.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 | */ 11 | 12 | namespace yswery\DNS\Resolver; 13 | 14 | use yswery\DNS\RecordTypeEnum; 15 | use yswery\DNS\ResourceRecord; 16 | use yswery\DNS\UnsupportedTypeException; 17 | 18 | abstract class AbstractResolver implements ResolverInterface 19 | { 20 | /** 21 | * @var bool 22 | */ 23 | protected $allowRecursion; 24 | 25 | /** 26 | * @var bool 27 | */ 28 | protected $isAuthoritative; 29 | 30 | /** 31 | * @var bool 32 | */ 33 | protected $supportsSaving = false; 34 | 35 | /** 36 | * @var ResourceRecord[] 37 | */ 38 | protected $resourceRecords = []; 39 | 40 | /** 41 | * Wildcard records are stored as an associative array of labels in reverse. E.g. 42 | * ResourceRecord for "*.example.com." is stored as ['com']['example']['*'][][][]. 43 | * 44 | * @var ResourceRecord[] 45 | */ 46 | protected $wildcardRecords = []; 47 | 48 | /** 49 | * @param ResourceRecord[] $queries 50 | * 51 | * @return array 52 | */ 53 | public function getAnswer(array $queries, ?string $client = null): array 54 | { 55 | $answers = []; 56 | foreach ($queries as $query) { 57 | $answer = $this->resourceRecords[$query->getName()][$query->getType()][$query->getClass()] ?? []; 58 | if (empty($answer)) { 59 | $answer = $this->findWildcardEntry($query); 60 | } 61 | 62 | $answers = array_merge($answers, $answer); 63 | } 64 | 65 | return $answers; 66 | } 67 | 68 | /** 69 | * {@inheritdoc} 70 | */ 71 | public function allowsRecursion(): bool 72 | { 73 | return $this->allowRecursion; 74 | } 75 | 76 | /** 77 | * {@inheritdoc} 78 | */ 79 | public function isAuthority($domain): bool 80 | { 81 | return $this->isAuthoritative; 82 | } 83 | 84 | /** 85 | * {@inheritdoc} 86 | */ 87 | public function supportsSaving() 88 | { 89 | return $this->supportsSaving; 90 | } 91 | 92 | /** 93 | * Determine if a domain is a wildcard domain. 94 | * 95 | * @param string $domain 96 | * 97 | * @return bool 98 | */ 99 | public function isWildcardDomain(string $domain): bool 100 | { 101 | $domain = rtrim($domain, '.').'.'; 102 | $pattern = '/^\*\.(?:[a-zA-Z0-9\-\_]+\.)*$/'; 103 | 104 | return (bool) preg_match($pattern, $domain); 105 | } 106 | 107 | /** 108 | * @param ResourceRecord[] $resourceRecords 109 | */ 110 | protected function addZone(array $resourceRecords): void 111 | { 112 | foreach ($resourceRecords as $resourceRecord) { 113 | if ($this->isWildcardDomain($resourceRecord->getName())) { 114 | $this->addWildcardRecord($resourceRecord); 115 | continue; 116 | } 117 | $this->resourceRecords[$resourceRecord->getName()][$resourceRecord->getType()][$resourceRecord->getClass()][] = $resourceRecord; 118 | } 119 | } 120 | 121 | /** 122 | * Add a wildcard ResourceRecord. 123 | * 124 | * @param ResourceRecord $resourceRecord 125 | */ 126 | protected function addWildcardRecord(ResourceRecord $resourceRecord): void 127 | { 128 | $labels = explode('.', rtrim($resourceRecord->getName(), '.')); 129 | $labels = array_reverse($labels); 130 | 131 | $array = &$this->wildcardRecords; 132 | foreach ($labels as $label) { 133 | if ('*' === $label) { 134 | $array[$label][$resourceRecord->getClass()][$resourceRecord->getType()][] = $resourceRecord; 135 | break; 136 | } 137 | 138 | $array = &$array[$label]; 139 | } 140 | } 141 | 142 | /** 143 | * @param ResourceRecord $query 144 | * 145 | * @return array 146 | */ 147 | protected function findWildcardEntry(ResourceRecord $query): array 148 | { 149 | $labels = explode('.', rtrim($query->getName(), '.')); 150 | $labels = array_reverse($labels); 151 | 152 | /** @var ResourceRecord[] $wildcards */ 153 | $wildcards = []; 154 | $array = &$this->wildcardRecords; 155 | foreach ($labels as $label) { 156 | if (array_key_exists($label, $array)) { 157 | $array = &$array[$label]; 158 | continue; 159 | } 160 | 161 | if (array_key_exists('*', $array)) { 162 | $wildcards = $array['*'][$query->getClass()][$query->getType()] ?? []; 163 | } 164 | } 165 | 166 | $answers = []; 167 | foreach ($wildcards as $wildcard) { 168 | $rr = clone $wildcard; 169 | $rr->setName($query->getName()); 170 | $answers[] = $rr; 171 | } 172 | 173 | return $answers; 174 | } 175 | 176 | /** 177 | * Add the parent domain to names that are not fully qualified. 178 | * 179 | * AbstractResolver::handleName('www', 'example.com.') //Outputs 'www.example.com.' 180 | * AbstractResolver::handleName('@', 'example.com.') //Outputs 'example.com.' 181 | * AbstractResolver::handleName('ns1.example.com.', 'example.com.') //Outputs 'ns1.example.com.' 182 | * 183 | * @param $name 184 | * @param $parent 185 | * 186 | * @return string 187 | */ 188 | protected function handleName(string $name, string $parent) 189 | { 190 | if ('@' === $name || '' === $name) { 191 | return $parent; 192 | } 193 | 194 | if ('.' !== substr($name, -1, 1)) { 195 | return $name.'.'.$parent; 196 | } 197 | 198 | return $name; 199 | } 200 | 201 | /** 202 | * @param array $resourceRecord 203 | * @param int $type 204 | * @param string $parent 205 | * 206 | * @return mixed 207 | * 208 | * @throws UnsupportedTypeException 209 | */ 210 | protected function extractRdata(array $resourceRecord, int $type, string $parent) 211 | { 212 | switch ($type) { 213 | case RecordTypeEnum::TYPE_A: 214 | case RecordTypeEnum::TYPE_AAAA: 215 | return $resourceRecord['address']; 216 | case RecordTypeEnum::TYPE_NS: 217 | case RecordTypeEnum::TYPE_CNAME: 218 | case RecordTypeEnum::TYPE_PTR: 219 | return $this->handleName($resourceRecord['target'], $parent); 220 | case RecordTypeEnum::TYPE_SOA: 221 | return [ 222 | 'mname' => $this->handleName($resourceRecord['mname'], $parent), 223 | 'rname' => $this->handleName($resourceRecord['rname'], $parent), 224 | 'serial' => $resourceRecord['serial'], 225 | 'refresh' => $resourceRecord['refresh'], 226 | 'retry' => $resourceRecord['retry'], 227 | 'expire' => $resourceRecord['expire'], 228 | 'minimum' => $resourceRecord['minimum'], 229 | ]; 230 | case RecordTypeEnum::TYPE_MX: 231 | return [ 232 | 'preference' => $resourceRecord['preference'], 233 | 'exchange' => $this->handleName($resourceRecord['exchange'], $parent), 234 | ]; 235 | case RecordTypeEnum::TYPE_TXT: 236 | return $resourceRecord['text']; 237 | case RecordTypeEnum::TYPE_SRV: 238 | return [ 239 | 'priority' => (int) $resourceRecord['priority'], 240 | 'weight' => (int) $resourceRecord['weight'], 241 | 'port' => (int) $resourceRecord['port'], 242 | 'target' => $this->handleName($resourceRecord['target'], $parent), 243 | ]; 244 | case RecordTypeEnum::TYPE_AXFR: 245 | case RecordTypeEnum::TYPE_ANY: 246 | return ''; 247 | default: 248 | throw new UnsupportedTypeException(sprintf('Resource Record type "%s" is not a supported type.', RecordTypeEnum::getName($type))); 249 | } 250 | } 251 | } 252 | -------------------------------------------------------------------------------- /src/Resolver/ArrayRdata.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 | */ 11 | 12 | namespace yswery\DNS\Resolver; 13 | 14 | use ArrayAccess; 15 | use Badcow\DNS\Rdata\RdataInterface; 16 | use Symfony\Component\PropertyAccess\PropertyAccess; 17 | use Symfony\Component\PropertyAccess\PropertyAccessor; 18 | 19 | /** 20 | * Represents Badcow\DNS\Rdata\RdataInterface as an array. 21 | */ 22 | class ArrayRdata implements ArrayAccess 23 | { 24 | /** 25 | * @var RdataInterface 26 | */ 27 | private $rdata; 28 | 29 | /** 30 | * @var PropertyAccessor 31 | */ 32 | private $propertyAccessor; 33 | 34 | public function __construct(RdataInterface $rdata) 35 | { 36 | $this->rdata = $rdata; 37 | $this->propertyAccessor = PropertyAccess::createPropertyAccessor(); 38 | } 39 | 40 | public function getBadcowRdata(): RdataInterface 41 | { 42 | return $this->rdata; 43 | } 44 | 45 | public function offsetExists($offset): bool 46 | { 47 | return $this->propertyAccessor->isReadable($this->rdata, $offset); 48 | } 49 | 50 | public function offsetGet($offset) 51 | { 52 | return $this->propertyAccessor->getValue($this->rdata, $offset); 53 | } 54 | 55 | public function offsetSet($offset, $value): void 56 | { 57 | $this->propertyAccessor->setValue($this->rdata, $offset, $value); 58 | } 59 | 60 | public function offsetUnset($offset): void 61 | { 62 | $this->propertyAccessor->setValue($this->rdata, $offset, null); 63 | } 64 | 65 | public function __toString() 66 | { 67 | return $this->rdata->toText(); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/Resolver/BindResolver.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 | */ 11 | 12 | namespace yswery\DNS\Resolver; 13 | 14 | use Badcow\DNS\Parser\ParseException; 15 | use Badcow\DNS\Parser\Parser; 16 | use Badcow\DNS\ResourceRecord as BadcowRR; 17 | use Badcow\DNS\ZoneBuilder; 18 | use yswery\DNS\ResourceRecord; 19 | 20 | class BindResolver extends AbstractResolver 21 | { 22 | /** 23 | * BindResolver constructor. 24 | * 25 | * @param array $files 26 | * 27 | * @throws ParseException 28 | */ 29 | public function __construct(array $files) 30 | { 31 | $this->isAuthoritative = true; 32 | $this->allowRecursion = false; 33 | 34 | $resourceRecords = []; 35 | 36 | foreach ($files as $file) { 37 | $fileContents = file_get_contents($file); 38 | $zone = Parser::parse('.', $fileContents); 39 | ZoneBuilder::fillOutZone($zone); 40 | 41 | foreach ($zone as $rr) { 42 | $resourceRecords[] = self::convertResourceRecord($rr); 43 | } 44 | } 45 | 46 | $this->addZone($resourceRecords); 47 | } 48 | 49 | /** 50 | * Converts Badcow\DNS\ResourceRecord object to yswery\DNS\ResourceRecord object. 51 | * 52 | * @return ResourceRecord 53 | */ 54 | public static function convertResourceRecord(BadcowRR $badcowResourceRecord, bool $isQuestion = false): ResourceRecord 55 | { 56 | $rr = new ResourceRecord(); 57 | $rr->setName($badcowResourceRecord->getName()); 58 | $rr->setClass($badcowResourceRecord->getClassId()); 59 | $rr->setRdata(new ArrayRdata($badcowResourceRecord->getRdata())); 60 | $rr->setTtl($badcowResourceRecord->getTtl()); 61 | $rr->setType($badcowResourceRecord->getRdata()->getTypeCode()); 62 | $rr->setQuestion($isQuestion); 63 | 64 | return $rr; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/Resolver/JsonFileSystemResolver.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 | */ 11 | 12 | namespace yswery\DNS\Resolver; 13 | 14 | use yswery\DNS\ClassEnum; 15 | use yswery\DNS\Exception\ZoneFileNotFoundException; 16 | use yswery\DNS\Filesystem\FilesystemManager; 17 | use yswery\DNS\RecordTypeEnum; 18 | use yswery\DNS\ResourceRecord; 19 | use yswery\DNS\UnsupportedTypeException; 20 | 21 | class JsonFileSystemResolver extends AbstractResolver 22 | { 23 | /** 24 | * @var int 25 | */ 26 | protected $defaultClass = ClassEnum::INTERNET; 27 | 28 | /** 29 | * @var FilesystemManager 30 | */ 31 | protected $filesystemManager; 32 | 33 | /** 34 | * @var int 35 | */ 36 | protected $defaultTtl; 37 | 38 | /** 39 | * JsonResolver constructor. 40 | * 41 | * @param FilesystemManager $filesystemManager 42 | * @param int $defaultTtl 43 | * 44 | * @throws UnsupportedTypeException 45 | */ 46 | public function __construct(FilesystemManager $filesystemManager, $defaultTtl = 300) 47 | { 48 | $this->isAuthoritative = true; 49 | $this->allowRecursion = false; 50 | $this->filesystemManager = $filesystemManager; 51 | $this->defaultTtl = $defaultTtl; 52 | 53 | $zones = glob($filesystemManager->zonePath().'/*.json'); 54 | foreach ($zones as $file) { 55 | $zone = json_decode(file_get_contents($file), true); 56 | $resourceRecords = $this->isLegacyFormat($zone) ? $this->processLegacyZone($zone) : $this->processZone($zone); 57 | $this->addZone($resourceRecords); 58 | } 59 | } 60 | 61 | /** 62 | * Load a zone file. 63 | * 64 | * @param string $file 65 | * 66 | * @throws UnsupportedTypeException 67 | * @throws ZoneFileNotFoundException 68 | */ 69 | public function loadZone($file) 70 | { 71 | if (file_exists($file)) { 72 | $zone = json_decode(file_get_contents($file), true); 73 | $resourceRecords = $this->isLegacyFormat($zone) ? $this->processLegacyZone($zone) : $this->processZone($zone); 74 | $this->addZone($resourceRecords); 75 | } else { 76 | throw new ZoneFileNotFoundException('The zone file could not be found'); 77 | } 78 | } 79 | 80 | /** 81 | * Saves the zone to a .json file. 82 | * 83 | * @param string $zone 84 | */ 85 | public function saveZone($zone) 86 | { 87 | } 88 | 89 | /** 90 | * @param array $zone 91 | * 92 | * @return ResourceRecord[] 93 | * 94 | * @throws UnsupportedTypeException 95 | */ 96 | protected function processZone(array $zone): array 97 | { 98 | $parent = rtrim($zone['domain'], '.').'.'; 99 | $defaultTtl = $zone['default-ttl']; 100 | $rrs = $zone['resource-records']; 101 | $resourceRecords = []; 102 | 103 | foreach ($rrs as $rr) { 104 | $name = $rr['name'] ?? $parent; 105 | $class = isset($rr['class']) ? ClassEnum::getClassFromName($rr['class']) : $this->defaultClass; 106 | 107 | $resourceRecords[] = (new ResourceRecord()) 108 | ->setName($this->handleName($name, $parent)) 109 | ->setClass($class) 110 | ->setType($type = RecordTypeEnum::getTypeFromName($rr['type'])) 111 | ->setTtl($rr['ttl'] ?? $defaultTtl) 112 | ->setRdata($this->extractRdata($rr, $type, $parent)); 113 | } 114 | 115 | return $resourceRecords; 116 | } 117 | 118 | /** 119 | * Determine if a $zone is in the legacy format. 120 | * 121 | * @param array $zone 122 | * 123 | * @return bool 124 | */ 125 | protected function isLegacyFormat(array $zone): bool 126 | { 127 | $keys = array_map(function ($value) { 128 | return strtolower($value); 129 | }, array_keys($zone)); 130 | 131 | return 132 | (false === array_search('domain', $keys, true)) || 133 | (false === array_search('resource-records', $keys, true)); 134 | } 135 | 136 | /** 137 | * @param array $zones 138 | * 139 | * @return array 140 | */ 141 | protected function processLegacyZone(array $zones): array 142 | { 143 | $resourceRecords = []; 144 | foreach ($zones as $domain => $types) { 145 | $domain = rtrim($domain, '.').'.'; 146 | foreach ($types as $type => $data) { 147 | $data = (array) $data; 148 | $type = RecordTypeEnum::getTypeFromName($type); 149 | foreach ($data as $rdata) { 150 | $resourceRecords[] = (new ResourceRecord()) 151 | ->setName($domain) 152 | ->setType($type) 153 | ->setClass($this->defaultClass) 154 | ->setTtl($this->defaultTtl) 155 | ->setRdata($rdata); 156 | } 157 | } 158 | } 159 | 160 | return $resourceRecords; 161 | } 162 | } 163 | -------------------------------------------------------------------------------- /src/Resolver/JsonResolver.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 | */ 11 | 12 | namespace yswery\DNS\Resolver; 13 | 14 | use yswery\DNS\ClassEnum; 15 | use yswery\DNS\RecordTypeEnum; 16 | use yswery\DNS\ResourceRecord; 17 | use yswery\DNS\UnsupportedTypeException; 18 | 19 | class JsonResolver extends AbstractResolver 20 | { 21 | /** 22 | * @var int 23 | */ 24 | protected $defaultClass = ClassEnum::INTERNET; 25 | 26 | /** 27 | * @var int 28 | */ 29 | protected $defaultTtl; 30 | 31 | /** 32 | * JsonResolver constructor. 33 | * 34 | * @param array $files 35 | * @param int $defaultTtl 36 | * 37 | * @throws UnsupportedTypeException 38 | */ 39 | public function __construct(array $files, $defaultTtl = 300) 40 | { 41 | $this->isAuthoritative = true; 42 | $this->allowRecursion = false; 43 | $this->defaultTtl = $defaultTtl; 44 | 45 | foreach ($files as $file) { 46 | $zone = json_decode(file_get_contents($file), true); 47 | $resourceRecords = $this->isLegacyFormat($zone) ? $this->processLegacyZone($zone) : $this->processZone($zone); 48 | $this->addZone($resourceRecords); 49 | } 50 | } 51 | 52 | /** 53 | * @param array $zone 54 | * 55 | * @return ResourceRecord[] 56 | * 57 | * @throws UnsupportedTypeException 58 | */ 59 | protected function processZone(array $zone): array 60 | { 61 | $parent = rtrim($zone['domain'], '.').'.'; 62 | $defaultTtl = $zone['default-ttl']; 63 | $rrs = $zone['resource-records']; 64 | $resourceRecords = []; 65 | 66 | foreach ($rrs as $rr) { 67 | $name = $rr['name'] ?? $parent; 68 | $class = isset($rr['class']) ? ClassEnum::getClassFromName($rr['class']) : $this->defaultClass; 69 | 70 | $resourceRecords[] = (new ResourceRecord()) 71 | ->setName($this->handleName($name, $parent)) 72 | ->setClass($class) 73 | ->setType($type = RecordTypeEnum::getTypeFromName($rr['type'])) 74 | ->setTtl($rr['ttl'] ?? $defaultTtl) 75 | ->setRdata($this->extractRdata($rr, $type, $parent)); 76 | } 77 | 78 | return $resourceRecords; 79 | } 80 | 81 | /** 82 | * Determine if a $zone is in the legacy format. 83 | * 84 | * @param array $zone 85 | * 86 | * @return bool 87 | */ 88 | protected function isLegacyFormat(array $zone): bool 89 | { 90 | $keys = array_map(function ($value) { 91 | return strtolower($value); 92 | }, array_keys($zone)); 93 | 94 | return 95 | (false === array_search('domain', $keys, true)) || 96 | (false === array_search('resource-records', $keys, true)); 97 | } 98 | 99 | /** 100 | * @param array $zones 101 | * 102 | * @return array 103 | */ 104 | protected function processLegacyZone(array $zones): array 105 | { 106 | $resourceRecords = []; 107 | foreach ($zones as $domain => $types) { 108 | $domain = rtrim($domain, '.').'.'; 109 | foreach ($types as $type => $data) { 110 | $data = (array) $data; 111 | $type = RecordTypeEnum::getTypeFromName($type); 112 | foreach ($data as $rdata) { 113 | $resourceRecords[] = (new ResourceRecord()) 114 | ->setName($domain) 115 | ->setType($type) 116 | ->setClass($this->defaultClass) 117 | ->setTtl($this->defaultTtl) 118 | ->setRdata($rdata); 119 | } 120 | } 121 | } 122 | 123 | return $resourceRecords; 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /src/Resolver/ResolverInterface.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 | */ 11 | 12 | namespace yswery\DNS\Resolver; 13 | 14 | use yswery\DNS\ResourceRecord; 15 | 16 | /** 17 | * Class ResolverInterface. 18 | */ 19 | interface ResolverInterface 20 | { 21 | /** 22 | * Return answer for given query. 23 | * 24 | * @param ResourceRecord[] $queries 25 | * 26 | * @return ResourceRecord[] 27 | */ 28 | public function getAnswer(array $queries, ?string $client = null): array; 29 | 30 | /** 31 | * Returns true if resolver supports recursion. 32 | * 33 | * @return bool 34 | */ 35 | public function allowsRecursion(): bool; 36 | 37 | /** 38 | * Check if the resolver knows about a domain. 39 | * Returns true if the resolver holds info about $domain. 40 | * 41 | * @param string $domain The domain to check for 42 | * 43 | * @return bool 44 | */ 45 | public function isAuthority($domain): bool; 46 | } 47 | -------------------------------------------------------------------------------- /src/Resolver/StackableResolver.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 | */ 11 | 12 | namespace yswery\DNS\Resolver; 13 | 14 | use yswery\DNS\ResourceRecord; 15 | 16 | class StackableResolver implements ResolverInterface 17 | { 18 | /** 19 | * @var ResolverInterface[] 20 | */ 21 | protected $resolvers; 22 | 23 | public function __construct(array $resolvers = []) 24 | { 25 | $this->resolvers = $resolvers; 26 | } 27 | 28 | /** 29 | * @param ResourceRecord[] $question 30 | * 31 | * @return array 32 | */ 33 | public function getAnswer(array $question, ?string $client = null): array 34 | { 35 | foreach ($this->resolvers as $resolver) { 36 | $answer = $resolver->getAnswer($question); 37 | if (!empty($answer)) { 38 | return $answer; 39 | } 40 | } 41 | 42 | return []; 43 | } 44 | 45 | /** 46 | * Check if any of the resolvers supports recursion. 47 | * 48 | * @return bool true if any resolver supports recursion 49 | */ 50 | public function allowsRecursion(): bool 51 | { 52 | foreach ($this->resolvers as $resolver) { 53 | if ($resolver->allowsRecursion()) { 54 | return true; 55 | } 56 | } 57 | 58 | return false; 59 | } 60 | 61 | /* 62 | * Check if any resolver knows about a domain 63 | * 64 | * @param string $domain the domain to check for 65 | * @return boolean true if some resolver holds info about $domain 66 | */ 67 | public function isAuthority($domain): bool 68 | { 69 | foreach ($this->resolvers as $resolver) { 70 | if ($resolver->isAuthority($domain)) { 71 | return true; 72 | } 73 | } 74 | 75 | return false; 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/Resolver/SystemResolver.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 | */ 11 | 12 | namespace yswery\DNS\Resolver; 13 | 14 | use yswery\DNS\RecordTypeEnum; 15 | use yswery\DNS\ResourceRecord; 16 | use yswery\DNS\UnsupportedTypeException; 17 | 18 | /** 19 | * Use the host system's configured DNS. 20 | */ 21 | class SystemResolver extends AbstractResolver 22 | { 23 | /** 24 | * SystemResolver constructor. 25 | * 26 | * @param bool $recursionAvailable 27 | * @param bool $authoritative 28 | */ 29 | public function __construct($recursionAvailable = true, $authoritative = false) 30 | { 31 | $this->allowRecursion = (bool) $recursionAvailable; 32 | $this->isAuthoritative = (bool) $authoritative; 33 | } 34 | 35 | /** 36 | * @param ResourceRecord[] $queries 37 | * 38 | * @return ResourceRecord[] 39 | * 40 | * @throws UnsupportedTypeException 41 | */ 42 | public function getAnswer(array $queries, ?string $client = null): array 43 | { 44 | $answer = []; 45 | foreach ($queries as $query) { 46 | $answer = array_merge($answer, $this->getRecordsRecursively($query)); 47 | } 48 | 49 | return $answer; 50 | } 51 | 52 | /** 53 | * Resolve the $query using the system configured local DNS. 54 | * 55 | * @param ResourceRecord $query 56 | * 57 | * @return ResourceRecord[] 58 | * 59 | * @throws UnsupportedTypeException 60 | */ 61 | private function getRecordsRecursively(ResourceRecord $query): array 62 | { 63 | $records = dns_get_record($query->getName(), $this->IANA2PHP($query->getType())); 64 | $result = []; 65 | 66 | foreach ($records as $record) { 67 | $result[] = (new ResourceRecord()) 68 | ->setName($query->getName()) 69 | ->setClass($query->getClass()) 70 | ->setTtl($record['ttl']) 71 | ->setRdata($this->extractPhpRdata($record)) 72 | ->setType($query->getType()); 73 | } 74 | 75 | return $result; 76 | } 77 | 78 | /** 79 | * @param array $resourceRecord 80 | * 81 | * @return array|string 82 | * 83 | * @throws UnsupportedTypeException 84 | */ 85 | protected function extractPhpRdata(array $resourceRecord) 86 | { 87 | $type = RecordTypeEnum::getTypeFromName($resourceRecord['type']); 88 | 89 | switch ($type) { 90 | case RecordTypeEnum::TYPE_A: 91 | return $resourceRecord['ip']; 92 | case RecordTypeEnum::TYPE_AAAA: 93 | return $resourceRecord['ipv6']; 94 | case RecordTypeEnum::TYPE_NS: 95 | case RecordTypeEnum::TYPE_CNAME: 96 | case RecordTypeEnum::TYPE_PTR: 97 | return $resourceRecord['target']; 98 | case RecordTypeEnum::TYPE_SOA: 99 | return [ 100 | 'mname' => $resourceRecord['mname'], 101 | 'rname' => $resourceRecord['rname'], 102 | 'serial' => $resourceRecord['serial'], 103 | 'refresh' => $resourceRecord['refresh'], 104 | 'retry' => $resourceRecord['retry'], 105 | 'expire' => $resourceRecord['expire'], 106 | 'minimum' => $resourceRecord['minimum-ttl'], 107 | ]; 108 | case RecordTypeEnum::TYPE_MX: 109 | return [ 110 | 'preference' => $resourceRecord['pri'], 111 | 'exchange' => $resourceRecord['host'], 112 | ]; 113 | case RecordTypeEnum::TYPE_TXT: 114 | return $resourceRecord['txt']; 115 | case RecordTypeEnum::TYPE_SRV: 116 | return [ 117 | 'priority' => $resourceRecord['pri'], 118 | 'port' => $resourceRecord['port'], 119 | 'weight' => $resourceRecord['weight'], 120 | 'target' => $resourceRecord['target'], 121 | ]; 122 | default: 123 | throw new UnsupportedTypeException(sprintf('Record type "%s" is not a supported type.', RecordTypeEnum::getName($type))); 124 | } 125 | } 126 | 127 | /** 128 | * Maps an IANA Rdata type to the built-in PHP DNS constant. 129 | * 130 | * @example $this->IANA_to_PHP(5) //Returns DNS_CNAME int(16) 131 | * 132 | * @param int $type the IANA RTYPE 133 | * 134 | * @return int the built-in PHP DNS_ constant or `false` if the type is not defined 135 | * 136 | * @throws UnsupportedTypeException|\InvalidArgumentException 137 | */ 138 | private function IANA2PHP(int $type): int 139 | { 140 | $constantName = 'DNS_'.RecordTypeEnum::getName($type); 141 | if (!defined($constantName)) { 142 | throw new UnsupportedTypeException(sprintf('Record type "%d" is not a supported type.', $type)); 143 | } 144 | 145 | $phpType = constant($constantName); 146 | 147 | if (!is_int($phpType)) { 148 | throw new \InvalidArgumentException(sprintf('Constant "%s" is not an integer.', $constantName)); 149 | } 150 | 151 | return $phpType; 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /src/Resolver/XmlResolver.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 | */ 11 | 12 | namespace yswery\DNS\Resolver; 13 | 14 | use yswery\DNS\ClassEnum; 15 | use yswery\DNS\RecordTypeEnum; 16 | use yswery\DNS\ResourceRecord; 17 | use yswery\DNS\UnsupportedTypeException; 18 | 19 | class XmlResolver extends AbstractResolver 20 | { 21 | private $defaultClass = ClassEnum::INTERNET; 22 | 23 | /** 24 | * XmlResolver constructor. 25 | * 26 | * @param array $files 27 | * 28 | * @throws UnsupportedTypeException 29 | */ 30 | public function __construct(array $files) 31 | { 32 | $this->isAuthoritative = true; 33 | $this->allowRecursion = false; 34 | 35 | foreach ($files as $file) { 36 | $xml = new \SimpleXMLElement(file_get_contents($file)); 37 | $this->addZone($this->process($xml)); 38 | } 39 | } 40 | 41 | /** 42 | * @param object $xml 43 | * 44 | * @return ResourceRecord[] 45 | * 46 | * @throws UnsupportedTypeException 47 | */ 48 | private function process($xml): array 49 | { 50 | $parent = (string) $xml->{'name'}; 51 | $defaultTtl = (int) $xml->{'default-ttl'}; 52 | $resourceRecords = []; 53 | 54 | foreach ($xml->{'resource-records'}->{'resource-record'} as $rr) { 55 | $name = (string) $rr->{'name'} ?? $parent; 56 | $class = isset($rr->{'class'}) ? ClassEnum::getClassFromName($rr->{'class'}) : $this->defaultClass; 57 | $ttl = isset($rr->{'ttl'}) ? (int) $rr->{'ttl'} : $defaultTtl; 58 | 59 | $resourceRecords[] = (new ResourceRecord()) 60 | ->setName($this->handleName($name, $parent)) 61 | ->setClass($class) 62 | ->setType($type = RecordTypeEnum::getTypeFromName($rr->{'type'})) 63 | ->setTtl($ttl) 64 | ->setRdata($this->extractRdata($this->simpleXmlToArray($rr->{'rdata'}), $type, $parent)); 65 | } 66 | 67 | return $resourceRecords; 68 | } 69 | 70 | /** 71 | * Convert a SimpleXML object to an associative array. 72 | * 73 | * @param \SimpleXMLElement $xmlObject 74 | * 75 | * @return array 76 | */ 77 | private function simpleXmlToArray(\SimpleXMLElement $xmlObject): array 78 | { 79 | $array = []; 80 | foreach ($xmlObject->children() as $node) { 81 | $array[$node->getName()] = is_array($node) ? $this->simpleXmlToArray($node) : (string) $node; 82 | } 83 | 84 | return $array; 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/Resolver/YamlResolver.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 | */ 11 | 12 | namespace yswery\DNS\Resolver; 13 | 14 | use Symfony\Component\Yaml\Yaml; 15 | use yswery\DNS\UnsupportedTypeException; 16 | 17 | /** 18 | * Store dns records in yaml files. 19 | */ 20 | class YamlResolver extends JsonResolver 21 | { 22 | /** 23 | * YamlResolver constructor. 24 | * 25 | * @param array $files 26 | * @param int $defaultTtl 27 | * 28 | * @throws UnsupportedTypeException 29 | */ 30 | public function __construct(array $files, $defaultTtl = 300) 31 | { 32 | parent::__construct([], $defaultTtl); 33 | 34 | foreach ($files as $file) { 35 | $zone = Yaml::parseFile($file); 36 | $resourceRecords = $this->isLegacyFormat($zone) ? $this->processLegacyZone($zone) : $this->processZone($zone); 37 | $this->addZone($resourceRecords); 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/ResourceRecord.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 | */ 11 | 12 | namespace yswery\DNS; 13 | 14 | class ResourceRecord 15 | { 16 | /** 17 | * @var string 18 | */ 19 | private $name; 20 | 21 | /** 22 | * @var int 23 | */ 24 | private $type; 25 | 26 | /** 27 | * @var int 28 | */ 29 | private $ttl; 30 | 31 | /** 32 | * @var string|array 33 | */ 34 | private $rdata; 35 | 36 | /** 37 | * @var int 38 | */ 39 | private $class = ClassEnum::INTERNET; 40 | 41 | /** 42 | * @var bool 43 | */ 44 | private $question = false; 45 | 46 | /** 47 | * @return string 48 | */ 49 | public function getName(): string 50 | { 51 | return $this->name; 52 | } 53 | 54 | /** 55 | * @param string $name 56 | * 57 | * @return ResourceRecord 58 | */ 59 | public function setName(string $name): ResourceRecord 60 | { 61 | $this->name = $name; 62 | 63 | return $this; 64 | } 65 | 66 | /** 67 | * @return int 68 | */ 69 | public function getType(): int 70 | { 71 | return $this->type; 72 | } 73 | 74 | /** 75 | * @param int $type 76 | * 77 | * @return ResourceRecord 78 | */ 79 | public function setType(int $type): ResourceRecord 80 | { 81 | $this->type = $type; 82 | 83 | return $this; 84 | } 85 | 86 | /** 87 | * @return int 88 | */ 89 | public function getTtl(): int 90 | { 91 | return $this->ttl; 92 | } 93 | 94 | /** 95 | * @param int $ttl 96 | * 97 | * @return ResourceRecord 98 | */ 99 | public function setTtl(int $ttl): ResourceRecord 100 | { 101 | $this->ttl = $ttl; 102 | 103 | return $this; 104 | } 105 | 106 | /** 107 | * @return array|string 108 | */ 109 | public function getRdata() 110 | { 111 | return $this->rdata; 112 | } 113 | 114 | /** 115 | * @param array|string $rdata 116 | * 117 | * @return ResourceRecord 118 | */ 119 | public function setRdata($rdata): ResourceRecord 120 | { 121 | $this->rdata = $rdata; 122 | 123 | return $this; 124 | } 125 | 126 | /** 127 | * @return int 128 | */ 129 | public function getClass(): int 130 | { 131 | return $this->class; 132 | } 133 | 134 | /** 135 | * @param int $class 136 | * 137 | * @return ResourceRecord 138 | */ 139 | public function setClass(int $class): ResourceRecord 140 | { 141 | $this->class = $class; 142 | 143 | return $this; 144 | } 145 | 146 | /** 147 | * @return bool 148 | */ 149 | public function isQuestion(): bool 150 | { 151 | return $this->question; 152 | } 153 | 154 | /** 155 | * @param bool $question 156 | * 157 | * @return ResourceRecord 158 | */ 159 | public function setQuestion(bool $question): ResourceRecord 160 | { 161 | $this->question = $question; 162 | 163 | return $this; 164 | } 165 | 166 | /** 167 | * @return string 168 | */ 169 | public function __toString() 170 | { 171 | if (is_array($this->rdata)) { 172 | $rdata = '('; 173 | foreach ($this->rdata as $key => $value) { 174 | $rdata .= $key.': '.$value.', '; 175 | } 176 | $rdata = rtrim($rdata, ', ').')'; 177 | } else { 178 | $rdata = $this->rdata; 179 | } 180 | 181 | return sprintf( 182 | '%s %s %s %s %s', 183 | $this->name, 184 | RecordTypeEnum::getName($this->type), 185 | ClassEnum::getName($this->class), 186 | $this->ttl, 187 | $rdata 188 | ); 189 | } 190 | } 191 | -------------------------------------------------------------------------------- /src/Server.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 | */ 11 | 12 | namespace yswery\DNS; 13 | 14 | use React\Datagram\Socket; 15 | use React\Datagram\SocketInterface; 16 | use React\EventLoop\LoopInterface; 17 | use Symfony\Component\EventDispatcher\Event; 18 | use Symfony\Component\EventDispatcher\EventDispatcherInterface; 19 | use yswery\DNS\Config\FileConfig; 20 | use yswery\DNS\Event\Events; 21 | use yswery\DNS\Event\MessageEvent; 22 | use yswery\DNS\Event\QueryReceiveEvent; 23 | use yswery\DNS\Event\QueryResponseEvent; 24 | use yswery\DNS\Event\ServerExceptionEvent; 25 | use yswery\DNS\Event\ServerStartEvent; 26 | use yswery\DNS\Filesystem\FilesystemManager; 27 | use yswery\DNS\Resolver\JsonFileSystemResolver; 28 | use yswery\DNS\Resolver\ResolverInterface; 29 | 30 | class Server 31 | { 32 | /** 33 | * The version of PhpDnsServer we are running. 34 | * 35 | * @var string 36 | */ 37 | const VERSION = '1.4.0'; 38 | 39 | /** 40 | * @var EventDispatcherInterface 41 | */ 42 | protected $dispatcher; 43 | 44 | /** 45 | * @var ResolverInterface 46 | */ 47 | protected $resolver; 48 | 49 | /** 50 | * @var int 51 | */ 52 | protected $port; 53 | 54 | /** 55 | * @var string 56 | */ 57 | protected $ip; 58 | 59 | /** 60 | * @var LoopInterface 61 | */ 62 | protected $loop; 63 | 64 | /** 65 | * @var FilesystemManager 66 | */ 67 | private $filesystemManager; 68 | 69 | /** 70 | * @var FileConfig 71 | */ 72 | private $config; 73 | 74 | /** 75 | * @var bool 76 | */ 77 | private $useFilesystem; 78 | 79 | /** 80 | * @var bool 81 | */ 82 | private $isWindows; 83 | 84 | /** 85 | * Server constructor. 86 | * 87 | * @param ResolverInterface $resolver 88 | * @param EventDispatcherInterface $dispatcher 89 | * @param FileConfig $config 90 | * @param string|null $storageDirectory 91 | * @param bool $useFilesystem 92 | * @param string $ip 93 | * @param int $port 94 | * 95 | * @throws \Exception 96 | */ 97 | public function __construct(?ResolverInterface $resolver = null, ?EventDispatcherInterface $dispatcher = null, ?FileConfig $config = null, string $storageDirectory = null, bool $useFilesystem = false, string $ip = '0.0.0.0', int $port = 53) 98 | { 99 | if (!function_exists('socket_create') || !extension_loaded('sockets')) { 100 | throw new \Exception('Socket extension or socket_create() function not found.'); 101 | } 102 | 103 | $this->dispatcher = $dispatcher; 104 | $this->resolver = $resolver; 105 | $this->config = $config; 106 | $this->port = $port; 107 | $this->ip = $ip; 108 | $this->useFilesystem = $useFilesystem; 109 | 110 | // detect os 111 | if ('WIN' === strtoupper(substr(PHP_OS, 0, 3))) { 112 | $this->isWindows = true; 113 | } else { 114 | $this->isWindows = false; 115 | } 116 | 117 | // only register filesystem if we want to use it 118 | if ($useFilesystem) { 119 | $this->filesystemManager = new FilesystemManager($storageDirectory); 120 | $this->resolver = new JsonFileSystemResolver($this->filesystemManager); 121 | } 122 | 123 | $this->loop = \React\EventLoop\Factory::create(); 124 | $factory = new \React\Datagram\Factory($this->loop); 125 | $factory->createServer($this->ip.':'.$this->port)->then(function (Socket $server) { 126 | $this->dispatch(Events::SERVER_START, new ServerStartEvent($server)); 127 | $server->on('message', [$this, 'onMessage']); 128 | })->otherwise(function (\Exception $exception) { 129 | $this->dispatch(Events::SERVER_START_FAIL, new ServerExceptionEvent($exception)); 130 | }); 131 | } 132 | 133 | /** 134 | * Start the server. 135 | */ 136 | public function start(): void 137 | { 138 | set_time_limit(0); 139 | $this->loop->run(); 140 | } 141 | 142 | public function run() 143 | { 144 | $this->start(); 145 | } 146 | 147 | /** 148 | * This methods gets called each time a query is received. 149 | * 150 | * @param string $message 151 | * @param string $address 152 | * @param SocketInterface $socket 153 | */ 154 | public function onMessage(string $message, string $address, SocketInterface $socket) 155 | { 156 | try { 157 | $this->dispatch(Events::MESSAGE, new MessageEvent($socket, $address, $message)); 158 | $socket->send($this->handleQueryFromStream($message, $address), $address); 159 | } catch (\Exception $exception) { 160 | $this->dispatch(Events::SERVER_EXCEPTION, new ServerExceptionEvent($exception)); 161 | } 162 | } 163 | 164 | /** 165 | * Decode a message and return an encoded response. 166 | * 167 | * @param string $buffer 168 | * 169 | * @return string 170 | * 171 | * @throws UnsupportedTypeException 172 | */ 173 | public function handleQueryFromStream(string $buffer, ?string $client = null): string 174 | { 175 | $message = Decoder::decodeMessage($buffer); 176 | $this->dispatch(Events::QUERY_RECEIVE, new QueryReceiveEvent($message)); 177 | 178 | $responseMessage = clone $message; 179 | $responseMessage->getHeader() 180 | ->setResponse(true) 181 | ->setRecursionAvailable($this->resolver->allowsRecursion()) 182 | ->setAuthoritative($this->isAuthoritative($message->getQuestions())); 183 | 184 | try { 185 | $answers = $this->resolver->getAnswer($responseMessage->getQuestions(), $client); 186 | $responseMessage->setAnswers($answers); 187 | $this->needsAdditionalRecords($responseMessage); 188 | $this->dispatch(Events::QUERY_RESPONSE, new QueryResponseEvent($responseMessage)); 189 | 190 | return Encoder::encodeMessage($responseMessage); 191 | } catch (UnsupportedTypeException $e) { 192 | $responseMessage 193 | ->setAnswers([]) 194 | ->getHeader()->setRcode(Header::RCODE_NOT_IMPLEMENTED); 195 | $this->dispatch(Events::QUERY_RESPONSE, new QueryResponseEvent($responseMessage)); 196 | 197 | return Encoder::encodeMessage($responseMessage); 198 | } 199 | } 200 | 201 | /** 202 | * @return EventDispatcherInterface 203 | */ 204 | public function getDispatcher(): EventDispatcherInterface 205 | { 206 | return $this->dispatcher; 207 | } 208 | 209 | /** 210 | * @return ResolverInterface 211 | */ 212 | public function getResolver(): ResolverInterface 213 | { 214 | return $this->resolver; 215 | } 216 | 217 | /** 218 | * @return int 219 | */ 220 | public function getPort(): int 221 | { 222 | return $this->port; 223 | } 224 | 225 | /** 226 | * @return string 227 | */ 228 | public function getIp(): string 229 | { 230 | return $this->ip; 231 | } 232 | 233 | /** 234 | * Populate the additional records of a message if required. 235 | * 236 | * @param Message $message 237 | */ 238 | protected function needsAdditionalRecords(Message $message): void 239 | { 240 | foreach ($message->getAnswers() as $answer) { 241 | $name = null; 242 | switch ($answer->getType()) { 243 | case RecordTypeEnum::TYPE_NS: 244 | $name = $answer->getRdata(); 245 | break; 246 | case RecordTypeEnum::TYPE_MX: 247 | $name = $answer->getRdata()['exchange']; 248 | break; 249 | case RecordTypeEnum::TYPE_SRV: 250 | $name = $answer->getRdata()['target']; 251 | break; 252 | } 253 | 254 | if (null === $name) { 255 | continue; 256 | } 257 | 258 | $query = [ 259 | (new ResourceRecord()) 260 | ->setQuestion(true) 261 | ->setType(RecordTypeEnum::TYPE_A) 262 | ->setName($name), 263 | 264 | (new ResourceRecord()) 265 | ->setQuestion(true) 266 | ->setType(RecordTypeEnum::TYPE_AAAA) 267 | ->setName($name), 268 | ]; 269 | 270 | foreach ($this->resolver->getAnswer($query) as $additional) { 271 | $message->addAdditional($additional); 272 | } 273 | } 274 | } 275 | 276 | /** 277 | * @param ResourceRecord[] $query 278 | * 279 | * @return bool 280 | */ 281 | protected function isAuthoritative(array $query): bool 282 | { 283 | if (empty($query)) { 284 | return false; 285 | } 286 | 287 | $authoritative = true; 288 | foreach ($query as $rr) { 289 | $authoritative &= $this->resolver->isAuthority($rr->getName()); 290 | } 291 | 292 | return $authoritative; 293 | } 294 | 295 | /** 296 | * @param string $eventName 297 | * @param Event|null $event 298 | * 299 | * @return Event|null 300 | */ 301 | protected function dispatch($eventName, ?Event $event = null): ?Event 302 | { 303 | if (null === $this->dispatcher) { 304 | return null; 305 | } 306 | 307 | return $this->dispatcher->dispatch($eventName, $event); 308 | } 309 | } 310 | -------------------------------------------------------------------------------- /src/UnsupportedTypeException.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 | */ 11 | 12 | namespace yswery\DNS; 13 | 14 | class UnsupportedTypeException extends \Exception 15 | { 16 | } 17 | -------------------------------------------------------------------------------- /tests/ClassEnumTest.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 | */ 11 | 12 | namespace yswery\DNS\Tests; 13 | 14 | use PHPUnit\Framework\TestCase; 15 | use yswery\DNS\ClassEnum; 16 | 17 | class ClassEnumTest extends TestCase 18 | { 19 | public function testGetClassFromName() 20 | { 21 | $this->assertEquals(ClassEnum::HESIOD, ClassEnum::getClassFromName('HS')); 22 | $this->assertEquals(ClassEnum::CHAOS, ClassEnum::getClassFromName('chaos')); 23 | $this->expectException(\InvalidArgumentException::class); 24 | ClassEnum::getClassFromName('NO'); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /tests/DecoderTest.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 | */ 11 | 12 | namespace yswery\DNS\Tests; 13 | 14 | use PHPUnit\Framework\TestCase; 15 | use yswery\DNS\Decoder; 16 | use yswery\DNS\Encoder; 17 | use yswery\DNS\RdataDecoder; 18 | use yswery\DNS\RdataEncoder; 19 | use yswery\DNS\RecordTypeEnum; 20 | use yswery\DNS\ResourceRecord; 21 | 22 | class DecoderTest extends TestCase 23 | { 24 | public function testDecodeDomainName() 25 | { 26 | $decoded_1 = 'www.example.com.'; 27 | $encoded_1 = chr(3).'www'.chr(7).'example'.chr(3).'com'."\0"; 28 | 29 | $decoded_2 = '.'; 30 | $encoded_2 = "\0"; 31 | 32 | $decoded_3 = 'tld.'; 33 | $encoded_3 = chr(3).'tld'."\0"; 34 | 35 | $offset = 0; 36 | $this->assertEquals($decoded_1, Decoder::decodeDomainName($encoded_1, $offset)); 37 | 38 | $offset = 0; 39 | $this->assertEquals($decoded_2, Decoder::decodeDomainName($encoded_2, $offset)); 40 | 41 | $offset = 0; 42 | $this->assertEquals($decoded_3, Decoder::decodeDomainName($encoded_3, $offset)); 43 | } 44 | 45 | /** 46 | * @throws \yswery\DNS\UnsupportedTypeException 47 | */ 48 | public function testDecodeQuestionResourceRecord() 49 | { 50 | $decoded_1[] = (new ResourceRecord()) 51 | ->setName('www.example.com.') 52 | ->setType(RecordTypeEnum::TYPE_A) 53 | ->setQuestion(true); 54 | 55 | $encoded_1 = 56 | chr(3).'www'.chr(7).'example'.chr(3).'com'."\0". 57 | pack('nn', 1, 1); 58 | 59 | $decoded_2[] = (new ResourceRecord()) 60 | ->setName('domain.com.au.') 61 | ->setType(RecordTypeEnum::TYPE_MX) 62 | ->setQuestion(true); 63 | 64 | $encoded_2 = 65 | chr(6).'domain'.chr(3).'com'.chr(2).'au'."\0". 66 | pack('nn', 15, 1); 67 | 68 | $decoded_3 = [$decoded_1[0], $decoded_2[0]]; 69 | $encoded_3 = $encoded_1.$encoded_2; 70 | 71 | $offset = 0; 72 | $this->assertEquals($decoded_1, Decoder::decodeResourceRecords($encoded_1, 1, $offset, true)); 73 | $offset = 0; 74 | $this->assertEquals($decoded_2, Decoder::decodeResourceRecords($encoded_2, 1, $offset, true)); 75 | $offset = 0; 76 | $this->assertEquals($decoded_3, Decoder::decodeResourceRecords($encoded_3, 2, $offset, true)); 77 | } 78 | 79 | /** 80 | * @throws \yswery\DNS\UnsupportedTypeException 81 | */ 82 | public function testDecodeResourceRecords() 83 | { 84 | $name = 'example.com.'; 85 | $nameEncoded = Encoder::encodeDomainName($name); 86 | $exchange = 'mail.example.com.'; 87 | $exchangeEncoded = Encoder::encodeDomainName($exchange); 88 | $priority = 10; 89 | $ttl = 1337; 90 | $class = 1; //INTERNET 91 | $type = RecordTypeEnum::TYPE_MX; 92 | $ipAddress = '192.163.5.2'; 93 | 94 | $rdata = pack('n', $priority).$exchangeEncoded; 95 | $rdata2 = inet_pton($ipAddress); 96 | 97 | $decoded1[] = (new ResourceRecord()) 98 | ->setName($name) 99 | ->setClass($class) 100 | ->setTtl($ttl) 101 | ->setType($type) 102 | ->setRdata([ 103 | 'preference' => $priority, 104 | 'exchange' => $exchange, 105 | ]); 106 | 107 | $decoded2[] = (new ResourceRecord()) 108 | ->setName($name) 109 | ->setClass($class) 110 | ->setTtl($ttl) 111 | ->setType(RecordTypeEnum::TYPE_A) 112 | ->setRdata($ipAddress); 113 | 114 | $decoded3 = array_merge($decoded1, $decoded2); 115 | 116 | $encoded1 = $nameEncoded.pack('nnNn', $type, $class, $ttl, strlen($rdata)).$rdata; 117 | $encoded2 = $nameEncoded.pack('nnNn', 1, $class, $ttl, strlen($rdata2)).$rdata2; 118 | $encoded3 = $encoded1.$encoded2; 119 | 120 | $this->assertEquals($decoded1, Decoder::decodeResourceRecords($encoded1)); 121 | $this->assertEquals($decoded2, Decoder::decodeResourceRecords($encoded2)); 122 | $this->assertEquals($decoded3, Decoder::decodeResourceRecords($encoded3, 2)); 123 | } 124 | 125 | /** 126 | * @throws \yswery\DNS\UnsupportedTypeException 127 | */ 128 | public function testDecodeRdata() 129 | { 130 | $decoded_1 = '192.168.0.1'; 131 | $encoded_1 = inet_pton($decoded_1); 132 | 133 | $decoded_2 = '2001:acad:1337:b8::19'; 134 | $encoded_2 = inet_pton($decoded_2); 135 | 136 | $decoded_5 = 'dns1.example.com.'; 137 | $encoded_5 = chr(4).'dns1'.chr(7).'example'.chr(3).'com'."\0"; 138 | 139 | $decoded_6_prime = [ 140 | 'mname' => 'example.com.', 141 | 'rname' => 'postmaster.example.com.', 142 | 'serial' => 1970010188, 143 | 'refresh' => 1800, 144 | 'retry' => 7200, 145 | 'expire' => 10800, 146 | 'minimum' => 3600, 147 | ]; 148 | 149 | $encoded_6 = 150 | chr(7).'example'.chr(3).'com'."\0". 151 | chr(10).'postmaster'.chr(7).'example'.chr(3).'com'."\0". 152 | pack('NNNNN', 1970010188, 1800, 7200, 10800, 3600); 153 | 154 | $encoded_7 = pack('n', 10).chr(4).'mail'.chr(7).'example'.chr(3).'com'."\0"; 155 | $decoded_7_prime = [ 156 | 'preference' => 10, 157 | 'exchange' => 'mail.example.com.', 158 | ]; 159 | 160 | $decoded_8 = 'This is a comment.'; 161 | $encoded_8 = chr(strlen($decoded_8)).$decoded_8; 162 | 163 | $this->assertEquals($decoded_1, RdataDecoder::decodeRdata(RecordTypeEnum::TYPE_A, $encoded_1)); 164 | $this->assertEquals($decoded_2, RdataDecoder::decodeRdata(RecordTypeEnum::TYPE_AAAA, $encoded_2)); 165 | $this->assertEquals($decoded_5, RdataDecoder::decodeRdata(RecordTypeEnum::TYPE_NS, $encoded_5)); 166 | $this->assertEquals($decoded_6_prime, RdataDecoder::decodeRdata(RecordTypeEnum::TYPE_SOA, $encoded_6)); 167 | $this->assertEquals($decoded_7_prime, RdataDecoder::decodeRdata(RecordTypeEnum::TYPE_MX, $encoded_7)); 168 | $this->assertEquals($decoded_8, RdataDecoder::decodeRdata(RecordTypeEnum::TYPE_TXT, $encoded_8)); 169 | } 170 | 171 | public function testDecodeHeader() 172 | { 173 | $id = 1337; 174 | $flags = 0b1000010000000000; //Indicates authoritative response. 175 | $qdcount = 1; 176 | $ancount = 2; 177 | $nscount = 0; 178 | $arcount = 0; 179 | 180 | $encoded = pack('nnnnnn', $id, $flags, $qdcount, $ancount, $nscount, $arcount); 181 | $header = Decoder::decodeHeader($encoded); 182 | 183 | $this->assertEquals($id, $header->getId()); 184 | $this->assertEquals($qdcount, $header->getQuestionCount()); 185 | $this->assertEquals($ancount, $header->getAnswerCount()); 186 | $this->assertEquals($nscount, $header->getNameServerCount()); 187 | $this->assertEquals($arcount, $header->getAdditionalRecordsCount()); 188 | 189 | $this->assertTrue($header->isResponse()); 190 | $this->assertEquals(0, $header->getOpcode()); 191 | $this->assertTrue($header->isAuthoritative()); 192 | $this->assertFalse($header->isTruncated()); 193 | $this->assertFalse($header->isRecursionDesired()); 194 | $this->assertFalse($header->isRecursionAvailable()); 195 | $this->assertEquals(0, $header->getZ()); 196 | $this->assertEquals(0, $header->getRcode()); 197 | } 198 | 199 | /** 200 | * @throws \yswery\DNS\UnsupportedTypeException 201 | */ 202 | public function testDecodeSrv() 203 | { 204 | $rdata = [ 205 | 'priority' => 1, 206 | 'weight' => 5, 207 | 'port' => 389, 208 | 'target' => 'ldap.example.com.', 209 | ]; 210 | 211 | $encoded = RdataEncoder::encodeRdata(RecordTypeEnum::TYPE_SRV, $rdata); 212 | $this->assertEquals($rdata, RdataDecoder::decodeRdata(RecordTypeEnum::TYPE_SRV, $encoded)); 213 | } 214 | } 215 | -------------------------------------------------------------------------------- /tests/DummyResolver.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 | */ 11 | 12 | namespace yswery\DNS\Tests; 13 | 14 | use yswery\DNS\RecordTypeEnum; 15 | use yswery\DNS\Resolver\ResolverInterface; 16 | use yswery\DNS\ResourceRecord; 17 | 18 | class DummyResolver implements ResolverInterface 19 | { 20 | public function isAuthority($domain): bool 21 | { 22 | return true; 23 | } 24 | 25 | public function allowsRecursion(): bool 26 | { 27 | return false; 28 | } 29 | 30 | /** 31 | * @param ResourceRecord[] $queries 32 | * 33 | * @return array 34 | */ 35 | public function getAnswer(array $queries, ?string $client = null): array 36 | { 37 | $q = $queries[0]; 38 | 39 | return [(new ResourceRecord()) 40 | ->setName($q->getName()) 41 | ->setClass($q->getClass()) 42 | ->setTtl(300) 43 | ->setType(RecordTypeEnum::TYPE_OPT) 44 | ->setRdata('Some data'), ]; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /tests/EncoderTest.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 | */ 11 | 12 | namespace yswery\DNS\Tests; 13 | 14 | use PHPUnit\Framework\TestCase; 15 | use yswery\DNS\ClassEnum; 16 | use yswery\DNS\Encoder; 17 | use yswery\DNS\Header; 18 | use yswery\DNS\RdataEncoder; 19 | use yswery\DNS\RecordTypeEnum; 20 | use yswery\DNS\ResourceRecord; 21 | 22 | class EncoderTest extends TestCase 23 | { 24 | public function testEncodeDomainName() 25 | { 26 | $input_1 = 'www.example.com.'; 27 | $expectation_1 = chr(3).'www'.chr(7).'example'.chr(3).'com'."\0"; 28 | 29 | $input_2 = '.'; 30 | $expectation_2 = "\0"; 31 | 32 | $input_3 = 'tld.'; 33 | $expectation_3 = chr(3).'tld'."\0"; 34 | 35 | $this->assertEquals($expectation_1, Encoder::encodeDomainName($input_1)); 36 | $this->assertEquals($expectation_2, Encoder::encodeDomainName($input_2)); 37 | $this->assertEquals($expectation_3, Encoder::encodeDomainName($input_3)); 38 | } 39 | 40 | /** 41 | * @throws \yswery\DNS\UnsupportedTypeException 42 | */ 43 | public function testEncodeQuestionResourceRecord() 44 | { 45 | $input_1 = []; 46 | $input_1[] = (new ResourceRecord()) 47 | ->setName('www.example.com.') 48 | ->setType(RecordTypeEnum::TYPE_A) 49 | ->setClass(ClassEnum::INTERNET) 50 | ->setQuestion(true); 51 | 52 | $expectation_1 = 53 | chr(3).'www'.chr(7).'example'.chr(3).'com'."\0". 54 | pack('nn', 1, 1); 55 | 56 | $input_2 = []; 57 | $input_2[] = (new ResourceRecord()) 58 | ->setName('domain.com.au.') 59 | ->setType(RecordTypeEnum::TYPE_MX) 60 | ->setClass(ClassEnum::INTERNET) 61 | ->setQuestion(2); 62 | 63 | $expectation_2 = 64 | chr(6).'domain'.chr(3).'com'.chr(2).'au'."\0". 65 | pack('nn', 15, 1); 66 | 67 | $input_3 = [$input_1[0], $input_2[0]]; 68 | $expectation_3 = $expectation_1.$expectation_2; 69 | 70 | $this->assertEquals($expectation_1, Encoder::encodeResourceRecords($input_1)); 71 | $this->assertEquals($expectation_2, Encoder::encodeResourceRecords($input_2)); 72 | $this->assertEquals($expectation_3, Encoder::encodeResourceRecords($input_3)); 73 | } 74 | 75 | /** 76 | * @throws \yswery\DNS\UnsupportedTypeException 77 | */ 78 | public function testEncodeResourceRecord() 79 | { 80 | $name = 'example.com.'; 81 | $nameEncoded = Encoder::encodeDomainName($name); 82 | $exchange = 'mail.example.com.'; 83 | $exchangeEncoded = Encoder::encodeDomainName($exchange); 84 | $preference = 10; 85 | $ttl = 1337; 86 | $class = ClassEnum::INTERNET; 87 | $type = RecordTypeEnum::TYPE_MX; 88 | $ipAddress = '192.163.5.2'; 89 | 90 | $rdata = pack('n', $preference).$exchangeEncoded; 91 | $rdata2 = inet_pton($ipAddress); 92 | 93 | $decoded1 = (new ResourceRecord()) 94 | ->setName($name) 95 | ->setTtl($ttl) 96 | ->setType(RecordTypeEnum::TYPE_MX) 97 | ->setRdata([ 98 | 'preference' => $preference, 99 | 'exchange' => $exchange, 100 | ]); 101 | 102 | $decoded2 = (new ResourceRecord()) 103 | ->setName($name) 104 | ->setTtl($ttl) 105 | ->setType(RecordTypeEnum::TYPE_A) 106 | ->setRdata($ipAddress); 107 | 108 | $encoded1 = $nameEncoded.pack('nnNn', $type, $class, $ttl, strlen($rdata)).$rdata; 109 | $encoded2 = $nameEncoded.pack('nnNn', 1, $class, $ttl, strlen($rdata2)).$rdata2; 110 | 111 | $this->assertEquals($encoded1, Encoder::encodeResourceRecords([$decoded1])); 112 | $this->assertEquals($encoded2, Encoder::encodeResourceRecords([$decoded2])); 113 | } 114 | 115 | /** 116 | * @throws \yswery\DNS\UnsupportedTypeException 117 | */ 118 | public function testEncodeType() 119 | { 120 | $decoded_1 = '192.168.0.1'; 121 | $encoded_1 = inet_pton($decoded_1); 122 | 123 | $decoded_2 = '2001:acad:1337:b8::19'; 124 | $encoded_2 = inet_pton($decoded_2); 125 | 126 | $decoded_5 = 'dns1.example.com.'; 127 | $encoded_5 = chr(4).'dns1'.chr(7).'example'.chr(3).'com'."\0"; 128 | 129 | $decoded_6 = [ 130 | 'mname' => 'example.com.', 131 | 'rname' => 'postmaster.example.com', 132 | 'serial' => 1970010188, 133 | 'refresh' => 1800, 134 | 'retry' => 7200, 135 | 'expire' => 10800, 136 | 'minimum' => 3600, 137 | ]; 138 | 139 | $encoded_6 = 140 | chr(7).'example'.chr(3).'com'."\0". 141 | chr(10).'postmaster'.chr(7).'example'.chr(3).'com'."\0". 142 | pack('NNNNN', 1970010188, 1800, 7200, 10800, 3600); 143 | 144 | $decoded_7 = [ 145 | 'preference' => 15, 146 | 'exchange' => 'mail.example.com.', 147 | ]; 148 | 149 | $encoded_7 = pack('n', 15).chr(4).'mail'.chr(7).'example'.chr(3).'com'."\0"; 150 | 151 | $decoded_8 = 'This is a comment.'; 152 | $encoded_8 = chr(18).$decoded_8; 153 | 154 | $this->assertEquals($encoded_1, RdataEncoder::encodeRdata(1, $decoded_1)); 155 | $this->assertEquals($encoded_2, RdataEncoder::encodeRdata(28, $decoded_2)); 156 | $this->assertEquals($encoded_5, RdataEncoder::encodeRdata(2, $decoded_5)); 157 | $this->assertEquals($encoded_6, RdataEncoder::encodeRdata(6, $decoded_6)); 158 | $this->assertEquals($encoded_7, RdataEncoder::encodeRdata(15, $decoded_7)); 159 | $this->assertEquals($encoded_8, RdataEncoder::encodeRdata(16, $decoded_8)); 160 | } 161 | 162 | /** 163 | * @expectedException \InvalidArgumentException 164 | * 165 | * @throws \yswery\DNS\UnsupportedTypeException 166 | */ 167 | public function testInvalidIpv4() 168 | { 169 | RdataEncoder::encodeRdata(RecordTypeEnum::TYPE_A, '192.168.1'); 170 | } 171 | 172 | /** 173 | * @expectedException \InvalidArgumentException 174 | * 175 | * @throws \yswery\DNS\UnsupportedTypeException 176 | */ 177 | public function testInvalidIpv6() 178 | { 179 | RdataEncoder::encodeRdata(RecordTypeEnum::TYPE_AAAA, '2001:acad:1337:b8:19'); 180 | } 181 | 182 | public function testEncodeHeader() 183 | { 184 | $id = 1337; 185 | $flags = 0b1000010000000000; 186 | $qdcount = 1; 187 | $ancount = 2; 188 | $nscount = 0; 189 | $arcount = 0; 190 | 191 | $encoded = pack('nnnnnn', $id, $flags, $qdcount, $ancount, $nscount, $arcount); 192 | 193 | $header = new Header(); 194 | $header 195 | ->setId($id) 196 | ->setResponse(true) 197 | ->setOpcode(Header::OPCODE_STANDARD_QUERY) 198 | ->setAuthoritative(true) 199 | ->setTruncated(false) 200 | ->setRecursionDesired(false) 201 | ->setRecursionAvailable(false) 202 | ->setRcode(Header::RCODE_NO_ERROR) 203 | ->setQuestionCount($qdcount) 204 | ->setAnswerCount($ancount) 205 | ->setNameServerCount($nscount) 206 | ->setAdditionalRecordsCount($arcount) 207 | ; 208 | 209 | $this->assertEquals($encoded, Encoder::encodeHeader($header)); 210 | } 211 | } 212 | -------------------------------------------------------------------------------- /tests/MockSocket.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 | */ 11 | 12 | namespace yswery\DNS\Tests; 13 | 14 | use Evenement\EventEmitterTrait; 15 | use React\Datagram\SocketInterface; 16 | 17 | class MockSocket implements SocketInterface 18 | { 19 | use EventEmitterTrait; 20 | 21 | private $transmissions = []; 22 | 23 | public function send($data, $remoteAddress = null) 24 | { 25 | $this->transmissions[] = $data; 26 | } 27 | 28 | public function getLastTransmission(): string 29 | { 30 | return end($this->transmissions); 31 | } 32 | 33 | public function close() 34 | { 35 | // TODO: Implement close() method. 36 | } 37 | 38 | public function end() 39 | { 40 | // TODO: Implement end() method. 41 | } 42 | 43 | public function resume() 44 | { 45 | // TODO: Implement resume() method. 46 | } 47 | 48 | public function pause() 49 | { 50 | // TODO: Implement pause() method. 51 | } 52 | 53 | public function getLocalAddress() 54 | { 55 | // TODO: Implement getLocalAddress() method. 56 | } 57 | 58 | public function getRemoteAddress() 59 | { 60 | // TODO: Implement getRemoteAddress() method. 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /tests/RecordTypeEnumTest.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 | */ 11 | 12 | namespace yswery\DNS\Tests; 13 | 14 | use PHPUnit\Framework\TestCase; 15 | use yswery\DNS\RecordTypeEnum; 16 | 17 | class RecordTypeEnumTest extends TestCase 18 | { 19 | public function testIsValid() 20 | { 21 | $this->assertTrue(RecordTypeEnum::isValid(1)); 22 | $this->assertFalse(RecordTypeEnum::isValid(3)); 23 | } 24 | 25 | public function testGetName() 26 | { 27 | $this->assertEquals('MX', RecordTypeEnum::getName(RecordTypeEnum::TYPE_MX)); 28 | $this->expectException(\InvalidArgumentException::class); 29 | RecordTypeEnum::getName(651); 30 | } 31 | 32 | public function testGetTypeFromName() 33 | { 34 | $this->assertEquals(15, RecordTypeEnum::getTypeFromName('MX')); 35 | $this->assertEquals(6, RecordTypeEnum::getTypeFromName('soa')); 36 | 37 | $this->expectException(\InvalidArgumentException::class); 38 | RecordTypeEnum::getTypeFromName('NONE'); 39 | } 40 | 41 | public function testGetTypes() 42 | { 43 | $this->assertTrue(is_array(RecordTypeEnum::getTypes())); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /tests/Resolver/AbstractResolverTest.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 | */ 11 | 12 | namespace yswery\DNS\Tests\Resolver; 13 | 14 | use PHPUnit\Framework\TestCase; 15 | use yswery\DNS\ClassEnum; 16 | use yswery\DNS\RecordTypeEnum; 17 | use yswery\DNS\Resolver\JsonResolver; 18 | use yswery\DNS\Resolver\ResolverInterface; 19 | use yswery\DNS\ResourceRecord; 20 | 21 | abstract class AbstractResolverTest extends TestCase 22 | { 23 | /** 24 | * @var ResolverInterface 25 | */ 26 | protected $resolver; 27 | 28 | public function testGetAnswer() 29 | { 30 | $query[] = (new ResourceRecord()) 31 | ->setName('example.com.') 32 | ->setType(RecordTypeEnum::TYPE_SOA) 33 | ->setClass(ClassEnum::INTERNET) 34 | ->setQuestion(true); 35 | 36 | $query[] = (new ResourceRecord()) 37 | ->setName('example.com.') 38 | ->setType(RecordTypeEnum::TYPE_AAAA) 39 | ->setClass(ClassEnum::INTERNET) 40 | ->setQuestion(true); 41 | 42 | $answer = $this->resolver->getAnswer($query); 43 | $this->assertCount(2, $answer); 44 | list($soa, $aaaa) = $answer; 45 | 46 | $this->assertEquals('example.com.', $soa->getName()); 47 | $this->assertEquals(ClassEnum::INTERNET, $soa->getClass()); 48 | $this->assertEquals(10800, $soa->getTtl()); 49 | $this->assertEquals(RecordTypeEnum::TYPE_SOA, $soa->getType()); 50 | $this->assertEquals('example.com.', $soa->getRdata()['mname']); 51 | $this->assertEquals('postmaster.example.com.', $soa->getRdata()['rname']); 52 | $this->assertEquals(2, $soa->getRdata()['serial']); 53 | $this->assertEquals(3600, $soa->getRdata()['refresh']); 54 | $this->assertEquals(7200, $soa->getRdata()['retry']); 55 | $this->assertEquals(10800, $soa->getRdata()['expire']); 56 | $this->assertEquals(3600, $soa->getRdata()['minimum']); 57 | 58 | $this->assertEquals('example.com.', $aaaa->getName()); 59 | $this->assertEquals(ClassEnum::INTERNET, $aaaa->getClass()); 60 | $this->assertEquals(7200, $aaaa->getTtl()); 61 | $this->assertEquals(RecordTypeEnum::TYPE_AAAA, $aaaa->getType()); 62 | $this->assertEquals(inet_pton('2001:acad:ad::32'), inet_pton($aaaa->getRdata())); 63 | } 64 | 65 | public function testUnconfiguredRecordDoesNotResolve() 66 | { 67 | $question[] = (new ResourceRecord()) 68 | ->setName('testestestes.com.') 69 | ->setType(RecordTypeEnum::TYPE_A) 70 | ->setQuestion(true); 71 | 72 | $this->assertEmpty($this->resolver->getAnswer($question)); 73 | } 74 | 75 | public function testHostRecordReturnsArray() 76 | { 77 | $question[] = (new ResourceRecord()) 78 | ->setName('test2.com.') 79 | ->setType(RecordTypeEnum::TYPE_A) 80 | ->setQuestion(true); 81 | 82 | $answer = $this->resolver->getAnswer($question); 83 | $this->assertCount(2, $answer); 84 | $this->assertEquals('test2.com.', $answer[0]->getName()); 85 | $this->assertEquals(RecordTypeEnum::TYPE_A, $answer[0]->getType()); 86 | $this->assertEquals('111.111.111.111', (string) $answer[0]->getRdata()); 87 | $this->assertEquals(300, $answer[0]->getTtl()); 88 | $this->assertEquals('test2.com.', $answer[1]->getName()); 89 | $this->assertEquals(RecordTypeEnum::TYPE_A, $answer[1]->getType()); 90 | $this->assertEquals('112.112.112.112', (string) $answer[1]->getRdata()); 91 | $this->assertEquals(300, $answer[1]->getTtl()); 92 | } 93 | 94 | public function testWildcardDomains() 95 | { 96 | $question[] = (new ResourceRecord()) 97 | ->setName('badcow.subdomain.example.com.') 98 | ->setType(RecordTypeEnum::TYPE_A) 99 | ->setQuestion(true); 100 | 101 | $answer = $this->resolver->getAnswer($question); 102 | $this->assertCount(1, $answer); 103 | $this->assertEquals('badcow.subdomain.example.com.', $answer[0]->getName()); 104 | $this->assertEquals(1, $answer[0]->getType()); 105 | $this->assertEquals('192.168.1.42', (string) $answer[0]->getRdata()); 106 | $this->assertEquals(7200, $answer[0]->getTtl()); 107 | } 108 | 109 | /** 110 | * @throws \yswery\DNS\UnsupportedTypeException 111 | */ 112 | public function testIsWildcardDomain() 113 | { 114 | $resolver = new JsonResolver([]); 115 | $this->assertTrue($resolver->isWildcardDomain('*.cat.com.')); 116 | $this->assertFalse($resolver->isWildcardDomain('github.com.')); 117 | } 118 | 119 | public function testAllowsRecursion() 120 | { 121 | $this->assertFalse($this->resolver->allowsRecursion()); 122 | } 123 | 124 | public function testIsAuthority() 125 | { 126 | $this->assertTrue($this->resolver->isAuthority('example.com.')); 127 | } 128 | 129 | public function testSrvRdata() 130 | { 131 | $query[] = (new ResourceRecord()) 132 | ->setName('_ldap._tcp.example.com.') 133 | ->setType(RecordTypeEnum::TYPE_SRV) 134 | ->setQuestion(true); 135 | 136 | $expectation[] = (new ResourceRecord()) 137 | ->setName('_ldap._tcp.example.com.') 138 | ->setType(RecordTypeEnum::TYPE_SRV) 139 | ->setTtl(7200) 140 | ->setRdata([ 141 | 'priority' => 1, 142 | 'weight' => 5, 143 | 'port' => 389, 144 | 'target' => 'ldap.example.com.', 145 | ]); 146 | 147 | $answer = $this->resolver->getAnswer($query); 148 | $this->assertCount(1, $answer); 149 | 150 | $srv = $answer[0]; 151 | $this->assertEquals('_ldap._tcp.example.com.', $srv->getName()); 152 | $this->assertEquals(ClassEnum::INTERNET, $srv->getClass()); 153 | $this->assertEquals(7200, $srv->getTtl()); 154 | $this->assertEquals(RecordTypeEnum::TYPE_SRV, $srv->getType()); 155 | 156 | $this->assertEquals(1, $srv->getRdata()['priority']); 157 | $this->assertEquals(5, $srv->getRdata()['weight']); 158 | $this->assertEquals(389, $srv->getRdata()['port']); 159 | $this->assertEquals('ldap.example.com.', $srv->getRdata()['target']); 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /tests/Resolver/BindResolverTest.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 | */ 11 | 12 | namespace yswery\DNS\Tests\Resolver; 13 | 14 | use Badcow\DNS\Parser\ParseException; 15 | use yswery\DNS\RdataEncoder; 16 | use yswery\DNS\RecordTypeEnum; 17 | use yswery\DNS\Resolver\BindResolver; 18 | use yswery\DNS\ResourceRecord; 19 | use yswery\DNS\UnsupportedTypeException; 20 | 21 | class BindResolverTest extends AbstractResolverTest 22 | { 23 | /** 24 | * @throws ParseException 25 | */ 26 | public function setUp() 27 | { 28 | $files = [ 29 | __DIR__.'/../Resources/example.com.db', 30 | __DIR__.'/../Resources/test2.com.db', 31 | ]; 32 | $this->resolver = new BindResolver($files); 33 | } 34 | 35 | /** 36 | * @throws ParseException 37 | * @throws UnsupportedTypeException 38 | */ 39 | public function testResolver() 40 | { 41 | $files = [__DIR__.'/../Resources/example.com-2.db']; 42 | $resolver = new BindResolver($files); 43 | 44 | $query = new ResourceRecord(); 45 | $query->setQuestion(true); 46 | $query->setType(2); 47 | $query->setName('example.com.'); 48 | 49 | $nsAnswer = $resolver->getAnswer([$query]); 50 | $this->assertCount(2, $nsAnswer); 51 | $this->assertEquals('ns2.nameserver.com.', (string) $nsAnswer[1]->getRdata()); 52 | $this->assertEquals(chr(3).'ns2'.chr(10).'nameserver'.chr(3).'com'.chr(0), RdataEncoder::encodeRdata(2, $nsAnswer[1]->getRdata())); 53 | 54 | $query = new ResourceRecord(); 55 | $query->setQuestion(true); 56 | $query->setType(RecordTypeEnum::TYPE_AAAA); 57 | $query->setName('ipv6.domain.example.com.'); 58 | 59 | $ipv6 = $resolver->getAnswer([$query]); 60 | $this->assertCount(1, $ipv6); 61 | $this->assertEquals('0000:0000:0000:0000:0000:0000:0000:0001', (string) $ipv6[0]->getRdata()); 62 | $this->assertEquals(inet_pton('::1'), RdataEncoder::encodeRdata(28, $ipv6[0]->getRdata())); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /tests/Resolver/JsonResolverTest.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 | */ 11 | 12 | namespace yswery\DNS\Tests\Resolver; 13 | 14 | use yswery\DNS\RecordTypeEnum; 15 | use yswery\DNS\Resolver\JsonResolver; 16 | use yswery\DNS\ResourceRecord; 17 | 18 | class JsonResolverTest extends AbstractResolverTest 19 | { 20 | /** 21 | * @throws \yswery\DNS\UnsupportedTypeException 22 | */ 23 | public function setUp() 24 | { 25 | $files = [ 26 | __DIR__.'/../Resources/example.com.json', 27 | __DIR__.'/../Resources/test_records.json', 28 | ]; 29 | $this->resolver = new JsonResolver($files, 300); 30 | } 31 | 32 | public function testResolveLegacyRecord() 33 | { 34 | $question[] = (new ResourceRecord()) 35 | ->setName('test.com.') 36 | ->setType(RecordTypeEnum::TYPE_A) 37 | ->setQuestion(true); 38 | 39 | $expectation[] = (new ResourceRecord()) 40 | ->setName('test.com.') 41 | ->setType(RecordTypeEnum::TYPE_A) 42 | ->setTtl(300) 43 | ->setRdata('111.111.111.111'); 44 | 45 | $this->assertEquals($expectation, $this->resolver->getAnswer($question)); 46 | } 47 | 48 | /** 49 | * @throws \yswery\DNS\UnsupportedTypeException 50 | */ 51 | public function testIsWildcardDomain() 52 | { 53 | $input1 = '*.example.com.'; 54 | $input2 = '*.sub.domain.com.'; 55 | $input3 = '*'; 56 | $input4 = 'www.test.com.au.'; 57 | 58 | $resolver = new JsonResolver([]); 59 | 60 | $this->assertTrue($resolver->isWildcardDomain($input1)); 61 | $this->assertTrue($resolver->isWildcardDomain($input2)); 62 | $this->assertTrue($resolver->isWildcardDomain($input3)); 63 | $this->assertFalse($resolver->isWildcardDomain($input4)); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /tests/Resolver/StackableResolverTest.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 | */ 11 | 12 | namespace yswery\DNS\Tests\Resolver; 13 | 14 | use yswery\DNS\Resolver\JsonResolver; 15 | use yswery\DNS\Resolver\StackableResolver; 16 | use yswery\DNS\Resolver\XmlResolver; 17 | use yswery\DNS\Resolver\YamlResolver; 18 | 19 | class StackableResolverTest extends AbstractResolverTest 20 | { 21 | /** 22 | * @throws \Exception 23 | */ 24 | public function setUp() 25 | { 26 | $jsonFiles = [ 27 | __DIR__.'/../Resources/example.com.json', 28 | __DIR__.'/../Resources/test_records.json', 29 | ]; 30 | 31 | $xmlFiles = [ 32 | __DIR__.'/../Resources/example.com.xml', 33 | __DIR__.'/../Resources/test.com.xml', 34 | __DIR__.'/../Resources/test2.com.xml', 35 | ]; 36 | 37 | $ymlFiles = [ 38 | __DIR__.'/../Resources/records.yml', 39 | __DIR__.'/../Resources/example.com.yml', 40 | ]; 41 | 42 | $this->resolver = new StackableResolver([ 43 | new JsonResolver($jsonFiles), 44 | new XmlResolver($xmlFiles), 45 | new YamlResolver($ymlFiles), 46 | ]); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /tests/Resolver/SystemResolverTest.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 | */ 11 | 12 | namespace yswery\DNS\Tests\Resolver; 13 | 14 | use PHPUnit\Framework\TestCase; 15 | use yswery\DNS\RecordTypeEnum; 16 | use yswery\DNS\Resolver\SystemResolver; 17 | use yswery\DNS\ResourceRecord; 18 | 19 | class SystemResolverTest extends TestCase 20 | { 21 | /** 22 | * @throws \yswery\DNS\UnsupportedTypeException 23 | */ 24 | public function testGetAnswer() 25 | { 26 | $query1 = (new ResourceRecord()) 27 | ->setQuestion(true) 28 | ->setName('google-public-dns-a.google.com.') 29 | ->setType(RecordTypeEnum::TYPE_A); 30 | 31 | $expectation1 = '8.8.8.8'; 32 | 33 | $query2 = (new ResourceRecord()) 34 | ->setQuestion(true) 35 | ->setName('google-public-dns-a.google.com.') 36 | ->setType(RecordTypeEnum::TYPE_AAAA); 37 | 38 | $expectation2 = '2001:4860:4860::8888'; 39 | 40 | $query3 = (new ResourceRecord()) 41 | ->setQuestion(true) 42 | ->setName('google-public-dns-b.google.com.') 43 | ->setType(RecordTypeEnum::TYPE_A); 44 | 45 | $expectation3 = '8.8.4.4'; 46 | 47 | $query4 = (new ResourceRecord()) 48 | ->setQuestion(true) 49 | ->setName('google-public-dns-b.google.com.') 50 | ->setType(RecordTypeEnum::TYPE_AAAA); 51 | 52 | $expectation4 = '2001:4860:4860::8844'; 53 | 54 | $resolver = new SystemResolver(); 55 | 56 | $this->assertEquals($expectation1, $resolver->getAnswer([$query1])[0]->getRdata()); 57 | $this->assertEquals($expectation2, $resolver->getAnswer([$query2])[0]->getRdata()); 58 | $this->assertEquals($expectation3, $resolver->getAnswer([$query3])[0]->getRdata()); 59 | $this->assertEquals($expectation4, $resolver->getAnswer([$query4])[0]->getRdata()); 60 | } 61 | 62 | /** 63 | * @throws \yswery\DNS\UnsupportedTypeException 64 | */ 65 | public function testGetAnswerWithEmptyQuestion() 66 | { 67 | $resolver = new SystemResolver(); 68 | $this->assertEquals([], $resolver->getAnswer([])); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /tests/Resolver/XmlResolverTest.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 | */ 11 | 12 | namespace yswery\DNS\Tests\Resolver; 13 | 14 | use yswery\DNS\Resolver\XmlResolver; 15 | 16 | class XmlResolverTest extends AbstractResolverTest 17 | { 18 | /** 19 | * @throws \yswery\DNS\UnsupportedTypeException 20 | */ 21 | public function setUp() 22 | { 23 | $files = [ 24 | __DIR__.'/../Resources/example.com.xml', 25 | __DIR__.'/../Resources/test.com.xml', 26 | __DIR__.'/../Resources/test2.com.xml', 27 | ]; 28 | $this->resolver = new XmlResolver($files); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /tests/Resolver/YamlResolverTest.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 | */ 11 | 12 | namespace yswery\DNS\Tests\Resolver; 13 | 14 | use Symfony\Component\Yaml\Exception\ParseException; 15 | use yswery\DNS\RecordTypeEnum; 16 | use yswery\DNS\Resolver\YamlResolver; 17 | use yswery\DNS\ResourceRecord; 18 | 19 | class YamlResolverTest extends AbstractResolverTest 20 | { 21 | /** 22 | * @throws \Exception 23 | */ 24 | public function setUp() 25 | { 26 | $files = [ 27 | __DIR__.'/../Resources/records.yml', 28 | __DIR__.'/../Resources/example.com.yml', 29 | ]; 30 | $this->resolver = new YamlResolver($files); 31 | } 32 | 33 | /** 34 | * @throws \Exception 35 | */ 36 | public function testParseException() 37 | { 38 | $this->expectException(ParseException::class); 39 | new YamlResolver([__DIR__.'/../Resources/invalid_dns_records.json']); 40 | } 41 | 42 | public function testResolveLegacyRecord() 43 | { 44 | $question[] = (new ResourceRecord()) 45 | ->setName('test2.com.') 46 | ->setType(RecordTypeEnum::TYPE_MX) 47 | ->setQuestion(true); 48 | 49 | $expectation[] = (new ResourceRecord()) 50 | ->setName('test2.com.') 51 | ->setType(RecordTypeEnum::TYPE_MX) 52 | ->setTtl(300) 53 | ->setRdata([ 54 | 'preference' => 20, 55 | 'exchange' => 'mail-gw1.test2.com.', 56 | ]); 57 | 58 | $expectation[] = (new ResourceRecord()) 59 | ->setName('test2.com.') 60 | ->setType(RecordTypeEnum::TYPE_MX) 61 | ->setTtl(300) 62 | ->setRdata([ 63 | 'preference' => 30, 64 | 'exchange' => 'mail-gw2.test2.com.', 65 | ]); 66 | 67 | $this->assertEquals($expectation, $this->resolver->getAnswer($question)); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /tests/Resources/example.com-2.db: -------------------------------------------------------------------------------- 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/Resources/example.com.db: -------------------------------------------------------------------------------- 1 | $origin example.com. 2 | $ttl 7200 3 | 4 | @ IN 10800 SOA example.com. postmaster 2 3600 7200 10800 3600 5 | @ IN a 12.34.56.78 6 | @ IN ns ns1.test.com. 7 | @ IN ns ns2.test.com. 8 | @ IN a 90.12.34.56 9 | @ IN aaaa 2001:acad:ad::32 10 | 11 | mail-gw1 IN aaaa 2001:acad:ad::64 12 | mail-gw2 IN aaaa 2001:acad:ad::92 13 | 14 | www cname @ 15 | @ MX 15 mail-gw1 16 | @ MX 20 mail-gw2 17 | 18 | ldap a 192.168.3.89 19 | *.subdomain IN a 192.168.1.42 20 | 21 | _ldap._tcp SRV 1 5 389 ldap.example.com. 22 | -------------------------------------------------------------------------------- /tests/Resources/example.com.json: -------------------------------------------------------------------------------- 1 | { 2 | "domain": "example.com.", 3 | "default-ttl": 7200, 4 | "resource-records": [ 5 | { 6 | "name": "@", 7 | "ttl": 10800, 8 | "type": "SOA", 9 | "class": "IN", 10 | "mname": "example.com.", 11 | "rname": "postmaster", 12 | "serial": 2, 13 | "refresh": 3600, 14 | "retry": 7200, 15 | "expire": 10800, 16 | "minimum": 3600 17 | }, { 18 | "type": "A", 19 | "address": "12.34.56.78" 20 | },{ 21 | "type": "NS", 22 | "target": "ns1.test.com." 23 | },{ 24 | "type": "NS", 25 | "target": "ns2.test.com." 26 | }, { 27 | "type": "A", 28 | "address": "90.12.34.56" 29 | }, { 30 | "type": "AAAA", 31 | "address": "2001:acad:ad::32" 32 | }, { 33 | "name": "mail-gw1", 34 | "type": "AAAA", 35 | "address": "2001:acad:ad::64" 36 | }, { 37 | "name": "mail-gw2", 38 | "type": "AAAA", 39 | "address": "2001:acad:ad::92" 40 | }, { 41 | "name": "www", 42 | "type": "cname", 43 | "target": "@" 44 | }, { 45 | "name": "@", 46 | "type": "MX", 47 | "preference": 15, 48 | "exchange": "mail-gw1" 49 | }, { 50 | "name": "@", 51 | "type": "MX", 52 | "preference": 20, 53 | "exchange": "mail-gw2" 54 | }, { 55 | "name": "ldap", 56 | "type": "A", 57 | "address": "192.168.3.89" 58 | }, { 59 | "name": "*.subdomain", 60 | "type": "A", 61 | "address": "192.168.1.42" 62 | }, { 63 | "name": "_ldap._tcp", 64 | "type": "SRV", 65 | "priority": 1, 66 | "weight": 5, 67 | "port": 389, 68 | "target": "ldap.example.com." 69 | } 70 | ] 71 | } -------------------------------------------------------------------------------- /tests/Resources/example.com.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | example.com. 7 | 7200 8 | 9 | 10 | 11 | @ 12 | 10800 13 | SOA 14 | IN 15 | 16 | example.com. 17 | postmaster 18 | 2 19 | 3600 20 | 7200 21 | 10800 22 | 3600 23 | 24 | 25 | 26 | 27 | A 28 | 29 |
12.34.56.78
30 |
31 |
32 | 33 | 34 | A 35 | 36 |
90.12.34.56
37 |
38 |
39 | 40 | 41 | AAAA 42 | 43 |
2001:acad:ad::32
44 |
45 |
46 | 47 | 48 | www 49 | cname 50 | 51 | @ 52 | 53 | 54 | 55 | 56 | ldap 57 | A 58 | 59 |
192.168.3.89
60 |
61 |
62 | 63 | 64 | @ 65 | MX 66 | 67 | 15 68 | mail 69 | 70 | 71 | 72 | 73 | *.subdomain 74 | A 75 | 76 |
192.168.1.42
77 |
78 |
79 | 80 | 81 | _ldap._tcp 82 | SRV 83 | 84 | 1 85 | 5 86 | 389 87 | ldap 88 | 89 | 90 |
91 |
92 | -------------------------------------------------------------------------------- /tests/Resources/example.com.yml: -------------------------------------------------------------------------------- 1 | domain: example.com. 2 | default-ttl: 7200 3 | resource-records: 4 | - name: '@' 5 | ttl: 10800 6 | type: SOA 7 | class: IN 8 | mname: example.com. 9 | rname: postmaster 10 | serial: 2 11 | refresh: 3600 12 | retry: 7200 13 | expire: 10800 14 | minimum: 3600 15 | 16 | - type: A 17 | address: 12.34.56.78 18 | 19 | - type: A 20 | address: 90.12.34.56 21 | 22 | - type: AAAA 23 | address: 2001:acad:ad::32 24 | 25 | - name: www 26 | type: cname 27 | target: '@' 28 | 29 | - name: ldap 30 | type: A 31 | address: 192.168.3.89 32 | 33 | - name: '@' 34 | type: MX 35 | preference: 15 36 | exchange: mail 37 | 38 | - name: '*.subdomain' 39 | type: A 40 | address: 192.168.1.42 41 | 42 | - name: _ldap._tcp 43 | type: SRV 44 | priority: 1 45 | weight: 5 46 | port: 389 47 | target: ldap -------------------------------------------------------------------------------- /tests/Resources/invalid_dns_records.json: -------------------------------------------------------------------------------- 1 | { alkj } -------------------------------------------------------------------------------- /tests/Resources/records.yml: -------------------------------------------------------------------------------- 1 | --- 2 | test.com: 3 | A: 111.111.111.111 4 | MX: 5 | - exchange: mail-gw1.test.com 6 | preference: 10 7 | - exchange: mail-gw2.test.com 8 | preference: 20 9 | NS: 10 | - ns1.test.com 11 | - ns2.test.com 12 | TXT: Some text. 13 | AAAA: DEAD:01::BEEF 14 | CNAME: www2.test.com 15 | SOA: 16 | - mname: ns1.test.com 17 | rname: admin.test.com 18 | serial: '2014111100' 19 | retry: '7200' 20 | refresh: '1800' 21 | expire: '8600' 22 | minimum: '300' 23 | test2.com: 24 | A: 25 | - 111.111.111.111 26 | - 112.112.112.112 27 | MX: 28 | - preference: 20 29 | exchange: mail-gw1.test2.com. 30 | - preference: 30 31 | exchange: mail-gw2.test2.com. 32 | -------------------------------------------------------------------------------- /tests/Resources/test.com.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | test.com. 7 | 7200 8 | 9 | 10 | 11 | @ 12 | 10800 13 | SOA 14 | IN 15 | 16 | ns1 17 | admin 18 | 2014111100 19 | 1800 20 | 7200 21 | 8600 22 | 300 23 | 24 | 25 | 26 | 27 | A 28 | 29 |
111.111.111.111
30 |
31 |
32 | 33 | 34 | AAAA 35 | 36 |
DEAD:01::BEEF
37 |
38 |
39 | 40 | 41 | @ 42 | NS 43 | 44 | ns1 45 | 46 | 47 | 48 | 49 | @ 50 | NS 51 | 52 | ns2 53 | 54 | 55 | 56 | 57 | ns1 58 | A 59 | 60 |
15.15.12.10
61 |
62 |
63 | 64 | 65 | ns2 66 | A 67 | 68 |
15.15.12.20
69 |
70 |
71 | 72 | 73 | @ 74 | cname 75 | 76 | ww2 77 | 78 | 79 | 80 | 81 | MX 82 | 83 | 10 84 | mail-gw1 85 | 86 | 87 | 88 | 89 | MX 90 | 91 | 20 92 | mail-gw2 93 | 94 | 95 | 96 | 97 | TXT 98 | 99 | Some text. 100 | 101 | 102 |
103 |
-------------------------------------------------------------------------------- /tests/Resources/test2.com.db: -------------------------------------------------------------------------------- 1 | $origin test2.com. 2 | $ttl 300 3 | 4 | @ IN A 111.111.111.111 5 | @ IN A 112.112.112.112 6 | @ IN MX 20 mail-gw1.test2.com. 7 | @ IN MX 30 mail-gw2.test2.com. 8 | -------------------------------------------------------------------------------- /tests/Resources/test2.com.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | test2.com. 7 | 300 8 | 9 | 10 | 11 | @ 12 | 10800 13 | SOA 14 | IN 15 | 16 | ns1 17 | admin 18 | 2014111100 19 | 1800 20 | 7200 21 | 8600 22 | 300 23 | 24 | 25 | 26 | 27 | A 28 | 29 |
111.111.111.111
30 |
31 |
32 | 33 | 34 | A 35 | 36 |
112.112.112.112
37 |
38 |
39 | 40 | 41 | MX 42 | 43 | 20 44 | mail-gw1 45 | 46 | 47 | 48 | 49 | MX 50 | 51 | 30 52 | mail-gw2 53 | 54 | 55 | 56 | 57 | TXT 58 | 59 | Some text. 60 | 61 | 62 |
63 |
-------------------------------------------------------------------------------- /tests/Resources/test_records.json: -------------------------------------------------------------------------------- 1 | { 2 | "test.com": { 3 | "A": "111.111.111.111", 4 | "MX": [ 5 | { 6 | "exchange": "mail-gw1.test.com", 7 | "preference": 10 8 | }, 9 | { 10 | "exchange": "mail-gw2.test.com", 11 | "preference": 20 12 | } 13 | ], 14 | "NS": [ 15 | "ns1.test.com", 16 | "ns2.test.com" 17 | ], 18 | "TXT": "Some text.", 19 | "AAAA": "DEAD:01::BEEF", 20 | "CNAME": "www2.test.com", 21 | "SOA": [ 22 | { 23 | "mname": "ns1.test.com", 24 | "rname": "admin.test.com", 25 | "serial": "2014111100", 26 | "retry": "7200", 27 | "refresh": "1800", 28 | "expire": "8600", 29 | "minimum": "300" 30 | } 31 | ] 32 | }, 33 | "test2.com": { 34 | "A": [ 35 | "111.111.111.111", 36 | "112.112.112.112" 37 | ], 38 | "MX": [ 39 | { 40 | "preference": 20, 41 | "exchange": "mail-gw1.test2.com." 42 | }, 43 | { 44 | "preference": 30, 45 | "exchange": "mail-gw2.test2.com." 46 | } 47 | ] 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /tests/ServerTest.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 | */ 11 | 12 | namespace yswery\DNS\Tests; 13 | 14 | use PHPUnit\Framework\TestCase; 15 | use Symfony\Component\EventDispatcher\EventDispatcher; 16 | use yswery\DNS\ClassEnum; 17 | use yswery\DNS\Decoder; 18 | use yswery\DNS\Encoder; 19 | use yswery\DNS\Header; 20 | use yswery\DNS\Message; 21 | use yswery\DNS\RecordTypeEnum; 22 | use yswery\DNS\Resolver\JsonResolver; 23 | use yswery\DNS\Resolver\StackableResolver; 24 | use yswery\DNS\Resolver\XmlResolver; 25 | use yswery\DNS\ResourceRecord; 26 | use yswery\DNS\Server; 27 | 28 | class ServerTest extends TestCase 29 | { 30 | /** 31 | * @var Server 32 | */ 33 | private $server; 34 | 35 | /** 36 | * @throws \Exception 37 | */ 38 | public function setUp() 39 | { 40 | $xmlResolver = new XmlResolver([ 41 | __DIR__.'/Resources/test.com.xml', 42 | ]); 43 | 44 | $jsonResolver = new JsonResolver([ 45 | __DIR__.'/Resources/test_records.json', 46 | __DIR__.'/Resources/example.com.json', 47 | ]); 48 | 49 | $resolver = new StackableResolver([ 50 | $jsonResolver, 51 | $xmlResolver, 52 | ]); 53 | 54 | $this->server = new Server($resolver, new EventDispatcher(), null, null, false); 55 | } 56 | 57 | /** 58 | * @param $name 59 | * @param $type 60 | * @param $id 61 | * 62 | * @return array 63 | */ 64 | private function encodeQuery($name, $type, $id) 65 | { 66 | $qname = Encoder::encodeDomainName($name); 67 | $flags = 0b0000000000000000; 68 | $header = pack('nnnnnn', $id, $flags, 1, 0, 0, 0); 69 | $question = $qname.pack('nn', $type, 1); 70 | 71 | return [$header, $question]; 72 | } 73 | 74 | /** 75 | * Create a mock query and response pair. 76 | * 77 | * @return array 78 | */ 79 | private function mockQueryAndResponse(): array 80 | { 81 | list($queryHeader, $question) = $this->encodeQuery($name = 'test.com.', RecordTypeEnum::TYPE_A, $id = 1337); 82 | $query = $queryHeader.$question; 83 | 84 | $flags = 0b1000010000000000; 85 | $qname = Encoder::encodeDomainName($name); 86 | $header = pack('nnnnnn', $id, $flags, 1, 1, 0, 0); 87 | 88 | $rdata = inet_pton('111.111.111.111'); 89 | $answer = $qname.pack('nnNn', 1, 1, 300, strlen($rdata)).$rdata; 90 | 91 | $response = $header.$question.$answer; 92 | 93 | return [$query, $response]; 94 | } 95 | 96 | /** 97 | * @throws \yswery\DNS\UnsupportedTypeException 98 | */ 99 | public function testHandleQueryFromStream() 100 | { 101 | list($query, $response) = $this->mockQueryAndResponse(); 102 | 103 | $this->assertEquals($response, $this->server->handleQueryFromStream($query)); 104 | } 105 | 106 | public function testStatusQueryWithNoQuestionsResolves() 107 | { 108 | $message = new Message(); 109 | $message->getHeader() 110 | ->setOpcode(Header::OPCODE_STATUS_REQUEST) 111 | ->setId(1234); 112 | 113 | $encodedMessage = Encoder::encodeMessage($message); 114 | 115 | $message->getHeader()->setResponse(true); 116 | $expectation = Encoder::encodeMessage($message); 117 | 118 | $this->assertEquals($expectation, $this->server->handleQueryFromStream($encodedMessage)); 119 | } 120 | 121 | /** 122 | * Tests that the server sends back a "Not implemented" RCODE for a type that has not been implemented, namely "OPT". 123 | * 124 | * @throws \yswery\DNS\UnsupportedTypeException | \Exception 125 | */ 126 | public function testOptType() 127 | { 128 | $q_RR = (new ResourceRecord()) 129 | ->setName('test.com.') 130 | ->setType(RecordTypeEnum::TYPE_OPT) 131 | ->setClass(ClassEnum::INTERNET) 132 | ->setQuestion(true); 133 | 134 | $query = new Message(); 135 | $query->setQuestions([$q_RR]) 136 | ->getHeader() 137 | ->setQuery(true) 138 | ->setId($id = 1337); 139 | 140 | $response = new Message(); 141 | $response->setQuestions([$q_RR]) 142 | ->getHeader() 143 | ->setId($id) 144 | ->setResponse(true) 145 | ->setRcode(Header::RCODE_NOT_IMPLEMENTED) 146 | ->setAuthoritative(true); 147 | 148 | $queryEncoded = Encoder::encodeMessage($query); 149 | $responseEncoded = Encoder::encodeMessage($response); 150 | 151 | $server = new Server(new DummyResolver(), new EventDispatcher()); 152 | $this->assertEquals($responseEncoded, $server->handleQueryFromStream($queryEncoded)); 153 | } 154 | 155 | public function testOnMessage() 156 | { 157 | list($query, $response) = $this->mockQueryAndResponse(); 158 | $this->server->onMessage($query, '127.0.0.1', $socket = new MockSocket()); 159 | 160 | $this->assertEquals($response, $socket->getLastTransmission()); 161 | } 162 | 163 | /** 164 | * Certain queries such as SRV, SOA, and NS records SHOULD return additional records in order to prevent 165 | * unnecessary additional requests. 166 | * 167 | * @throws \yswery\DNS\UnsupportedTypeException 168 | */ 169 | public function testSrvAdditionalRecords() 170 | { 171 | $queryHeader = (new Header()) 172 | ->setQuery(true) 173 | ->setOpcode(Header::OPCODE_STANDARD_QUERY) 174 | ->setId(1234); 175 | 176 | $queryRecord = (new ResourceRecord()) 177 | ->setQuestion(true) 178 | ->setName('_ldap._tcp.example.com.') 179 | ->setType(RecordTypeEnum::TYPE_SRV); 180 | 181 | $message = (new Message()) 182 | ->setHeader($queryHeader) 183 | ->addQuestion($queryRecord); 184 | 185 | $query = Encoder::encodeMessage($message); 186 | $this->server->onMessage($query, '127.0.0.1', $socket = new MockSocket()); 187 | $encodedResponse = $socket->getLastTransmission(); 188 | $response = Decoder::decodeMessage($encodedResponse); 189 | 190 | $this->assertEquals(1, $response->getHeader()->getAnswerCount()); 191 | $this->assertEquals(1, $response->getHeader()->getAdditionalRecordsCount()); 192 | $this->assertEquals('192.168.3.89', $response->getAdditionals()[0]->getRdata()); 193 | } 194 | 195 | /** 196 | * @throws \yswery\DNS\UnsupportedTypeException 197 | */ 198 | public function testMxAdditionalRecords() 199 | { 200 | $queryHeader = (new Header()) 201 | ->setQuery(true) 202 | ->setOpcode(Header::OPCODE_STANDARD_QUERY) 203 | ->setId(1234); 204 | 205 | $mxQuestion = (new ResourceRecord()) 206 | ->setQuestion(true) 207 | ->setType(RecordTypeEnum::TYPE_MX) 208 | ->setName('example.com.'); 209 | 210 | $message = (new Message()) 211 | ->setHeader($queryHeader) 212 | ->addQuestion($mxQuestion); 213 | 214 | $query = Encoder::encodeMessage($message); 215 | $this->server->onMessage($query, '127.0.0.1', $socket = new MockSocket()); 216 | $encodedResponse = $socket->getLastTransmission(); 217 | $response = Decoder::decodeMessage($encodedResponse); 218 | 219 | $this->assertEquals(2, $response->getHeader()->getAnswerCount()); 220 | $this->assertEquals(2, $response->getHeader()->getAdditionalRecordsCount()); 221 | } 222 | 223 | /** 224 | * @throws \yswery\DNS\UnsupportedTypeException 225 | */ 226 | public function testNsAdditionalRecords() 227 | { 228 | $queryHeader = (new Header()) 229 | ->setQuery(true) 230 | ->setOpcode(Header::OPCODE_STANDARD_QUERY) 231 | ->setId(1234); 232 | 233 | $nsQuestion = (new ResourceRecord()) 234 | ->setQuestion(true) 235 | ->setType(RecordTypeEnum::TYPE_NS) 236 | ->setName('example.com.'); 237 | 238 | $message = (new Message()) 239 | ->setHeader($queryHeader) 240 | ->addQuestion($nsQuestion); 241 | 242 | $query = Encoder::encodeMessage($message); 243 | $this->server->onMessage($query, '127.0.0.1', $socket = new MockSocket()); 244 | $encodedResponse = $socket->getLastTransmission(); 245 | $response = Decoder::decodeMessage($encodedResponse); 246 | 247 | $this->assertEquals(2, $response->getHeader()->getAnswerCount()); 248 | $this->assertEquals(2, $response->getHeader()->getAdditionalRecordsCount()); 249 | } 250 | } 251 | -------------------------------------------------------------------------------- /vendor-bin/box/composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "require-dev": { 3 | "humbug/box": "^3.8" 4 | } 5 | } 6 | --------------------------------------------------------------------------------