From 934cfc5fa776e4c8f40a232d91a843c4075cb380 Mon Sep 17 00:00:00 2001 From: Eric Hocking Date: Mon, 23 Sep 2019 14:19:35 -0400 Subject: [PATCH 1/8] add support for running PhpDnsServer from command line with a filesystem resolver --- bin/PhpDnsConsole.php | 13 ++ bin/PhpDnsServer.php | 30 ++++ composer.json | 12 +- console.box.json | 13 ++ server.box.json | 13 ++ src/Console/CommandServer.php | 22 +++ src/Console/Commands/VersionCommand.php | 24 +++ src/Exception/InvalidZoneFileException.php | 10 ++ src/Exception/ZoneFileNotFoundException.php | 10 ++ src/Filesystem/FilesystemManager.php | 91 +++++++++++ src/Resolver/AbstractResolver.php | 13 ++ src/Resolver/JsonFileSystemResolver.php | 163 ++++++++++++++++++++ src/Server.php | 38 ++++- vendor-bin/box/composer.json | 5 + 14 files changed, 454 insertions(+), 3 deletions(-) create mode 100644 bin/PhpDnsConsole.php create mode 100644 bin/PhpDnsServer.php create mode 100644 console.box.json create mode 100644 server.box.json create mode 100644 src/Console/CommandServer.php create mode 100644 src/Console/Commands/VersionCommand.php create mode 100644 src/Exception/InvalidZoneFileException.php create mode 100644 src/Exception/ZoneFileNotFoundException.php create mode 100644 src/Filesystem/FilesystemManager.php create mode 100644 src/Resolver/JsonFileSystemResolver.php create mode 100644 vendor-bin/box/composer.json diff --git a/bin/PhpDnsConsole.php b/bin/PhpDnsConsole.php new file mode 100644 index 0000000..4046a42 --- /dev/null +++ b/bin/PhpDnsConsole.php @@ -0,0 +1,13 @@ +addSubscriber(new \yswery\DNS\Event\Subscriber\EchoLogger()); + +// Create a new instance of Server class +$server = new yswery\DNS\Console\CommandServer(); + +// Start DNS server +$server->run(); \ No newline at end of file diff --git a/bin/PhpDnsServer.php b/bin/PhpDnsServer.php new file mode 100644 index 0000000..ffc1627 --- /dev/null +++ b/bin/PhpDnsServer.php @@ -0,0 +1,30 @@ +opt('bind:b', 'Bind to a specific ip interface', false) + ->opt('port:p', 'specify the port to bind to', false); + +$args = $cli->parse($argv, true); + +// defaults +$host = $args->getOpt('bind', '0.0.0.0'); +$port = $args->getOpt('port', 53); + + +// Parse and return cli args. + +// Create the eventDispatcher and add the event subscribers +$eventDispatcher = new \Symfony\Component\EventDispatcher\EventDispatcher(); +$eventDispatcher->addSubscriber(new \yswery\DNS\Event\Subscriber\EchoLogger()); + +// Create a new instance of Server class +$server = new yswery\DNS\Server(null, $eventDispatcher, true, $host, $port); + +// Start DNS server +$server->run(); diff --git a/composer.json b/composer.json index a057fd7..61ca4d0 100644 --- a/composer.json +++ b/composer.json @@ -3,7 +3,8 @@ "description": "A DNS server implementation in pure PHP.", "require-dev": { "phpunit/phpunit": "~7.3", - "php-coveralls/php-coveralls": "~2.1" + "php-coveralls/php-coveralls": "~2.1", + "bamarni/composer-bin-plugin": "^1.3" }, "license": "MIT", "authors": [ @@ -31,7 +32,10 @@ "ext-json": "~1.0", "ext-simplexml": "*", "symfony/event-dispatcher": "~4.0", - "psr/log": "^1.0" + "psr/log": "^1.0", + "symfony/filesystem": "^4.3", + "symfony/console": "^4.3", + "vanilla/garden-cli": "^2.2" }, "autoload": { "psr-4": { @@ -42,5 +46,9 @@ "psr-4": { "yswery\\DNS\\Tests\\": "tests/" } + }, + "scripts": { + "build-server": "box compile -c server.box.json", + "build-console": "box compile -c console.box.json" } } diff --git a/console.box.json b/console.box.json new file mode 100644 index 0000000..a0f1111 --- /dev/null +++ b/console.box.json @@ -0,0 +1,13 @@ +{ + "chmod": "0755", + "main": "bin/PhpDnsConsole.php", + "output": "bin/phpdnscli.phar", + "directories": ["src"], + "finder": [ + { + "name": "*.php", + "exclude": ["test", "tests"], + "in": "vendor" + } + ], "stub": true +} \ No newline at end of file diff --git a/server.box.json b/server.box.json new file mode 100644 index 0000000..29f40db --- /dev/null +++ b/server.box.json @@ -0,0 +1,13 @@ +{ + "chmod": "0755", + "main": "bin/PhpDnsServer.php", + "output": "bin/phpdnsserver.phar", + "directories": ["src"], + "finder": [ + { + "name": "*.php", + "exclude": ["test", "tests"], + "in": "vendor" + } + ], "stub": true +} \ No newline at end of file diff --git a/src/Console/CommandServer.php b/src/Console/CommandServer.php new file mode 100644 index 0000000..b5ca0f1 --- /dev/null +++ b/src/Console/CommandServer.php @@ -0,0 +1,22 @@ +registerCommands(); + } + + protected function registerCommands() + { + $this->add(new VersionCommand()); + } +} \ No newline at end of file diff --git a/src/Console/Commands/VersionCommand.php b/src/Console/Commands/VersionCommand.php new file mode 100644 index 0000000..a36f734 --- /dev/null +++ b/src/Console/Commands/VersionCommand.php @@ -0,0 +1,24 @@ +setDescription('Shows the current PhpDnsServer version') + ->setHelp('Shows the current PhpDnsServer version'); + } + + protected function execute(InputInterface $input, OutputInterface $output) + { + $output->writeln('PowerDnsServer version '.Server::VERSION); + } +} \ No newline at end of file diff --git a/src/Exception/InvalidZoneFileException.php b/src/Exception/InvalidZoneFileException.php new file mode 100644 index 0000000..545fc2c --- /dev/null +++ b/src/Exception/InvalidZoneFileException.php @@ -0,0 +1,10 @@ +setBasePath($basePath); + } + + if ($zonePath) + { + $this->setZonePath($zonePath); + } + + + $this->registerFilesystem(); + } + + protected function registerFilesystem() + { + $this->filesystem = new Filesystem(); + + // make sure our directories exist + if (!$this->filesystem->exists($this->zonePath())) + { + try { + $this->filesystem->mkdir($this->zonePath(), 0700); + } catch (IOExceptionInterface $e) { + // todo: implement logging functions + } + + } + + } + + public function setBasePath($basePath) + { + $this->basePath = rtrim($basePath, '\/'); + return $this; + } + + public function basePath() + { + return $this->basePath; + } + + public function setZonePath($zonePath) + { + $this->zonePath = rtrim($zonePath, '\/'); + return $this; + } + + public function zonePath() + { + return $this->zonePath ?: $this->basePath.DIRECTORY_SEPARATOR.'zones'; + } + + /** + * @param string $zone + * @return JsonFileSystemResolver + * @throws \yswery\DNS\UnsupportedTypeException + */ + public function getZone(string $zone) + { + $zoneFile = $this->basePath().DIRECTORY_SEPARATOR.$zone.'.json'; + return new JsonFileSystemResolver($zoneFile); + } +} \ No newline at end of file diff --git a/src/Resolver/AbstractResolver.php b/src/Resolver/AbstractResolver.php index 57783c7..fb07c25 100644 --- a/src/Resolver/AbstractResolver.php +++ b/src/Resolver/AbstractResolver.php @@ -27,6 +27,11 @@ abstract class AbstractResolver implements ResolverInterface */ protected $isAuthoritative; + /** + * @var bool + */ + protected $supportsSaving = false; + /** * @var ResourceRecord[] */ @@ -76,6 +81,14 @@ public function isAuthority($domain): bool return $this->isAuthoritative; } + /** + * {@inheritDoc} + */ + public function supportsSaving() + { + return $this->supportsSaving; + } + /** * Determine if a domain is a wildcard domain. * diff --git a/src/Resolver/JsonFileSystemResolver.php b/src/Resolver/JsonFileSystemResolver.php new file mode 100644 index 0000000..8d61fd3 --- /dev/null +++ b/src/Resolver/JsonFileSystemResolver.php @@ -0,0 +1,163 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace yswery\DNS\Resolver; + +use yswery\DNS\ClassEnum; +use yswery\DNS\Exception\ZoneFileNotFoundException; +use yswery\DNS\Filesystem\FilesystemManager; +use yswery\DNS\RecordTypeEnum; +use yswery\DNS\ResourceRecord; +use yswery\DNS\UnsupportedTypeException; + +class JsonFileSystemResolver extends AbstractResolver +{ + /** + * @var int + */ + protected $defaultClass = ClassEnum::INTERNET; + + /** + * @var FilesystemManager + */ + protected $filesystemManager; + + /** + * @var int + */ + protected $defaultTtl; + + /** + * JsonResolver constructor. + * + * @param FilesystemManager $filesystemManager + * @param int $defaultTtl + * + * @throws UnsupportedTypeException + */ + public function __construct(FilesystemManager $filesystemManager, $defaultTtl = 300) + { + $this->isAuthoritative = true; + $this->allowRecursion = false; + $this->filesystemManager = $filesystemManager; + $this->defaultTtl = $defaultTtl; + + $zones = glob($filesystemManager->zonePath()."/*.json"); + foreach ($zones as $file) + { + $zone = json_decode(file_get_contents($file), true); + $resourceRecords = $this->isLegacyFormat($zone) ? $this->processLegacyZone($zone) : $this->processZone($zone); + $this->addZone($resourceRecords); + } + } + + /** + * Load a zone file + * + * @param string $file + * @throws UnsupportedTypeException + * @throws ZoneFileNotFoundException + */ + public function loadZone($file) { + if (file_exists($file)) + { + $zone = json_decode(file_get_contents($file), true); + $resourceRecords = $this->isLegacyFormat($zone) ? $this->processLegacyZone($zone) : $this->processZone($zone); + $this->addZone($resourceRecords); + } else { + throw new ZoneFileNotFoundException("The zone file could not be found"); + } + } + + + /** + * Saves the zone to a .json file + * @param string $zone + */ + public function saveZone($zone) + { + + } + + /** + * @param array $zone + * + * @return ResourceRecord[] + * + * @throws UnsupportedTypeException + */ + protected function processZone(array $zone): array + { + $parent = rtrim($zone['domain'], '.').'.'; + $defaultTtl = $zone['default-ttl']; + $rrs = $zone['resource-records']; + $resourceRecords = []; + + foreach ($rrs as $rr) { + $name = $rr['name'] ?? $parent; + $class = isset($rr['class']) ? ClassEnum::getClassFromName($rr['class']) : $this->defaultClass; + + $resourceRecords[] = (new ResourceRecord()) + ->setName($this->handleName($name, $parent)) + ->setClass($class) + ->setType($type = RecordTypeEnum::getTypeFromName($rr['type'])) + ->setTtl($rr['ttl'] ?? $defaultTtl) + ->setRdata($this->extractRdata($rr, $type, $parent)); + } + + return $resourceRecords; + } + + /** + * Determine if a $zone is in the legacy format. + * + * @param array $zone + * + * @return bool + */ + protected function isLegacyFormat(array $zone): bool + { + $keys = array_map(function ($value) { + return strtolower($value); + }, array_keys($zone)); + + return + (false === array_search('domain', $keys, true)) || + (false === array_search('resource-records', $keys, true)); + } + + /** + * @param array $zones + * + * @return array + */ + protected function processLegacyZone(array $zones): array + { + $resourceRecords = []; + foreach ($zones as $domain => $types) { + $domain = rtrim($domain, '.').'.'; + foreach ($types as $type => $data) { + $data = (array) $data; + $type = RecordTypeEnum::getTypeFromName($type); + foreach ($data as $rdata) { + $resourceRecords[] = (new ResourceRecord()) + ->setName($domain) + ->setType($type) + ->setClass($this->defaultClass) + ->setTtl($this->defaultTtl) + ->setRdata($rdata); + } + } + } + + return $resourceRecords; + } +} diff --git a/src/Server.php b/src/Server.php index 21c8d24..958ac7e 100644 --- a/src/Server.php +++ b/src/Server.php @@ -14,6 +14,8 @@ use React\Datagram\Socket; use React\Datagram\SocketInterface; use React\EventLoop\LoopInterface; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\EventDispatcher\EventDispatcherInterface; use Symfony\Component\EventDispatcher\Event; use yswery\DNS\Event\ServerExceptionEvent; @@ -21,11 +23,19 @@ use yswery\DNS\Event\QueryReceiveEvent; use yswery\DNS\Event\QueryResponseEvent; use yswery\DNS\Event\ServerStartEvent; +use yswery\DNS\Resolver\JsonFileSystemResolver; use yswery\DNS\Resolver\ResolverInterface; use yswery\DNS\Event\Events; +use yswery\DNS\Filesystem\FilesystemManager; class Server { + /** + * The version of PhpDnsServer we are running + * @var string + */ + const VERSION = "1.4.0"; + /** * @var EventDispatcherInterface */ @@ -51,17 +61,28 @@ class Server */ private $loop; + /** + * @var FilesystemManager + */ + private $filesystemManager; + + /** + * @var bool + */ + private $useFilesystem; + /** * Server constructor. * * @param ResolverInterface $resolver * @param EventDispatcherInterface $dispatcher + * @param bool $useFilesystem * @param string $ip * @param int $port * * @throws \Exception */ - public function __construct(ResolverInterface $resolver, ?EventDispatcherInterface $dispatcher = null, string $ip = '0.0.0.0', int $port = 53) + public function __construct(ResolverInterface $resolver = null, ?EventDispatcherInterface $dispatcher = null, bool $useFilesystem = false, string $ip = '0.0.0.0', int $port = 53) { if (!function_exists('socket_create') || !extension_loaded('sockets')) { throw new \Exception('Socket extension or socket_create() function not found.'); @@ -72,6 +93,14 @@ public function __construct(ResolverInterface $resolver, ?EventDispatcherInterfa $this->port = $port; $this->ip = $ip; + // setup file system manger + $this->filesystemManager = new FilesystemManager(getcwd()); + $this->useFilesystem = $useFilesystem; + + if ($useFilesystem) { + $this->resolver = new JsonFileSystemResolver($this->filesystemManager); + } + $this->loop = \React\EventLoop\Factory::create(); $factory = new \React\Datagram\Factory($this->loop); $factory->createServer($this->ip.':'.$this->port)->then(function (Socket $server) { @@ -82,6 +111,7 @@ public function __construct(ResolverInterface $resolver, ?EventDispatcherInterfa }); } + /** * Start the server. */ @@ -91,6 +121,11 @@ public function start(): void $this->loop->run(); } + public function run() + { + $this->start(); + } + /** * This methods gets called each time a query is received. * @@ -122,6 +157,7 @@ public function handleQueryFromStream(string $buffer): string $message = Decoder::decodeMessage($buffer); $this->dispatch(Events::QUERY_RECEIVE, new QueryReceiveEvent($message)); + $responseMessage = clone $message; $responseMessage->getHeader() ->setResponse(true) diff --git a/vendor-bin/box/composer.json b/vendor-bin/box/composer.json new file mode 100644 index 0000000..cf49d4c --- /dev/null +++ b/vendor-bin/box/composer.json @@ -0,0 +1,5 @@ +{ + "require-dev": { + "humbug/box": "^3.8" + } +} From 7b74ea0b3522ab49499f9ca5b9eb8d334ae9c147 Mon Sep 17 00:00:00 2001 From: Eric Hocking Date: Mon, 23 Sep 2019 14:30:26 -0400 Subject: [PATCH 2/8] default to current directory on windows and /etc/phpdnsserver directory on unix --- src/Server.php | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/src/Server.php b/src/Server.php index 958ac7e..ffad665 100644 --- a/src/Server.php +++ b/src/Server.php @@ -71,6 +71,11 @@ class Server */ private $useFilesystem; + /** + * @var bool + */ + private $isWindows; + /** * Server constructor. * @@ -93,7 +98,16 @@ public function __construct(ResolverInterface $resolver = null, ?EventDispatcher $this->port = $port; $this->ip = $ip; - // setup file system manger + // detect os and setup file manager, default to working directory on windows + if (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') { + $this->isWindows = true; + $this->filesystemManager = new FilesystemManager(getcwd()); + } else { + // default to /etc/phpdnsserver on unix + $this->isWindows = false; + $this->filesystemManager = new FilesystemManager("/etc/phpdnsserver/"); + } + $this->filesystemManager = new FilesystemManager(getcwd()); $this->useFilesystem = $useFilesystem; From e2378a73e53742f329522d9510814100c828e88a Mon Sep 17 00:00:00 2001 From: Eric Hocking Date: Tue, 24 Sep 2019 09:13:36 -0400 Subject: [PATCH 3/8] implement fileconfig reading and saving --- bin/PhpDnsServer.php | 47 +++++++++++-- src/Config/FileConfig.php | 67 +++++++++++++++++++ src/Config/RecursiveArrayObject.php | 21 ++++++ src/Exception/ConfigFileNotFoundException.php | 10 +++ src/Server.php | 16 +++-- 5 files changed, 151 insertions(+), 10 deletions(-) create mode 100644 src/Config/FileConfig.php create mode 100644 src/Config/RecursiveArrayObject.php create mode 100644 src/Exception/ConfigFileNotFoundException.php diff --git a/bin/PhpDnsServer.php b/bin/PhpDnsServer.php index ffc1627..2d9584d 100644 --- a/bin/PhpDnsServer.php +++ b/bin/PhpDnsServer.php @@ -4,11 +4,14 @@ use Garden\Cli\Cli; +// parse cli args $cli = new Cli(); $cli ->opt('bind:b', 'Bind to a specific ip interface', false) - ->opt('port:p', 'specify the port to bind to', false); + ->opt('port:p', 'specify the port to bind to', false) + ->opt('config:c', 'specify the path to the phpdns.json file', false) + ->opt('storage:s', 'specify the location to zone storage folder', false); $args = $cli->parse($argv, true); @@ -16,15 +19,47 @@ $host = $args->getOpt('bind', '0.0.0.0'); $port = $args->getOpt('port', 53); +// figure out config location +if (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') { + // default to current working directory on windows + $configFile = $args->getOpt('config', getcwd() . '/phpdns.json'); + $storageDirectory = $args->getOpt('storage', getcwd()); +} else { + // default to /etc/phpdns.json and /etc/phpdnsserver if not on windows + $configFile = $args->getOpt('config', '/etc/phpdns.json'); + $storageDirectory = $args->getOpt('storage', '/etc/phpdnserver'); +} -// Parse and return cli args. +// initialize the configuration +$config = new yswery\DNS\Config\FileConfig($configFile); +try { + $config->load(); + + if ($config->has('host')) { + $host = $config->get('host'); + } + + if ($config->has('port')) { + $port = $config->get('port'); + } + + if ($config->has('storage')) { + $storageDirectory = $config->get('storage'); + } +} catch (\yswery\DNS\Exception\ConfigFileNotFoundException $e) { + echo $e->getMessage(); +} // Create the eventDispatcher and add the event subscribers $eventDispatcher = new \Symfony\Component\EventDispatcher\EventDispatcher(); $eventDispatcher->addSubscriber(new \yswery\DNS\Event\Subscriber\EchoLogger()); -// Create a new instance of Server class -$server = new yswery\DNS\Server(null, $eventDispatcher, true, $host, $port); +try { + // Create a new instance of Server class + $server = new yswery\DNS\Server(null, $eventDispatcher, $config, $storageDirectory, true, $host, $port); -// Start DNS server -$server->run(); + // Start DNS server + $server->run(); +} catch (\Exception $e) { + echo $e->getMessage(); +} diff --git a/src/Config/FileConfig.php b/src/Config/FileConfig.php new file mode 100644 index 0000000..8f435c8 --- /dev/null +++ b/src/Config/FileConfig.php @@ -0,0 +1,67 @@ +configFile = $configFile; + } + + /** + * @throws ConfigFileNotFoundException + */ + public function load() + { + // make sure the file exists before loading the config + if (file_exists($this->configFile)) { + $data = file_get_contents($this->configFile); + $this->config = json_decode($data); + } else { + throw new ConfigFileNotFoundException("Config file not found."); + } + } + + public function save() + { + file_put_contents($this->configFile, json_encode($this->config)); + } + + public function get($key, $default = null) + { + if (isset($this->config->{$key})) { + return $this->config->{$key}; + } + + return $default; + } + + public function set(array $data) + { + $configObject = new RecursiveArrayObject($this->config); + $configArray = $configObject->getArrayCopy(); + + //$origional = json_decode(json_encode($this->config), true); + $new = array_merge($configArray, $data); + + $this->config = json_decode(json_encode($new), false); + } + + public function has($key) + { + return isset($this->config->{$key}); + } +} \ No newline at end of file diff --git a/src/Config/RecursiveArrayObject.php b/src/Config/RecursiveArrayObject.php new file mode 100644 index 0000000..64e6c8b --- /dev/null +++ b/src/Config/RecursiveArrayObject.php @@ -0,0 +1,21 @@ + $val) { + if (!is_object($val)) { + continue; + } + $o = new RecursiveArrayObject($val); + $resultArray[$key] = $o->getArrayCopy(); + } + return $resultArray; + } +} \ No newline at end of file diff --git a/src/Exception/ConfigFileNotFoundException.php b/src/Exception/ConfigFileNotFoundException.php new file mode 100644 index 0000000..c9c2ecf --- /dev/null +++ b/src/Exception/ConfigFileNotFoundException.php @@ -0,0 +1,10 @@ +dispatcher = $dispatcher; $this->resolver = $resolver; + $this->config = $config; $this->port = $port; $this->ip = $ip; // detect os and setup file manager, default to working directory on windows if (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') { $this->isWindows = true; - $this->filesystemManager = new FilesystemManager(getcwd()); + $this->filesystemManager = new FilesystemManager($storageDirectory); } else { // default to /etc/phpdnsserver on unix $this->isWindows = false; - $this->filesystemManager = new FilesystemManager("/etc/phpdnsserver/"); + $this->filesystemManager = new FilesystemManager($storageDirectory); } - $this->filesystemManager = new FilesystemManager(getcwd()); $this->useFilesystem = $useFilesystem; if ($useFilesystem) { From 23e8726b7368bc3dbad2a9d6be1bff7158e092b9 Mon Sep 17 00:00:00 2001 From: Eric Hocking Date: Tue, 24 Sep 2019 09:19:01 -0400 Subject: [PATCH 4/8] fix ServerTest --- tests/ServerTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/ServerTest.php b/tests/ServerTest.php index 05d55cf..a0826d6 100644 --- a/tests/ServerTest.php +++ b/tests/ServerTest.php @@ -51,7 +51,7 @@ public function setUp() $xmlResolver, ]); - $this->server = new Server($resolver, new EventDispatcher()); + $this->server = new Server($resolver, new EventDispatcher(), null, null, false); } /** From 4330ec41050705e718b8f8c6d74d3acb4aa17074 Mon Sep 17 00:00:00 2001 From: Eric Hocking Date: Tue, 24 Sep 2019 09:23:19 -0400 Subject: [PATCH 5/8] Server constructor cleanup, only register filesystem if needed --- src/Server.php | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/src/Server.php b/src/Server.php index 7a83391..7036056 100644 --- a/src/Server.php +++ b/src/Server.php @@ -88,14 +88,14 @@ class Server * @param ResolverInterface $resolver * @param EventDispatcherInterface $dispatcher * @param FileConfig $config - * @param string $storageDirectory + * @param string|null $storageDirectory * @param bool $useFilesystem * @param string $ip * @param int $port * * @throws \Exception */ - public function __construct(?ResolverInterface $resolver = null, ?EventDispatcherInterface $dispatcher = null, ?FileConfig $config = null, string $storageDirectory, bool $useFilesystem = false, string $ip = '0.0.0.0', int $port = 53) + 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) { if (!function_exists('socket_create') || !extension_loaded('sockets')) { throw new \Exception('Socket extension or socket_create() function not found.'); @@ -106,20 +106,18 @@ public function __construct(?ResolverInterface $resolver = null, ?EventDispatche $this->config = $config; $this->port = $port; $this->ip = $ip; - - // detect os and setup file manager, default to working directory on windows + $this->useFilesystem = $useFilesystem; + + // detect os if (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') { $this->isWindows = true; - $this->filesystemManager = new FilesystemManager($storageDirectory); } else { - // default to /etc/phpdnsserver on unix $this->isWindows = false; - $this->filesystemManager = new FilesystemManager($storageDirectory); } - $this->useFilesystem = $useFilesystem; - + // only register filesystem if we want to use it if ($useFilesystem) { + $this->filesystemManager = new FilesystemManager($storageDirectory); $this->resolver = new JsonFileSystemResolver($this->filesystemManager); } From dbaa63753d7100920634ca4df2de09258823408a Mon Sep 17 00:00:00 2001 From: Eric Hocking Date: Tue, 24 Sep 2019 09:25:37 -0400 Subject: [PATCH 6/8] whitespace fixes --- src/Server.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Server.php b/src/Server.php index 7036056..d4309e6 100644 --- a/src/Server.php +++ b/src/Server.php @@ -107,7 +107,7 @@ public function __construct(?ResolverInterface $resolver = null, ?EventDispatche $this->port = $port; $this->ip = $ip; $this->useFilesystem = $useFilesystem; - + // detect os if (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') { $this->isWindows = true; From 12fd80a556ee19f67955e6884cbca1c1d290cdd7 Mon Sep 17 00:00:00 2001 From: Eric Hocking Date: Tue, 24 Sep 2019 10:16:54 -0400 Subject: [PATCH 7/8] added a php based installer, and updated readme --- README.md | 19 ++++++++++++ bin/PhpDnsInstaller.php | 65 +++++++++++++++++++++++++++++++++++++++++ composer.json | 3 +- installer.box.json | 13 +++++++++ 4 files changed, 99 insertions(+), 1 deletion(-) create mode 100644 bin/PhpDnsInstaller.php create mode 100644 installer.box.json diff --git a/README.md b/README.md index 870c2a0..7969785 100644 --- a/README.md +++ b/README.md @@ -118,6 +118,25 @@ Unit tests using PHPUnit are provided. A simple script is located in the root. * run `composer install` to install PHPUnit and dependencies * run `vendor/bin/phpunit` from the root to run the tests +## Building .phar +* run `composer run-script build-server` to build the phpdnsserver.phar file, outputs in the bin folder. +* run `composer run-script build-console` to build the phpdnscli.phar file, outputs in the bin folder. +* run `composer run-script build-installer` to build the installer. Windows support for the installer is currently limited. + +## Running the .phar files +To run the new .phar files, download them from the release and move them to the desired folder. +* `phpdnsserver.phar` to run the phpdnsserver, uses the new filesystem by default +* `phpdnscli.phar` to run cli commands +* `phpdnsinstaller.phar` as root to create required folders and default config. + +## Supported command line switches +* `--bind:b` - bind to a specific ip. Uses `0.0.0.0` by default +* `--port:p` - bind to a specific port. Uses port `53` by default +* `--config:c` - specify the config file. Uses `phpdns.json` on windows +and `/etc/phpdns.json` on unix systems by default +* `--storage:s` - specify the path to the storage for zones, and logs. Uses `/etc/phpdnsserver` on unix, +and current working directory on windows. + ## Supported Record Types * A diff --git a/bin/PhpDnsInstaller.php b/bin/PhpDnsInstaller.php new file mode 100644 index 0000000..6bac70d --- /dev/null +++ b/bin/PhpDnsInstaller.php @@ -0,0 +1,65 @@ + '0.0.0.0', + 'port' => 53, + 'storage' => '/etc/phpdnsserver', + 'backend' => 'file' + ]; + + $filesystem = new \Symfony\Component\Filesystem\Filesystem; + + try { + echo "Creating required directories and config files...\n"; + + $filesystem->mkdir('/etc/phpdnsserver'); + $filesystem->mkdir('/etc/phpdnsserver/zones'); + $filesystem->mkdir('/etc/phpdnsserver/logs'); + + // create default config + file_put_contents('/etc/phpdns.json', json_encode(getcwd())); + } catch (\Symfony\Component\Filesystem\Exception\IOException $e){ + die("An error occurred during installation\n".$e->getMessage()); + } + +} else { + // create the default config file + $defaultConfig = [ + 'host' => '0.0.0.0', + 'port' => 53, + 'backend' => 'file' + ]; + + $filesystem = new \Symfony\Component\Filesystem\Filesystem; + + try { + echo "Creating required directories and config files...\n"; + + $filesystem->mkdir(getcwd().'\\zones'); + $filesystem->mkdir(getcwd().'\\logs'); + + // create default config + file_put_contents(getcwd().'\\phpdns.json', json_encode(getcwd())); + } catch (\Symfony\Component\Filesystem\Exception\IOException $e){ + die("An error occurred during installation\n".$e->getMessage()); + } +} \ No newline at end of file diff --git a/composer.json b/composer.json index 61ca4d0..0594de0 100644 --- a/composer.json +++ b/composer.json @@ -49,6 +49,7 @@ }, "scripts": { "build-server": "box compile -c server.box.json", - "build-console": "box compile -c console.box.json" + "build-console": "box compile -c console.box.json", + "build-installer": "box compile -c installer.box.json" } } diff --git a/installer.box.json b/installer.box.json new file mode 100644 index 0000000..8ad82ce --- /dev/null +++ b/installer.box.json @@ -0,0 +1,13 @@ +{ + "chmod": "0755", + "main": "bin/PhpDnsInstaller.php", + "output": "bin/phpdnsinstaller.phar", + "finder": [ + { + "name": "*.php", + "exclude": ["test", "tests"], + "in": "vendor" + } + ], + "stub": true +} \ No newline at end of file From 5f5a58192c0a9c9910252206e18e9c438f795974 Mon Sep 17 00:00:00 2001 From: Eric Hocking Date: Wed, 25 Sep 2019 08:49:53 -0400 Subject: [PATCH 8/8] ran php-cs-fixer against the updates --- src/Config/FileConfig.php | 13 +++++++-- src/Config/RecursiveArrayObject.php | 16 +++++++++-- src/Console/CommandServer.php | 11 +++++++- src/Console/Commands/VersionCommand.php | 13 +++++++-- src/Exception/ConfigFileNotFoundException.php | 12 ++++++-- src/Exception/InvalidZoneFileException.php | 12 ++++++-- src/Exception/ZoneFileNotFoundException.php | 12 ++++++-- src/Filesystem/FilesystemManager.php | 28 ++++++++++++------- src/Resolver/AbstractResolver.php | 2 +- src/Resolver/JsonFileSystemResolver.php | 23 ++++++++------- src/Server.php | 13 ++++----- 11 files changed, 110 insertions(+), 45 deletions(-) diff --git a/src/Config/FileConfig.php b/src/Config/FileConfig.php index 8f435c8..79908ed 100644 --- a/src/Config/FileConfig.php +++ b/src/Config/FileConfig.php @@ -1,5 +1,14 @@ + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + namespace yswery\DNS\Config; use yswery\DNS\Exception\ConfigFileNotFoundException; @@ -31,7 +40,7 @@ public function load() $data = file_get_contents($this->configFile); $this->config = json_decode($data); } else { - throw new ConfigFileNotFoundException("Config file not found."); + throw new ConfigFileNotFoundException('Config file not found.'); } } @@ -64,4 +73,4 @@ public function has($key) { return isset($this->config->{$key}); } -} \ No newline at end of file +} diff --git a/src/Config/RecursiveArrayObject.php b/src/Config/RecursiveArrayObject.php index 64e6c8b..2e0fe86 100644 --- a/src/Config/RecursiveArrayObject.php +++ b/src/Config/RecursiveArrayObject.php @@ -1,21 +1,31 @@ + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + namespace yswery\DNS\Config; use ArrayObject; class RecursiveArrayObject extends ArrayObject { - function getArrayCopy() + public function getArrayCopy() { $resultArray = parent::getArrayCopy(); - foreach($resultArray as $key => $val) { + foreach ($resultArray as $key => $val) { if (!is_object($val)) { continue; } $o = new RecursiveArrayObject($val); $resultArray[$key] = $o->getArrayCopy(); } + return $resultArray; } -} \ No newline at end of file +} diff --git a/src/Console/CommandServer.php b/src/Console/CommandServer.php index b5ca0f1..c4ff0a1 100644 --- a/src/Console/CommandServer.php +++ b/src/Console/CommandServer.php @@ -1,5 +1,14 @@ + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + namespace yswery\DNS\Console; use Symfony\Component\Console\Application; @@ -19,4 +28,4 @@ protected function registerCommands() { $this->add(new VersionCommand()); } -} \ No newline at end of file +} diff --git a/src/Console/Commands/VersionCommand.php b/src/Console/Commands/VersionCommand.php index a36f734..313c9d3 100644 --- a/src/Console/Commands/VersionCommand.php +++ b/src/Console/Commands/VersionCommand.php @@ -1,5 +1,14 @@ + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + namespace yswery\DNS\Console\Commands; use Symfony\Component\Console\Command\Command; @@ -9,7 +18,7 @@ class VersionCommand extends Command { - protected static $defaultName = "version"; + protected static $defaultName = 'version'; protected function configure() { @@ -21,4 +30,4 @@ protected function execute(InputInterface $input, OutputInterface $output) { $output->writeln('PowerDnsServer version '.Server::VERSION); } -} \ No newline at end of file +} diff --git a/src/Exception/ConfigFileNotFoundException.php b/src/Exception/ConfigFileNotFoundException.php index c9c2ecf..5fb13a9 100644 --- a/src/Exception/ConfigFileNotFoundException.php +++ b/src/Exception/ConfigFileNotFoundException.php @@ -1,10 +1,18 @@ + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + namespace yswery\DNS\Exception; use Exception; class ConfigFileNotFoundException extends Exception { - -} \ No newline at end of file +} diff --git a/src/Exception/InvalidZoneFileException.php b/src/Exception/InvalidZoneFileException.php index 545fc2c..1aa9dbc 100644 --- a/src/Exception/InvalidZoneFileException.php +++ b/src/Exception/InvalidZoneFileException.php @@ -1,10 +1,18 @@ + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + namespace yswery\DNS\Exception; use RuntimeException; class InvalidZoneFileException extends RuntimeException { - -} \ No newline at end of file +} diff --git a/src/Exception/ZoneFileNotFoundException.php b/src/Exception/ZoneFileNotFoundException.php index 94fc328..d3bb861 100644 --- a/src/Exception/ZoneFileNotFoundException.php +++ b/src/Exception/ZoneFileNotFoundException.php @@ -1,10 +1,18 @@ + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + namespace yswery\DNS\Exception; use RuntimeException; class ZoneFileNotFoundException extends RuntimeException { - -} \ No newline at end of file +} diff --git a/src/Filesystem/FilesystemManager.php b/src/Filesystem/FilesystemManager.php index e594286..f0e0ba4 100644 --- a/src/Filesystem/FilesystemManager.php +++ b/src/Filesystem/FilesystemManager.php @@ -1,5 +1,14 @@ + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + namespace yswery\DNS\Filesystem; use Symfony\Component\Filesystem\Exception\IOExceptionInterface; @@ -25,17 +34,14 @@ class FilesystemManager public function __construct($basePath = null, $zonePath = null) { - if ($basePath) - { + if ($basePath) { $this->setBasePath($basePath); } - if ($zonePath) - { + if ($zonePath) { $this->setZonePath($zonePath); } - $this->registerFilesystem(); } @@ -44,21 +50,19 @@ protected function registerFilesystem() $this->filesystem = new Filesystem(); // make sure our directories exist - if (!$this->filesystem->exists($this->zonePath())) - { + if (!$this->filesystem->exists($this->zonePath())) { try { $this->filesystem->mkdir($this->zonePath(), 0700); } catch (IOExceptionInterface $e) { // todo: implement logging functions } - } - } public function setBasePath($basePath) { $this->basePath = rtrim($basePath, '\/'); + return $this; } @@ -70,6 +74,7 @@ public function basePath() public function setZonePath($zonePath) { $this->zonePath = rtrim($zonePath, '\/'); + return $this; } @@ -80,12 +85,15 @@ public function zonePath() /** * @param string $zone + * * @return JsonFileSystemResolver + * * @throws \yswery\DNS\UnsupportedTypeException */ public function getZone(string $zone) { $zoneFile = $this->basePath().DIRECTORY_SEPARATOR.$zone.'.json'; + return new JsonFileSystemResolver($zoneFile); } -} \ No newline at end of file +} diff --git a/src/Resolver/AbstractResolver.php b/src/Resolver/AbstractResolver.php index fb07c25..c36773c 100644 --- a/src/Resolver/AbstractResolver.php +++ b/src/Resolver/AbstractResolver.php @@ -82,7 +82,7 @@ public function isAuthority($domain): bool } /** - * {@inheritDoc} + * {@inheritdoc} */ public function supportsSaving() { diff --git a/src/Resolver/JsonFileSystemResolver.php b/src/Resolver/JsonFileSystemResolver.php index 8d61fd3..97e6d84 100644 --- a/src/Resolver/JsonFileSystemResolver.php +++ b/src/Resolver/JsonFileSystemResolver.php @@ -39,7 +39,7 @@ class JsonFileSystemResolver extends AbstractResolver * JsonResolver constructor. * * @param FilesystemManager $filesystemManager - * @param int $defaultTtl + * @param int $defaultTtl * * @throws UnsupportedTypeException */ @@ -50,9 +50,8 @@ public function __construct(FilesystemManager $filesystemManager, $defaultTtl = $this->filesystemManager = $filesystemManager; $this->defaultTtl = $defaultTtl; - $zones = glob($filesystemManager->zonePath()."/*.json"); - foreach ($zones as $file) - { + $zones = glob($filesystemManager->zonePath().'/*.json'); + foreach ($zones as $file) { $zone = json_decode(file_get_contents($file), true); $resourceRecords = $this->isLegacyFormat($zone) ? $this->processLegacyZone($zone) : $this->processZone($zone); $this->addZone($resourceRecords); @@ -60,31 +59,31 @@ public function __construct(FilesystemManager $filesystemManager, $defaultTtl = } /** - * Load a zone file + * Load a zone file. * * @param string $file + * * @throws UnsupportedTypeException * @throws ZoneFileNotFoundException */ - public function loadZone($file) { - if (file_exists($file)) - { + public function loadZone($file) + { + if (file_exists($file)) { $zone = json_decode(file_get_contents($file), true); $resourceRecords = $this->isLegacyFormat($zone) ? $this->processLegacyZone($zone) : $this->processZone($zone); $this->addZone($resourceRecords); } else { - throw new ZoneFileNotFoundException("The zone file could not be found"); + throw new ZoneFileNotFoundException('The zone file could not be found'); } } - /** - * Saves the zone to a .json file + * Saves the zone to a .json file. + * * @param string $zone */ public function saveZone($zone) { - } /** diff --git a/src/Server.php b/src/Server.php index d4309e6..c9578be 100644 --- a/src/Server.php +++ b/src/Server.php @@ -14,8 +14,6 @@ use React\Datagram\Socket; use React\Datagram\SocketInterface; use React\EventLoop\LoopInterface; -use Symfony\Component\Console\Input\InputInterface; -use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\EventDispatcher\EventDispatcherInterface; use Symfony\Component\EventDispatcher\Event; use yswery\DNS\Config\FileConfig; @@ -32,10 +30,11 @@ class Server { /** - * The version of PhpDnsServer we are running + * The version of PhpDnsServer we are running. + * * @var string */ - const VERSION = "1.4.0"; + const VERSION = '1.4.0'; /** * @var EventDispatcherInterface @@ -95,7 +94,7 @@ class Server * * @throws \Exception */ - 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) + 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) { if (!function_exists('socket_create') || !extension_loaded('sockets')) { throw new \Exception('Socket extension or socket_create() function not found.'); @@ -109,7 +108,7 @@ public function __construct(?ResolverInterface $resolver = null, ?EventDispatche $this->useFilesystem = $useFilesystem; // detect os - if (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') { + if ('WIN' === strtoupper(substr(PHP_OS, 0, 3))) { $this->isWindows = true; } else { $this->isWindows = false; @@ -131,7 +130,6 @@ public function __construct(?ResolverInterface $resolver = null, ?EventDispatche }); } - /** * Start the server. */ @@ -177,7 +175,6 @@ public function handleQueryFromStream(string $buffer): string $message = Decoder::decodeMessage($buffer); $this->dispatch(Events::QUERY_RECEIVE, new QueryReceiveEvent($message)); - $responseMessage = clone $message; $responseMessage->getHeader() ->setResponse(true)