From 36de37845954d1ed02ce9bed84e454ec2082487e Mon Sep 17 00:00:00 2001 From: Laurynas Gadliauskas Date: Tue, 27 Aug 2024 10:21:13 +0300 Subject: [PATCH] feat: Add dig lookup functionality --- composer.json | 2 +- src/Client.php | 47 +++++++++++++ src/Command/DigCommand.php | 33 +++++++++ src/Command/DigOptions.php | 32 +++++++++ src/Command/DigQuery.php | 37 ++++++++++ src/Exceptions/DigExecException.php | 13 ++++ .../UnsupportedRecordTypeException.php | 13 ++++ tests/ClientTest.php | 36 ++++++++++ tests/Command/DigCommandTest.php | 69 +++++++++++++++++++ tests/Command/DigOptionsTest.php | 56 +++++++++++++++ tests/Command/DigQueryTest.php | 63 +++++++++++++++++ 11 files changed, 400 insertions(+), 1 deletion(-) create mode 100644 src/Command/DigCommand.php create mode 100644 src/Command/DigOptions.php create mode 100644 src/Command/DigQuery.php create mode 100644 src/Exceptions/DigExecException.php create mode 100644 src/Exceptions/UnsupportedRecordTypeException.php create mode 100644 tests/Command/DigCommandTest.php create mode 100644 tests/Command/DigOptionsTest.php create mode 100644 tests/Command/DigQueryTest.php diff --git a/composer.json b/composer.json index cbbec22..d774f38 100644 --- a/composer.json +++ b/composer.json @@ -8,7 +8,7 @@ ], "license": "MIT", "require": { - "php": ">=8.0", + "php": ">=8.1", "psr/log": "^1.0 || ^2.0 || ^3.0" }, "require-dev": { diff --git a/src/Client.php b/src/Client.php index 32921b0..ffb7420 100644 --- a/src/Client.php +++ b/src/Client.php @@ -6,6 +6,11 @@ use Closure; use ErrorException; +use Hostinger\Dig\Command\DigCommand; +use Hostinger\Dig\Command\DigOptions; +use Hostinger\Dig\Command\DigQuery; +use Hostinger\Dig\Exceptions\DigExecException; +use Hostinger\Dig\Exceptions\UnsupportedRecordTypeException; use Hostinger\Dig\RecordType\RecordType; use Psr\Log\LoggerAwareInterface; use Psr\Log\LoggerAwareTrait; @@ -77,6 +82,48 @@ public function getRecord(string $domain, int $type, string $dnsProvider = '8.8. return $this->executeDig($domain, $recordType, $dnsProvider, $timeout) ?? ($this->fallback)($domain, $type); } + /** + * @param string $name + * @param int $type One of the DNS_* constants + * @param DigOptions|null $options + * @param DigQuery|null $query + * @return array + */ + public function lookup( + string $name, + int $type, + ?DigOptions $options = null, + ?DigQuery $query = null, + ): array { + $recordTypeFactory = new RecordTypeFactory(); + $recordType = $recordTypeFactory->make($type); + + if (is_null($recordType)) { + throw new UnsupportedRecordTypeException($type); + } + + $execState = $this->execEnabled(); + if ($execState !== true) { + throw new DigExecException($execState); + } + + $command = new DigCommand($name, strtoupper($recordType->getType()), $options, $query); + $cliCommand = $command->toCliCommand(); + + $this->logger->debug('executing dig lookup', [ + 'name' => $name, + 'type' => $recordType->getType(), + 'command' => $cliCommand, + ]); + + exec($cliCommand, $output, $code); + if ($code !== 0) { + throw new DigExecException((string) $code); + } + + return $recordType->transform($output); + } + /** * Use custom error handler to convert dns_get_record() errors to exceptions. * It throws a warning when the DNS query fails. diff --git a/src/Command/DigCommand.php b/src/Command/DigCommand.php new file mode 100644 index 0000000..a01f722 --- /dev/null +++ b/src/Command/DigCommand.php @@ -0,0 +1,33 @@ +options) { + $args = array_merge($args, array_values($this->options->toCliOptions())); + } + + if ($this->query) { + $args = array_merge($args, array_values($this->query->toCliQuery())); + } + + $args[] = escapeshellarg($this->name); + $args[] = escapeshellarg($this->type); + + return join(' ', $args); + } +} diff --git a/src/Command/DigOptions.php b/src/Command/DigOptions.php new file mode 100644 index 0000000..e817409 --- /dev/null +++ b/src/Command/DigOptions.php @@ -0,0 +1,32 @@ +server !== null) { + $opts['server'] = escapeshellarg('@' . $this->server); + } + + if ($this->name !== null) { + $opts['name'] = '-q ' . escapeshellarg($this->name); + } + + if ($this->type !== null) { + $opts['type'] = '-t ' . escapeshellarg($this->type); + } + + return $opts; + } +} diff --git a/src/Command/DigQuery.php b/src/Command/DigQuery.php new file mode 100644 index 0000000..25f277b --- /dev/null +++ b/src/Command/DigQuery.php @@ -0,0 +1,37 @@ +all !== null) { + $opts['all'] = escapeshellarg('+' . ($this->all ? 'all' : 'noall')); + } + + if ($this->answer !== null) { + $opts['answer'] = escapeshellarg('+' . ($this->answer ? 'answer' : 'noanswer')); + } + + if ($this->authority !== null) { + $opts['authority'] = escapeshellarg('+' . ($this->authority ? 'authority' : 'noauthority')); + } + + if ($this->time !== null) { + $opts['time'] = escapeshellarg('+time=' . $this->time); + } + + return $opts; + } +} diff --git a/src/Exceptions/DigExecException.php b/src/Exceptions/DigExecException.php new file mode 100644 index 0000000..ba36546 --- /dev/null +++ b/src/Exceptions/DigExecException.php @@ -0,0 +1,13 @@ +assertCount(1, $result); $this->assertEquals('CUSTOM', $result[0]['type']); } + + public function testLookupFailsOnUnsupportedRecordType(): void + { + $this->expectException(UnsupportedRecordTypeException::class); + + $client = new Client(); + $client->lookup('test.com', 9999); + } + + public function testLookup(): void + { + $domain = 'hostinger.com'; + $type = DNS_A; + + $client = new Client(); + $result = $client->lookup( + $domain, + $type, + new DigOptions( + server: '8.8.8.8', + ), + new DigQuery( + all: false, + answer: true, + ), + ); + + $expected = dns_get_record($domain, $type); + + $this->assertEquals($expected[0]['host'], $result[0]['host']); + $this->assertEquals($expected[0]['class'], $result[0]['class']); + $this->assertEquals($expected[0]['type'], $result[0]['type']); + } } diff --git a/tests/Command/DigCommandTest.php b/tests/Command/DigCommandTest.php new file mode 100644 index 0000000..b0ef16f --- /dev/null +++ b/tests/Command/DigCommandTest.php @@ -0,0 +1,69 @@ + [ + [ + 'test.com', + 'NS', + ], + "dig 'test.com' 'NS'", + ]; + + yield 'with options' => [ + [ + 'test.com', + 'NS', + new DigOptions( + 'ping.com', + ), + ], + "dig '@ping.com' 'test.com' 'NS'", + ]; + + yield 'with options and query' => [ + [ + 'test.com', + 'NS', + new DigOptions( + 'ping.com', + ), + new DigQuery( + false, + true, + false, + 10, + ), + ], + "dig '@ping.com' '+noall' '+answer' '+noauthority' '+time=10' 'test.com' 'NS'", + ]; + + yield 'escapes args' => [ + [ + 'test.com && exit 1', + 'NS && ls -la', + ], + "dig 'test.com && exit 1' 'NS && ls -la'", + ]; + } + + /** + * @dataProvider dataProvider + */ + public function testToCliCommand(array $args, string $expected): void + { + $command = new DigCommand(...$args); + + $this->assertEquals($expected, $command->toCliCommand()); + } +} diff --git a/tests/Command/DigOptionsTest.php b/tests/Command/DigOptionsTest.php new file mode 100644 index 0000000..37c2a4b --- /dev/null +++ b/tests/Command/DigOptionsTest.php @@ -0,0 +1,56 @@ + [ + [ + null, + null, + null, + ], + [], + ]; + + yield 'all set' => [ + [ + 'test.com', + 'ping.com', + 'NS', + ], + [ + 'server' => escapeshellarg('@test.com'), + 'name' => '-q ' . escapeshellarg('ping.com'), + 'type' => '-t ' . escapeshellarg('NS'), + ], + ]; + + yield 'some set' => [ + [ + null, + null, + 'NS', + ], + [ + 'type' => '-t ' . escapeshellarg('NS'), + ], + ]; + } + + /** + * @dataProvider optionsDataProvider + */ + public function testToCliOptions(array $args, array $expected): void + { + $query = new DigOptions(...$args); + + $this->assertEquals($expected, $query->toCliOptions()); + } +} diff --git a/tests/Command/DigQueryTest.php b/tests/Command/DigQueryTest.php new file mode 100644 index 0000000..045e8fe --- /dev/null +++ b/tests/Command/DigQueryTest.php @@ -0,0 +1,63 @@ + [ + [ + null, + null, + null, + null, + ], + [], + ]; + + yield 'all set' => [ + [ + true, + true, + true, + 10, + ], + [ + 'all' => escapeshellarg('+all'), + 'answer' => escapeshellarg('+answer'), + 'authority' => escapeshellarg('+authority'), + 'time' => escapeshellarg('+time=10'), + ], + ]; + + yield 'all set to false' => [ + [ + false, + false, + false, + 5, + ], + [ + 'all' => escapeshellarg('+noall'), + 'answer' => escapeshellarg('+noanswer'), + 'authority' => escapeshellarg('+noauthority'), + 'time' => escapeshellarg('+time=5'), + ], + ]; + } + + /** + * @dataProvider queryDataProvider + */ + public function testToCliQuery(array $args, array $expected): void + { + $query = new DigQuery(...$args); + + $this->assertEquals($expected, $query->toCliQuery()); + } +}