├── _config.yml
├── .gitignore
├── phpstan.neon
├── src
├── Entities
│ ├── EntityAbstract.php
│ ├── Interfaces
│ │ ├── Arrayable.php
│ │ ├── Serializable.php
│ │ └── DNSRecordInterface.php
│ ├── TXTData.php
│ ├── NSData.php
│ ├── CNAMEData.php
│ ├── PTRData.php
│ ├── MXData.php
│ ├── SRVData.php
│ ├── IPAddress.php
│ ├── CAAData.php
│ ├── Hostname.php
│ ├── SOAData.php
│ ├── DataAbstract.php
│ ├── DNSRecord.php
│ ├── DNSRecordCollection.php
│ └── DNSRecordType.php
├── Exceptions
│ ├── InvalidArgumentException.php
│ └── Exception.php
├── Resolvers
│ ├── Exceptions
│ │ ├── ReverseLookupFailure.php
│ │ └── QueryFailure.php
│ ├── Interfaces
│ │ ├── Resolver.php
│ │ ├── ObservableResolver.php
│ │ ├── ReverseDNSQuery.php
│ │ ├── Chain.php
│ │ └── DNSQuery.php
│ ├── Traits
│ │ └── Time.php
│ ├── LocalSystem.php
│ ├── Dig.php
│ ├── GoogleDNS.php
│ ├── Cached.php
│ ├── CloudFlare.php
│ ├── Chain.php
│ └── ResolverAbstract.php
├── Observability
│ ├── Performance
│ │ ├── ProfileFactory.php
│ │ ├── Interfaces
│ │ │ ├── Time.php
│ │ │ └── ProfileInterface.php
│ │ ├── Timer.php
│ │ └── Profile.php
│ ├── Interfaces
│ │ └── Observable.php
│ ├── Traits
│ │ ├── Logger.php
│ │ ├── Profileable.php
│ │ └── Dispatcher.php
│ ├── Events
│ │ ├── ObservableEventAbstract.php
│ │ ├── DNSQueryProfiled.php
│ │ ├── DNSQueryFailed.php
│ │ └── DNSQueried.php
│ └── Subscribers
│ │ └── STDIOSubscriber.php
├── Mappers
│ ├── MapperInterface.php
│ ├── MapperAbstract.php
│ ├── CloudFlare.php
│ ├── GoogleDNS.php
│ ├── Dig.php
│ └── LocalSystem.php
└── Services
│ ├── Interfaces
│ └── LocalSystemDNS.php
│ └── LocalSystemDNS.php
├── tests
├── bootstrap.php
├── Unit
│ ├── Exceptions
│ │ └── ExceptionTest.php
│ ├── Observability
│ │ ├── Performance
│ │ │ ├── TimerTest.php
│ │ │ └── ProfileTest.php
│ │ ├── Events
│ │ │ ├── ObservableEventAbstractTest.php
│ │ │ ├── DNSQueryProfiledTest.php
│ │ │ ├── DNSQueryFailedTest.php
│ │ │ └── DNSQueriedTest.php
│ │ ├── Traits
│ │ │ ├── ProfileableTest.php
│ │ │ └── DispatcherTest.php
│ │ └── Subscribers
│ │ │ └── STDIOSubscriberTest.php
│ ├── Resolvers
│ │ ├── TestResolver.php
│ │ ├── DigTest.php
│ │ ├── CachedTest.php
│ │ ├── LocalSystemTest.php
│ │ └── ResolverAbstractTest.php
│ ├── BaseTestAbstract.php
│ ├── Entities
│ │ ├── DNSRecordTypeTest.php
│ │ ├── TXTDataTest.php
│ │ ├── NSDataTest.php
│ │ ├── IPAddressTest.php
│ │ ├── PTRDataTest.php
│ │ ├── CNAMEDataTest.php
│ │ ├── MXDataTest.php
│ │ ├── HostnameTest.php
│ │ ├── SRVDataTest.php
│ │ ├── CAADataTest.php
│ │ ├── SOADataTest.php
│ │ ├── DNSRecordTest.php
│ │ ├── DataAbstractTest.php
│ │ └── DNSRecordCollectionTest.php
│ └── Mappers
│ │ ├── CloudFlareTest.php
│ │ ├── GoogleDNSTest.php
│ │ ├── DigTest.php
│ │ └── LocalSystemTest.php
└── Integration
│ ├── ServicesTest.php
│ └── BaseTestAbstract.php
├── CODE_OF_CONDUCT.md
├── .github
├── ISSUE_TEMPLATE
│ ├── feature_request.md
│ └── bug_report.md
└── workflows
│ ├── php-ci.yml
│ └── php-code-coverage.yml
├── CONTRIBUTING.md
├── rector.php
├── phpunit.xml.dist
├── LICENSE
├── Makefile
├── composer.json
├── bootstrap
└── repl.php
├── churn.yml
└── psalm.xml
/_config.yml:
--------------------------------------------------------------------------------
1 | theme: jekyll-theme-cayman
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | composer.phar
2 | /vendor/
3 | /coverage/
4 | /.idea/
5 | .DS_Store
6 | .churn.cache
7 | composer.lock
8 | .phpunit.result.cache
--------------------------------------------------------------------------------
/phpstan.neon:
--------------------------------------------------------------------------------
1 | parameters:
2 | checkMissingIterableValueType: false
3 | checkGenericClassInNonGenericObjectType: false
4 | ignoreErrors:
5 |
--------------------------------------------------------------------------------
/src/Entities/EntityAbstract.php:
--------------------------------------------------------------------------------
1 | $this->getMessage(),
13 | 'code' => $this->getCode(),
14 | 'file' => $this->getFile(),
15 | 'line' => $this->getLine(),
16 | 'trace' => $this->getTraceAsString(),
17 | ];
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/src/Observability/Performance/Timer.php:
--------------------------------------------------------------------------------
1 | logger = $logger;
15 | }
16 |
17 | protected function getLogger(): LoggerInterface
18 | {
19 | if ($this->logger === null) {
20 | $this->logger = new NullLogger();
21 | }
22 |
23 | return $this->logger;
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/src/Observability/Events/ObservableEventAbstract.php:
--------------------------------------------------------------------------------
1 | $this->toArray(),
19 | ];
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Code of Conduct
2 |
3 | ## Be Kind
4 |
5 | We're all very different, and we're all human. What an amazing thing!
6 |
7 | Every person is welcome here in so much as they do not seek to harass or dehumanize another one
8 | on this project.
9 |
10 | Always assume best intent.
11 |
12 | Strive for civility, kindness, helpfulness, and humility above all else.
13 |
14 | In the words of the Author Kurt Vonnegut via Mr. Rosewater:
15 |
16 | > Hello babies. Welcome to Earth. It’s hot in the summer and cold in the winter. It’s round and wet and crowded. On the outside, babies, you’ve got a hundred years here. There’s only one rule that I know of, babies—God damn it, you’ve got to be kind.
17 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature request
3 | about: Suggest an idea for this project
4 | title: ''
5 | labels: ''
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Is your feature request related to a problem? Please describe.**
11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
12 |
13 | **Describe the solution you'd like**
14 | A clear and concise description of what you want to happen.
15 |
16 | **Describe alternatives you've considered**
17 | A clear and concise description of any alternative solutions or features you've considered.
18 |
19 | **Additional context**
20 | Add any other context or screenshots about the feature request here.
21 |
--------------------------------------------------------------------------------
/src/Entities/TXTData.php:
--------------------------------------------------------------------------------
1 | value;
14 | }
15 |
16 | public function getValue(): string
17 | {
18 | return $this->value;
19 | }
20 |
21 | public function toArray(): array
22 | {
23 | return [
24 | 'value' => $this->value,
25 | ];
26 | }
27 |
28 | public function __unserialize(array $unserialized): void
29 | {
30 | $this->value = $unserialized['value'];
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | First off, thank you for even considering contributing! All you need to contribute is curiousity, a helpful attitude, and a few extra things like:
2 |
3 | - 100% unit test coverage. Test coverage isn't everything, but it's easy. Good test cases are much appreciated.
4 | - Comply with PSR2 coding standards.
5 | - Keep your solution as SOLID and simple as possible.
6 |
7 | Of course I am available to help and am open to your way of doing something. If you want to propose a radically different approach,
8 | the best way to do it is in a PR with examples. It helps clear up any communication misunderstanding around your design.
9 |
10 | Again, thank you so much for being interested in contributing.
11 |
12 | Also, checkout the Makefile for useful goodies to get you up and running, coding, and testing.
13 |
14 | - Christian
15 |
--------------------------------------------------------------------------------
/src/Entities/NSData.php:
--------------------------------------------------------------------------------
1 | target;
17 | }
18 |
19 | public function getTarget(): Hostname
20 | {
21 | return $this->target;
22 | }
23 |
24 | public function toArray(): array
25 | {
26 | return [
27 | 'target' => (string)$this->target,
28 | ];
29 | }
30 |
31 | public function __unserialize(array $unserialized): void
32 | {
33 | $this->target = new Hostname($unserialized['target']);
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/src/Entities/CNAMEData.php:
--------------------------------------------------------------------------------
1 | hostname;
17 | }
18 |
19 | public function getHostname(): Hostname
20 | {
21 | return $this->hostname;
22 | }
23 |
24 | public function toArray(): array
25 | {
26 | return [
27 | 'hostname' => (string)$this->hostname,
28 | ];
29 | }
30 |
31 | public function __unserialize(array $unserialized): void
32 | {
33 | $this->hostname = new Hostname($unserialized['hostname']);
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/src/Entities/PTRData.php:
--------------------------------------------------------------------------------
1 | hostname;
17 | }
18 |
19 | public function getHostname(): Hostname
20 | {
21 | return $this->hostname;
22 | }
23 |
24 | public function toArray(): array
25 | {
26 | return [
27 | 'hostname' => (string)$this->hostname,
28 | ];
29 | }
30 |
31 | public function __unserialize(array $unserialized): void
32 | {
33 | $this->hostname = new Hostname($unserialized['hostname']);
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/src/Resolvers/Traits/Time.php:
--------------------------------------------------------------------------------
1 | dateTimeImmutable = $dateTimeImmutable;
14 | }
15 |
16 | public function getTimeStamp(): int
17 | {
18 | return $this->getNewDateTimeImmutable()->getTimestamp();
19 | }
20 |
21 | private function getNewDateTimeImmutable(): DateTimeImmutable
22 | {
23 | if (!$this->dateTimeImmutable) {
24 | $this->dateTimeImmutable = new DateTimeImmutable();
25 | }
26 |
27 | return /** @scrutinizer ignore-type */ $this->dateTimeImmutable
28 | ->setTimestamp(\time());
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/tests/Unit/Exceptions/ExceptionTest.php:
--------------------------------------------------------------------------------
1 | assertInstanceOf(JsonSerializable::class, $exception);
19 |
20 | $jsonReady = $exception->jsonSerialize();
21 |
22 | $this->assertSame('The exception', $jsonReady['message']);
23 | $this->assertSame(123, $jsonReady['code']);
24 | $this->assertTrue(is_int($jsonReady['line']));
25 | $this->assertTrue(is_string($jsonReady['trace']));
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/src/Entities/Interfaces/DNSRecordInterface.php:
--------------------------------------------------------------------------------
1 | timer = new Timer();
19 | }
20 |
21 | /**
22 | * @test
23 | */
24 | public function getsNowAndMicroTime(): void
25 | {
26 | $this->assertGreaterThan(0, $this->timer->getMicroTime());
27 | $this->assertTrue(is_float($this->timer->getMicroTime()));
28 |
29 | $this->assertInstanceOf(DateTimeInterface::class, $this->timer->now());
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/src/Observability/Traits/Profileable.php:
--------------------------------------------------------------------------------
1 | getProfileFactory()->create($transactionName);
15 | }
16 |
17 | public function setProfileFactory(ProfileFactory $profileFactory): void
18 | {
19 | $this->profileFactory = $profileFactory;
20 | }
21 |
22 | private function getProfileFactory(): ProfileFactory
23 | {
24 | if ($this->profileFactory === null) {
25 | $this->profileFactory = new ProfileFactory();
26 | }
27 |
28 | return $this->profileFactory;
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/tests/Unit/Resolvers/TestResolver.php:
--------------------------------------------------------------------------------
1 | recordCollection) {
20 | return $this->recordCollection;
21 | }
22 |
23 | if ($this->error) {
24 | throw $this->error;
25 | }
26 |
27 | return new DNSRecordCollection();
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: Create a report to help us improve
4 | title: ''
5 | labels: ''
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Describe the bug**
11 | A clear and concise description of what the bug is.
12 |
13 | **To Reproduce**
14 | Steps to reproduce the behavior:
15 | 1. Go to '...'
16 | 2. Click on '....'
17 | 3. Scroll down to '....'
18 | 4. See error
19 |
20 | **Expected behavior**
21 | A clear and concise description of what you expected to happen.
22 |
23 | **Screenshots**
24 | If applicable, add screenshots to help explain your problem.
25 |
26 | **Desktop (please complete the following information):**
27 | - OS: [e.g. iOS]
28 | - Browser [e.g. chrome, safari]
29 | - Version [e.g. 22]
30 |
31 | **Smartphone (please complete the following information):**
32 | - Device: [e.g. iPhone6]
33 | - OS: [e.g. iOS8.1]
34 | - Browser [e.g. stock browser, safari]
35 | - Version [e.g. 22]
36 |
37 | **Additional context**
38 | Add any other context about the problem here.
39 |
--------------------------------------------------------------------------------
/rector.php:
--------------------------------------------------------------------------------
1 | parameters();
14 | $parameters->set(Option::PATHS, [
15 | __DIR__ . '/src'
16 | ]);
17 |
18 | // Define what rule sets will be applied
19 | $containerConfigurator->import(LevelSetList::UP_TO_PHP_80);
20 | $containerConfigurator->import(SetList::DEAD_CODE);
21 | $containerConfigurator->import(SetList::CODE_QUALITY);
22 |
23 | // get services (needed for register a single rule)
24 | $services = $containerConfigurator->services();
25 |
26 | // register a single rule
27 | $services->set(TypedPropertyRector::class);
28 | };
29 |
--------------------------------------------------------------------------------
/.github/workflows/php-ci.yml:
--------------------------------------------------------------------------------
1 | name: PHP CI Run
2 |
3 | on:
4 | push:
5 | branches: [ master ]
6 | pull_request:
7 | branches: [ master ]
8 |
9 | jobs:
10 | build:
11 | runs-on: ubuntu-latest
12 | strategy:
13 | matrix:
14 | php-versions: ['8.0', '8.1']
15 | name: PHP ${{ matrix.php-versions }} Test on Ubuntu
16 | steps:
17 | - uses: actions/checkout@v2
18 |
19 | - name: Validate composer.json and composer.lock
20 | run: composer validate --strict
21 |
22 | - name: Cache Composer packages
23 | id: composer-cache
24 | uses: actions/cache@v2
25 | with:
26 | path: vendor
27 | key: ${{ runner.os }}-php-${{ hashFiles('**/composer.lock') }}
28 | restore-keys: |
29 | ${{ runner.os }}-php-
30 |
31 | - name: Install dependencies
32 | run: composer install --prefer-dist --no-progress
33 |
34 | - name: Run CI suite for ${{ matrix.php-versions }}
35 | run: make build
36 |
--------------------------------------------------------------------------------
/phpunit.xml.dist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | ./src
6 |
7 |
8 |
9 |
10 | ./tests/Unit
11 |
12 |
13 | ./tests/Integration
14 |
15 |
16 |
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/src/Observability/Events/DNSQueryProfiled.php:
--------------------------------------------------------------------------------
1 | profile;
20 | }
21 |
22 | public static function getName(): string
23 | {
24 | return self::NAME;
25 | }
26 |
27 | public function toArray(): array
28 | {
29 | return [
30 | 'elapsedSeconds' => $this->profile->getElapsedSeconds(),
31 | 'transactionName' => $this->profile->getTransactionName(),
32 | 'peakMemoryUsage' => $this->profile->getPeakMemoryUsage(),
33 | ];
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/src/Entities/MXData.php:
--------------------------------------------------------------------------------
1 | priority} {$this->target}";
17 | }
18 |
19 | public function getTarget(): Hostname
20 | {
21 | return $this->target;
22 | }
23 |
24 | public function getPriority(): int
25 | {
26 | return $this->priority;
27 | }
28 |
29 | public function toArray(): array
30 | {
31 | return [
32 | 'target' => (string)$this->target,
33 | 'priority' => $this->priority,
34 | ];
35 | }
36 |
37 | public function __unserialize(array $unserialized): void
38 | {
39 | $this->target = new Hostname($unserialized['target']);
40 | $this->priority = $unserialized['priority'];
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/tests/Integration/ServicesTest.php:
--------------------------------------------------------------------------------
1 | createLocalSystemDNS();
17 |
18 | $records = $localDNS->getRecord('google.com', DNS_A);
19 | $this->assertNotEmpty($records);
20 | }
21 |
22 | /**
23 | * @test
24 | */
25 | public function localDNSServiceProvidesReverseLookup(): void
26 | {
27 | $localDNS = $this->createLocalSystemDNS();
28 |
29 | $hostname = $localDNS->getHostnameByAddress('127.0.0.1');
30 | $this->assertSame('localhost', $hostname);
31 | }
32 |
33 | /**
34 | * @test
35 | */
36 | public function throwsOnReverseLookupFailure(): void
37 | {
38 | $this->expectException(ReverseLookupFailure::class);
39 | $this->createLocalSystemDNS()->getHostnameByAddress('0.0.0.0');
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2017 Christian Thomas
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining
6 | a copy of this software and associated documentation files (the
7 | "Software"), to deal in the Software without restriction, including
8 | without limitation the rights to use, copy, modify, merge, publish,
9 | distribute, sublicense, and/or sell copies of the Software, and to
10 | permit persons to whom the Software is furnished to do so, subject to
11 | the following conditions:
12 |
13 | The above copyright notice and this permission notice shall be
14 | included in all copies or substantial portions of the Software.
15 |
16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
--------------------------------------------------------------------------------
/tests/Unit/BaseTestAbstract.php:
--------------------------------------------------------------------------------
1 | assertEquals($serializable, unserialize(serialize($serializable)));
18 | }
19 |
20 | protected function assertArrayableAndEquals(array $expected, Arrayable $arrayable)
21 | {
22 | $this->assertEquals($expected, $arrayable->toArray());
23 | }
24 |
25 | protected function assertJsonSerializeableAndEquals(array $expected, JsonSerializable $jsonSerializeable)
26 | {
27 | $this->assertEquals($expected, $jsonSerializeable->jsonSerialize());
28 | }
29 |
30 | protected function assertStringableAndEquals(string $expected, string $stringable)
31 | {
32 | $this->assertSame($expected, $stringable);
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | build:
2 | @make dependencies && make dependency-check && make static-analysis && make style-check && make unit-tests && make integration-tests
3 |
4 | dependencies:
5 | @composer install
6 |
7 | unit-tests:
8 | @vendor/bin/paratest -p8 --runner=WrapperRunner --bootstrap=./tests/bootstrap.php --testsuite Unit
9 |
10 | integration-tests:
11 | @vendor/bin/paratest -p8 --runner=WrapperRunner --bootstrap=./tests/bootstrap.php --testsuite Integration
12 |
13 | test-coverage-ci:
14 | @mkdir -p ./build/logs && ./vendor/bin/phpunit -c phpunit.xml.dist --coverage-clover ./build/logs/clover.xml && php vendor/bin/php-coveralls --root_dir=. -v
15 |
16 | test-coverage-html:
17 | @vendor/bin/paratest -p8 --runner=WrapperRunner --bootstrap=./tests/bootstrap.php --coverage-html ./coverage
18 |
19 | style-check:
20 | @vendor/bin/phpcs --standard=PSR12 ./src/* ./tests/*
21 |
22 | dependency-check:
23 | @vendor/bin/composer-require-checker check -vvv ./composer.json
24 |
25 | churn-report:
26 | @vendor/bin/churn run
27 |
28 | static-analysis:
29 | @vendor/bin/phpstan analyze --level=8 ./src && ./vendor/bin/psalm --show-info=false
30 |
31 | style-fix:
32 | @vendor/bin/phpcbf --standard=PSR12 ./src ./tests
33 |
34 | repl:
35 | @vendor/bin/psysh bootstrap/repl.php
--------------------------------------------------------------------------------
/src/Mappers/CloudFlare.php:
--------------------------------------------------------------------------------
1 | fields['type']);
21 | $IPAddress = (isset($this->fields[self::DATA]) && IPAddress::isValid($this->fields[self::DATA]))
22 | ? $this->fields[self::DATA]
23 | : null;
24 |
25 | $value = (isset($this->fields[self::DATA]) && !$IPAddress)
26 | ? str_ireplace('"', '', (string)$this->fields[self::DATA])
27 | : null;
28 |
29 | return DNSRecord::createFromPrimitives(
30 | (string)$type,
31 | $this->fields['name'],
32 | $this->fields['TTL'],
33 | $IPAddress,
34 | 'IN',
35 | $value
36 | );
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/.github/workflows/php-code-coverage.yml:
--------------------------------------------------------------------------------
1 | name: PHP Code Coverage
2 |
3 | on:
4 | push:
5 | branches: [ master ]
6 | pull_request:
7 | branches: [ master ]
8 |
9 | jobs:
10 | build:
11 | runs-on: ubuntu-latest
12 | strategy:
13 | matrix:
14 | php-versions: ['8.0']
15 | name: PHP Code Coverage
16 | steps:
17 | - uses: actions/checkout@v2
18 |
19 | - name: Validate composer.json and composer.lock
20 | run: composer validate --strict
21 |
22 | - name: Cache Composer packages
23 | id: composer-cache
24 | uses: actions/cache@v2
25 | with:
26 | path: vendor
27 | key: ${{ runner.os }}-php-${{ hashFiles('**/composer.lock') }}
28 | restore-keys: |
29 | ${{ runner.os }}-php-
30 |
31 | - name: Install dependencies
32 | run: composer install --prefer-dist --no-progress
33 |
34 | - name: Run test coverage and send to coveralls
35 | run: make test-coverage-ci
36 | env:
37 | GITHUB_RUN_ID: ${{ github.run_id }}
38 | GITHUB_EVENT_NAME: ${{ github.event_name }}
39 | COVERALLS_REPO_TOKEN: ${{ secrets.GITHUB_TOKEN }}
40 | XDEBUG_MODE: coverage
41 |
--------------------------------------------------------------------------------
/src/Mappers/GoogleDNS.php:
--------------------------------------------------------------------------------
1 | fields['type']);
22 | $IPAddress = (isset($this->fields[self::DATA]) && IPAddress::isValid($this->fields[self::DATA]))
23 | ? $this->fields[self::DATA]
24 | : null;
25 |
26 | $value = (isset($this->fields[self::DATA]) && !$IPAddress)
27 | ? str_ireplace('"', '', (string)$this->fields[self::DATA])
28 | : null;
29 |
30 | return DNSRecord::createFromPrimitives(
31 | (string)$type,
32 | $this->fields['name'],
33 | $this->fields['TTL'],
34 | $IPAddress,
35 | 'IN',
36 | $value
37 | );
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/src/Services/LocalSystemDNS.php:
--------------------------------------------------------------------------------
1 | time = $time ?? new Timer();
23 | }
24 |
25 | public function startTransaction(): void
26 | {
27 | $this->startTime = $this->time->getMicroTime();
28 | }
29 |
30 | public function endTransaction(): void
31 | {
32 | $this->stopTime = $this->time->getMicroTime();
33 | }
34 |
35 | public function getTransactionName(): string
36 | {
37 | return $this->transactionName;
38 | }
39 |
40 | public function getElapsedSeconds(): float
41 | {
42 | return $this->stopTime - $this->startTime;
43 | }
44 |
45 | public function samplePeakMemoryUsage(): void
46 | {
47 | $this->peakMemoryUsage = memory_get_peak_usage();
48 | }
49 |
50 | public function getPeakMemoryUsage(): int
51 | {
52 | return $this->peakMemoryUsage;
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/src/Entities/SRVData.php:
--------------------------------------------------------------------------------
1 | priority} {$this->weight} {$this->port} {$this->target}";
17 | }
18 |
19 | public function getPriority(): int
20 | {
21 | return $this->priority;
22 | }
23 |
24 | public function getWeight(): int
25 | {
26 | return $this->weight;
27 | }
28 |
29 | public function getPort(): int
30 | {
31 | return $this->port;
32 | }
33 |
34 | public function getTarget(): Hostname
35 | {
36 | return $this->target;
37 | }
38 |
39 | public function toArray(): array
40 | {
41 | return [
42 | 'priority' => $this->priority,
43 | 'weight' => $this->weight,
44 | 'port' => $this->port,
45 | 'target' => (string)$this->target,
46 | ];
47 | }
48 |
49 | public function __unserialize(array $unserialized): void
50 | {
51 | $this->priority = $unserialized['priority'];
52 | $this->weight = $unserialized['weight'];
53 | $this->port = $unserialized['port'];
54 | $this->target = new Hostname($unserialized['target']);
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/src/Observability/Events/DNSQueryFailed.php:
--------------------------------------------------------------------------------
1 | resolver;
26 | }
27 |
28 | public function getHostName(): Hostname
29 | {
30 | return $this->hostname;
31 | }
32 |
33 | public function getRecordType(): DNSRecordType
34 | {
35 | return $this->recordType;
36 | }
37 |
38 | public function getError(): Exception
39 | {
40 | return $this->error;
41 | }
42 |
43 | public static function getName(): string
44 | {
45 | return self::NAME;
46 | }
47 |
48 | public function toArray(): array
49 | {
50 | return [
51 | 'resolver' => $this->resolver->getName(),
52 | 'hostname' => (string)$this->hostname,
53 | 'type' => (string)$this->recordType,
54 | 'error' => $this->error,
55 | ];
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/src/Observability/Subscribers/STDIOSubscriber.php:
--------------------------------------------------------------------------------
1 | 'onDNSQueryFailed',
24 | DNSQueried::getName() => 'onDNSQueried',
25 | DNSQueryProfiled::getName() => 'onDNSQueryProfiled',
26 | ];
27 | }
28 |
29 | public function onDNSQueryFailed(ObservableEventAbstract $event): void
30 | {
31 | $this->STDERR->fwrite(json_encode($event, JSON_PRETTY_PRINT) . PHP_EOL);
32 | }
33 |
34 | public function onDNSQueried(ObservableEventAbstract $event): void
35 | {
36 | $this->STDOUT->fwrite(json_encode($event, JSON_PRETTY_PRINT) . PHP_EOL);
37 | }
38 |
39 | public function onDNSQueryProfiled(ObservableEventAbstract $event): void
40 | {
41 | $this->STDOUT->fwrite(json_encode($event, JSON_PRETTY_PRINT) . PHP_EOL);
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "remotelyliving/php-dns",
3 | "description": "A php library for abstracting DNS querying",
4 | "type": "library",
5 | "license": "MIT",
6 | "authors": [
7 | {
8 | "name": "chthomas",
9 | "email": "christian.h.thomas@me.com"
10 | }
11 | ],
12 | "minimum-stability": "stable",
13 | "require": {
14 | "php": ">=8.0",
15 | "ext-json": "*",
16 | "ext-filter": "*",
17 | "ext-intl": "*",
18 | "ext-mbstring": "*",
19 | "guzzlehttp/guzzle": "^7.0 || ^6.0",
20 | "psr/cache": "^1.0 || ^2.0",
21 | "symfony/event-dispatcher": "^6.0 || ^5.0 || ^4.0 || ^3.0",
22 | "psr/log": "^1.0 || ^2.0 || ^3.0",
23 | "guzzlehttp/promises": "^1.3",
24 | "spatie/dns": "^2.0"
25 | },
26 | "require-dev": {
27 | "phpunit/phpunit": "^9.0",
28 | "maglnet/composer-require-checker": "@stable",
29 | "squizlabs/php_codesniffer": "^3.3",
30 | "psy/psysh": "^0.9.9",
31 | "php-coveralls/php-coveralls": "^2.1",
32 | "symfony/cache": "^4.3",
33 | "vimeo/psalm": "^4.10",
34 | "rector/rector": "^0.12.8",
35 | "bmitch/churn-php": "^1.5",
36 | "brianium/paratest": "^6.4"
37 | },
38 | "autoload": {
39 | "psr-4": {
40 | "RemotelyLiving\\PHPDNS\\" : "src/"
41 | }
42 | },
43 | "autoload-dev": {
44 | "psr-4": {
45 | "RemotelyLiving\\PHPDNS\\Tests\\" : "tests/"
46 | }
47 | },
48 | "config": {
49 | "allow-plugins": {
50 | "composer/package-versions-deprecated": true
51 | }
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/tests/Unit/Observability/Events/ObservableEventAbstractTest.php:
--------------------------------------------------------------------------------
1 | event = new class extends ObservableEventAbstract
19 | {
20 | public static function getName(): string
21 | {
22 | return 'the name';
23 | }
24 |
25 | public function toArray(): array
26 | {
27 | return ['beep' => 'boop'];
28 | }
29 | };
30 | }
31 |
32 | /**
33 | * @test
34 | */
35 | public function getsName(): void
36 | {
37 | $this->assertSame('the name', $this->event::getName());
38 | }
39 |
40 | /**
41 | * @test
42 | */
43 | public function isArrayable(): void
44 | {
45 | $this->assertInstanceOf(Arrayable::class, $this->event);
46 | $this->assertEquals(['beep' => 'boop'], $this->event->toArray());
47 | }
48 |
49 | /**
50 | * @test
51 | */
52 | public function isJsonSerializable(): void
53 | {
54 | $this->assertInstanceOf(JsonSerializable::class, $this->event);
55 | $this->assertEquals(['the name' => ['beep' => 'boop']], $this->event->jsonSerialize());
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/src/Entities/IPAddress.php:
--------------------------------------------------------------------------------
1 | IPAddress = $IPAddress;
26 | }
27 |
28 | public function __toString(): string
29 | {
30 | return $this->IPAddress;
31 | }
32 |
33 | public static function isValid(string $IPAddress): bool
34 | {
35 | return (bool) filter_var($IPAddress, FILTER_VALIDATE_IP);
36 | }
37 |
38 | public static function createFromString(string $IPAddress): IPAddress
39 | {
40 | return new self($IPAddress);
41 | }
42 |
43 | public function equals(IPAddress $IPAddress): bool
44 | {
45 | return $this->IPAddress === (string)$IPAddress;
46 | }
47 |
48 | public function getIPAddress(): string
49 | {
50 | return $this->IPAddress;
51 | }
52 |
53 | public function isIPv6(): bool
54 | {
55 | return (bool) filter_var($this->IPAddress, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6);
56 | }
57 |
58 | public function isIPv4(): bool
59 | {
60 | return (bool) filter_var($this->IPAddress, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4);
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/bootstrap/repl.php:
--------------------------------------------------------------------------------
1 | addSubscriber($IOSubscriber);
29 |
30 | $googleDNSResolver = new GoogleDNS();
31 | $googleDNSResolver->addSubscriber($IOSubscriber);
32 |
33 | $cloudFlareResolver = new CloudFlare();
34 | $cloudFlareResolver->addSubscriber($IOSubscriber);
35 |
36 | $digResolver = new \RemotelyLiving\PHPDNS\Resolvers\Dig(new Dns(), new Dig());
37 | $digResolver->addSubscriber($IOSubscriber);
38 |
39 | $chainResolver = new Chain($cloudFlareResolver, $googleDNSResolver, $localSystemResolver);
40 | $cachedResolver = new Cached(new FilesystemAdapter(), $chainResolver);
41 | $cachedResolver->addSubscriber($IOSubscriber);
--------------------------------------------------------------------------------
/tests/Unit/Entities/DNSRecordTypeTest.php:
--------------------------------------------------------------------------------
1 | assertSame($type, (string)$createdType);
20 | $this->assertEquals($createdType, DNSRecordType::createFromInt($createdType->toInt()));
21 | }
22 | }
23 |
24 | /**
25 | * @test
26 | */
27 | public function onlyAllowsValidTypesFromStrings(): void
28 | {
29 | $this->expectException(InvalidArgumentException::class);
30 |
31 | DNSRecordType::createFromString('SDFSF');
32 | }
33 |
34 | /**
35 | * @test
36 | */
37 | public function onlyAllowsValidTypesFromIntCodes(): void
38 | {
39 | $this->expectException(InvalidArgumentException::class);
40 |
41 | DNSRecordType::createFromInt(-100);
42 | }
43 |
44 | /**
45 | * @test
46 | */
47 | public function comparesItself(): void
48 | {
49 | $aRecord = DNSRecordType::createFromString('A');
50 | $cnameRecord = DNSRecordType::createFromString('CNAME');
51 | $otherARecord = DNSRecordType::createFromString('A');
52 |
53 | $this->assertFalse($aRecord->equals($cnameRecord));
54 | $this->assertTrue($otherARecord->equals($aRecord));
55 |
56 | $this->assertTrue($aRecord->isA('A'));
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/src/Resolvers/LocalSystem.php:
--------------------------------------------------------------------------------
1 | systemDNS = $systemDNS ?? new LocalDNSService();
23 | $this->mapper = $mapper ?? new LocalMapper();
24 | }
25 |
26 | public function getHostnameByAddress(string $IPAddress): Hostname
27 | {
28 | $result = $this->systemDNS->getHostnameByAddress((string)(new IPAddress($IPAddress)));
29 |
30 | return Hostname::createFromString($result);
31 | }
32 |
33 | protected function doQuery(Hostname $hostname, DNSRecordType $recordType): DNSRecordCollection
34 | {
35 | $results = $this->systemDNS->getRecord(
36 | $hostname->getHostnameWithoutTrailingDot(), // dns_get_record doesn't like trailing dot as much!
37 | $this->mapper->getTypeCodeFromType($recordType)
38 | );
39 |
40 | $collection = new DNSRecordCollection();
41 |
42 | foreach ($results as $result) {
43 | $collection[] = $this->mapper->mapFields($result)->toDNSRecord();
44 | }
45 |
46 | return $collection;
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/src/Entities/CAAData.php:
--------------------------------------------------------------------------------
1 | value = ($value)
20 | ? $this->normalizeValue($value)
21 | : null;
22 | }
23 |
24 | public function __toString(): string
25 | {
26 | return "{$this->flags} {$this->tag} \"{$this->value}\"";
27 | }
28 |
29 | public function __unserialize(array $unserialized): void
30 | {
31 | $this->flags = $unserialized['flags'];
32 | $this->tag = $unserialized['tag'];
33 | $this->value = $unserialized['value'];
34 | }
35 |
36 | public function getFlags(): int
37 | {
38 | return $this->flags;
39 | }
40 |
41 | public function getTag(): string
42 | {
43 | return $this->tag;
44 | }
45 |
46 | public function getValue(): ?string
47 | {
48 | return $this->value;
49 | }
50 |
51 | public function toArray(): array
52 | {
53 | return [
54 | 'flags' => $this->flags,
55 | 'tag' => $this->tag,
56 | 'value' => $this->value,
57 | ];
58 | }
59 |
60 | private function normalizeValue(string $value): string
61 | {
62 | $normalized = trim(str_ireplace('"', '', $value));
63 |
64 | if (preg_match('/\s/m', $normalized)) {
65 | throw new Exceptions\InvalidArgumentException("$value is not a valid CAA value");
66 | }
67 |
68 | return $normalized;
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/src/Resolvers/Interfaces/DNSQuery.php:
--------------------------------------------------------------------------------
1 | profile = new Profile('transactionName');
23 | $this->profileFactory = $this->createMock(ProfileFactory::class);
24 | $this->profileFactory->method('create')
25 | ->with('transactionName')
26 | ->willReturn($this->profile);
27 |
28 | $this->profileableClass = new class {
29 | use Profileable;
30 | };
31 | }
32 |
33 | /**
34 | * @test
35 | */
36 | public function createsProfiles(): void
37 | {
38 | $profile = $this->profileableClass->createProfile('name');
39 | $this->assertInstanceOf(Profile::class, $profile);
40 | $this->assertSame('name', $profile->getTransactionName());
41 | }
42 |
43 | /**
44 | * @test
45 | */
46 | public function setsAProfileFactory(): void
47 | {
48 | $this->profileableClass->setProfileFactory($this->profileFactory);
49 | $profile = $this->profileableClass->createProfile('transactionName');
50 |
51 | $this->assertInstanceOf(Profile::class, $profile);
52 | $this->assertSame('transactionName', $profile->getTransactionName());
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/tests/Unit/Entities/TXTDataTest.php:
--------------------------------------------------------------------------------
1 | TXTData = new TXTData($this->value);
22 | }
23 |
24 | /**
25 | * @test
26 | */
27 | public function knowsIfEquals(): void
28 | {
29 | $this->assertTrue($this->TXTData->equals($this->TXTData));
30 | $this->assertFalse($this->TXTData->equals(new TXTData('beep')));
31 | }
32 |
33 | /**
34 | * @test
35 | */
36 | public function isArrayable(): void
37 | {
38 | $this->assertArrayableAndEquals(['value' => $this->value], $this->TXTData);
39 | }
40 |
41 | /**
42 | * @test
43 | */
44 | public function isJsonSerializeable(): void
45 | {
46 | $this->assertJsonSerializeableAndEquals(['value' => $this->value], $this->TXTData);
47 | }
48 |
49 | /**
50 | * @test
51 | */
52 | public function isSerializable(): void
53 | {
54 | $this->assertSerializable($this->TXTData);
55 | $this->assertEquals($this->TXTData, unserialize(serialize($this->TXTData)));
56 | }
57 |
58 | /**
59 | * @test
60 | */
61 | public function isStringable(): void
62 | {
63 | $this->assertStringableAndEquals('boop', $this->TXTData);
64 | }
65 |
66 | /**
67 | * @test
68 | */
69 | public function hasBasicGetters(): void
70 | {
71 | $this->assertSame($this->value, $this->TXTData->getValue());
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/churn.yml:
--------------------------------------------------------------------------------
1 | # The maximum number of files to display in the results table.
2 | # Default: 10
3 | filesToShow: 20
4 |
5 | # The minimum score a file need to display in the results table.
6 | # Disabled if null.
7 | # Default: 0.1
8 | minScoreToShow: 0
9 |
10 | # The command returns an 1 exit code if the highest score is greater than the threshold.
11 | # Disabled if null.
12 | # Default: null
13 | maxScoreThreshold: 0.9
14 |
15 | # The number of parallel jobs to use when processing files.
16 | # Default: 10
17 | parallelJobs: 10
18 |
19 | # How far back in the VCS history to count the number of commits to a file
20 | # Can be a human readable date like 'One week ago' or a date like '2017-07-12'
21 | # Default: '10 Years ago'
22 | commitsSince: One year ago
23 |
24 | # Files to ignore when processing. The full path to the file relative to the root of your project is required.
25 | # Also supports regular expressions.
26 | # Default: All PHP files in the path provided to churn-php are processed.
27 | #filesToIgnore:
28 |
29 | # File extensions to use when processing.
30 | # Default: php
31 | fileExtensions:
32 | - php
33 | - inc
34 |
35 | # This list is used only if there is no argument when running churn.
36 | # Default:
37 | directoriesToScan:
38 | - src
39 | - tests/
40 |
41 | # List of user-defined hooks.
42 | # They can be referenced by their full qualified class name if churn has access to the autoloader.
43 | # Otherwise the file path can be used as well.
44 | # See below the section about hooks for more details.
45 | # Default:
46 | #hooks:
47 |
48 | # The version control system used for your project.
49 | # Accepted values: fossil, git, mercurial, subversion, none
50 | # Default: git
51 | vcs: git
52 |
53 | # The path of the cache file. It doesn't need to exist before running churn.
54 | # Disabled if null.
55 | # Default: null
56 | cachePath: .churn.cache
57 |
--------------------------------------------------------------------------------
/tests/Unit/Observability/Events/DNSQueryProfiledTest.php:
--------------------------------------------------------------------------------
1 | profile = $this->createMock(ProfileInterface::class);
22 | $this->DNSQueryProfiled = new DNSQueryProfiled($this->profile);
23 | }
24 |
25 | /**
26 | * @test
27 | */
28 | public function hasBasicGetters(): void
29 | {
30 | $this->assertSame($this->profile, $this->DNSQueryProfiled->getProfile());
31 | $this->assertSame(DNSQueryProfiled::NAME, $this->DNSQueryProfiled::getName());
32 | }
33 |
34 | /**
35 | * @test
36 | */
37 | public function isArrayable(): void
38 | {
39 | $this->profile->method('getElapsedSeconds')
40 | ->willReturn(100.1);
41 |
42 | $this->profile->method('getTransactionName')
43 | ->willReturn('transactionName');
44 |
45 | $this->profile->method('getPeakMemoryUsage')
46 | ->willReturn(123);
47 |
48 | $expected = [
49 | 'elapsedSeconds' => 100.1,
50 | 'transactionName' => 'transactionName',
51 | 'peakMemoryUsage' => 123,
52 | ];
53 |
54 | $this->assertInstanceOf(Arrayable::class, $this->DNSQueryProfiled);
55 | $this->assertEquals($expected, $this->DNSQueryProfiled->toArray());
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/tests/Unit/Entities/NSDataTest.php:
--------------------------------------------------------------------------------
1 | target = new Hostname('google.com');
23 | $this->NSData = new NSData($this->target);
24 | }
25 |
26 | /**
27 | * @test
28 | */
29 | public function knowsIfEquals(): void
30 | {
31 | $this->assertTrue($this->NSData->equals($this->NSData));
32 | $this->assertFalse($this->NSData->equals(new NSData(new Hostname('boop.com'))));
33 | }
34 |
35 | /**
36 | * @test
37 | */
38 | public function isArrayable(): void
39 | {
40 | $this->assertArrayableAndEquals(['target' => (string)$this->target], $this->NSData);
41 | }
42 |
43 | /**
44 | * @test
45 | */
46 | public function isJsonSerializeable(): void
47 | {
48 | $this->assertJsonSerializeableAndEquals(['target' => (string)$this->target], $this->NSData);
49 | }
50 |
51 | /**
52 | * @test
53 | */
54 | public function isSerializable(): void
55 | {
56 | $this->assertSerializable($this->NSData);
57 | $this->assertEquals($this->NSData, unserialize(serialize($this->NSData)));
58 | }
59 |
60 | /**
61 | * @test
62 | */
63 | public function isStringable(): void
64 | {
65 | $this->assertStringableAndEquals('google.com.', $this->NSData);
66 | }
67 |
68 | /**
69 | * @test
70 | */
71 | public function hasBasicGetters(): void
72 | {
73 | $this->assertSame($this->target, $this->NSData->getTarget());
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/tests/Unit/Entities/IPAddressTest.php:
--------------------------------------------------------------------------------
1 | IPAddress = new IPAddress('127.0.0.1');
18 | }
19 |
20 | /**
21 | * @test
22 | */
23 | public function hasBasicGettersAndIsStringy(): void
24 | {
25 | $this->assertSame('127.0.0.1', (string)$this->IPAddress);
26 | $this->assertSame('127.0.0.1', $this->IPAddress->getIPAddress());
27 | }
28 |
29 | /**
30 | * @test
31 | */
32 | public function testsForEquality(): void
33 | {
34 | $IPAddress1 = IPAddress::createFromString('127.0.0.1');
35 | $IPAddress2 = IPAddress::createFromString('192.168.1.1');
36 | $IPAddress3 = IPAddress::createFromString('127.0.0.1');
37 |
38 | $this->assertFalse($IPAddress1->equals($IPAddress2));
39 | $this->assertTrue($IPAddress3->equals($IPAddress1));
40 | }
41 |
42 | /**
43 | * @test
44 | */
45 | public function doesNotAllowInvalidHostNames(): void
46 | {
47 | $this->expectException(InvalidArgumentException::class);
48 | $this->assertFalse(IPAddress::isValid('lskjf'));
49 | IPAddress::createFromString('127.1.1');
50 | }
51 |
52 | /**
53 | * @test
54 | */
55 | public function knowsWhatVersionItIs(): void
56 | {
57 | $IPv4 = IPAddress::createFromString('127.0.0.1');
58 | $IPv6 = IPAddress::createFromString('2001:0db8:85a3:0000:0000:8a2e:0370:7334');
59 |
60 | $this->assertTrue($IPv4->isIPv4());
61 | $this->assertFalse($IPv4->isIPv6());
62 | $this->assertTrue($IPv6->isIPv6());
63 | $this->assertFalse($IPv6->isIPv4());
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/tests/Unit/Observability/Performance/ProfileTest.php:
--------------------------------------------------------------------------------
1 | time = $this->createMock(Time::class);
23 | $this->time->method('getMicroTime')
24 | ->willReturnOnConsecutiveCalls(1.0, 5.0);
25 |
26 | $this->factory = new ProfileFactory();
27 | $this->profile = new Profile('transactionName', $this->time);
28 | }
29 |
30 | /**
31 | * @test
32 | */
33 | public function timesTransactions(): void
34 | {
35 | $this->profile->startTransaction();
36 | $this->profile->endTransaction();
37 |
38 | $this->assertSame(4.0, $this->profile->getElapsedSeconds());
39 | }
40 |
41 | /**
42 | * @test
43 | */
44 | public function getsPeakMemoryUsage(): void
45 | {
46 | $this->profile->samplePeakMemoryUsage();
47 | $this->assertSame($this->profile->getPeakMemoryUsage(), memory_get_peak_usage());
48 | }
49 |
50 | /**
51 | * @test
52 | */
53 | public function getsATransactionName(): void
54 | {
55 | $this->assertSame('transactionName', $this->profile->getTransactionName());
56 | }
57 |
58 | /**
59 | * @test
60 | */
61 | public function hasAnAccompanyingFactory(): void
62 | {
63 | $this->assertEquals(new Profile('transactionName'), $this->factory->create('transactionName'));
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/tests/Unit/Entities/PTRDataTest.php:
--------------------------------------------------------------------------------
1 | hostname = new Hostname('google.com');
20 | $this->PTRData = new PTRData($this->hostname);
21 | }
22 |
23 | /**
24 | * @test
25 | */
26 | public function knowsIfEquals(): void
27 | {
28 | $this->assertTrue($this->PTRData->equals($this->PTRData));
29 | $this->assertFalse($this->PTRData->equals(new PTRData(new Hostname('boop.com'))));
30 | }
31 |
32 | /**
33 | * @test
34 | */
35 | public function isArrayable(): void
36 | {
37 | $this->assertArrayableAndEquals(
38 | ['hostname' => (string)$this->hostname],
39 | $this->PTRData
40 | );
41 | }
42 |
43 | /**
44 | * @test
45 | */
46 | public function isJsonSerializable(): void
47 | {
48 | $this->assertJsonSerializeableAndEquals(
49 | ['hostname' => (string)$this->hostname],
50 | $this->PTRData
51 | );
52 | }
53 |
54 | /**
55 | * @test
56 | */
57 | public function isSerializable(): void
58 | {
59 | $this->assertSerializable($this->PTRData);
60 | }
61 |
62 | /**
63 | * @test
64 | */
65 | public function isStringable(): void
66 | {
67 | $this->assertStringableAndEquals('google.com.', $this->PTRData);
68 | $this->assertEquals($this->PTRData, unserialize(serialize($this->PTRData)));
69 | }
70 |
71 | /**
72 | * @test
73 | */
74 | public function hasBasicGetters(): void
75 | {
76 | $this->assertSame($this->hostname, $this->PTRData->getHostname());
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/src/Observability/Events/DNSQueried.php:
--------------------------------------------------------------------------------
1 | recordCollection = $recordCollection ?? new DNSRecordCollection();
24 | }
25 |
26 | public function getResolver(): Resolver
27 | {
28 | return $this->resolver;
29 | }
30 |
31 | public function getHostname(): Hostname
32 | {
33 | return $this->hostname;
34 | }
35 |
36 | public function getRecordType(): DNSRecordType
37 | {
38 | return $this->recordType;
39 | }
40 |
41 | public function getRecordCollection(): DNSRecordCollection
42 | {
43 | return $this->recordCollection;
44 | }
45 |
46 | public static function getName(): string
47 | {
48 | return self::NAME;
49 | }
50 |
51 | public function toArray(): array
52 | {
53 | return [
54 | 'resolver' => $this->resolver->getName(),
55 | 'hostname' => (string)$this->hostname,
56 | 'type' => (string)$this->recordType,
57 | 'records' => $this->formatCollection($this->recordCollection),
58 | 'empty' => $this->recordCollection->isEmpty(),
59 | ];
60 | }
61 |
62 | private function formatCollection(DNSRecordCollection $recordCollection): array
63 | {
64 | $formatted = [];
65 |
66 | foreach ($recordCollection as $record) {
67 | if ($record !== null) {
68 | $formatted[] = $record->toArray();
69 | }
70 | }
71 |
72 | return $formatted;
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/tests/Unit/Entities/CNAMEDataTest.php:
--------------------------------------------------------------------------------
1 | hostname = new Hostname('google.com');
23 | $this->CNAMEData = new CNAMEData($this->hostname);
24 | }
25 |
26 | /**
27 | * @test
28 | */
29 | public function knowsIfEquals(): void
30 | {
31 | $this->assertTrue($this->CNAMEData->equals($this->CNAMEData));
32 | $this->assertFalse($this->CNAMEData->equals(new CNAMEData(new Hostname('boop.com'))));
33 | }
34 |
35 | /**
36 | * @test
37 | */
38 | public function isArrayable(): void
39 | {
40 | $this->assertArrayableAndEquals(
41 | ['hostname' => (string)$this->hostname],
42 | $this->CNAMEData
43 | );
44 | }
45 |
46 | /**
47 | * @test
48 | */
49 | public function isJsonSerializable(): void
50 | {
51 | $this->assertJsonSerializeableAndEquals(
52 | ['hostname' => (string)$this->hostname],
53 | $this->CNAMEData
54 | );
55 | }
56 |
57 | /**
58 | * @test
59 | */
60 | public function isSerializable(): void
61 | {
62 | $this->assertSerializable($this->CNAMEData);
63 | }
64 |
65 | /**
66 | * @test
67 | */
68 | public function isStringable(): void
69 | {
70 | $this->assertStringableAndEquals('google.com.', $this->CNAMEData);
71 | $this->assertEquals($this->CNAMEData, unserialize(serialize($this->CNAMEData)));
72 | }
73 |
74 | /**
75 | * @test
76 | */
77 | public function hasBasicGetters(): void
78 | {
79 | $this->assertSame($this->hostname, $this->CNAMEData->getHostname());
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/tests/Unit/Mappers/CloudFlareTest.php:
--------------------------------------------------------------------------------
1 | 1, 'TTL' => 343, 'data' => '127.0.0.1', 'name' => 'yelp.com.',
16 | ],
17 | [
18 | 'type' => 5, 'TTL' => 343, 'name' => 'google.com.',
19 | ],
20 | [
21 | 'type' => 38, 'TTL' => 343, 'name' => 'google.com.',
22 | ],
23 | ];
24 |
25 | protected function setUp(): void
26 | {
27 | parent::setUp();
28 |
29 | $this->mapper = new CloudFlare();
30 | }
31 |
32 | /**
33 | * @test
34 | */
35 | public function mapsACloudFlareDNSRecordVariants(): void
36 | {
37 | $mappedRecord = $this->mapper->mapFields([
38 | 'type' => 1,
39 | 'TTL' => 365,
40 | 'data' => '127.0.0.1',
41 | 'name' => 'facebook.com.',
42 | ])->toDNSRecord();
43 |
44 | $this->assertEquals(
45 | DNSRecord::createFromPrimitives('A', 'facebook.com', 365, '127.0.0.1'),
46 | $mappedRecord
47 | );
48 |
49 | $mappedRecord = $this->mapper->mapFields([
50 | 'type' => 16,
51 | 'TTL' => 365,
52 | 'name' => 'facebook.com.',
53 | 'data' => '"txtval"',
54 | ])->toDNSRecord();
55 |
56 | $this->assertEquals(
57 | DNSRecord::createFromPrimitives('TXT', 'facebook.com', 365, null, 'IN', 'txtval'),
58 | $mappedRecord
59 | );
60 | }
61 |
62 | /**
63 | * @test
64 | */
65 | public function mapsAllKindsOfGoogleDNSRecordVariants(): void
66 | {
67 | foreach (self::CLOUD_FLARE_DNS_FORMAT as $record) {
68 | $mappedRecord = $this->mapper->mapFields($record)->toDNSRecord();
69 | $this->assertInstanceOf(DNSRecord::class, $mappedRecord);
70 | }
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/src/Entities/Hostname.php:
--------------------------------------------------------------------------------
1 | normalizeHostName($hostname);
24 |
25 | if (filter_var($hostname, FILTER_VALIDATE_DOMAIN) !== $hostname) {
26 | throw new InvalidArgumentException("{$hostname} is not a valid hostname");
27 | }
28 |
29 | $this->hostname = $hostname;
30 | }
31 |
32 | public function __toString(): string
33 | {
34 | return $this->hostname;
35 | }
36 |
37 | public function equals(Hostname $hostname): bool
38 | {
39 | return $this->hostname === (string)$hostname;
40 | }
41 |
42 | public static function createFromString(string $hostname): Hostname
43 | {
44 | return new self($hostname);
45 | }
46 |
47 | public function getHostName(): string
48 | {
49 | return $this->hostname;
50 | }
51 |
52 | public function getHostnameWithoutTrailingDot(): string
53 | {
54 | return substr($this->hostname, 0, -1);
55 | }
56 |
57 | public function isPunycoded(): bool
58 | {
59 | return $this->toUTF8() !== $this->hostname;
60 | }
61 |
62 | public function toUTF8(): string
63 | {
64 | return (string)idn_to_utf8($this->hostname, IDNA_ERROR_PUNYCODE, INTL_IDNA_VARIANT_UTS46);
65 | }
66 |
67 | private static function punyCode(string $hostname): string
68 | {
69 | return (string)idn_to_ascii($hostname, IDNA_ERROR_PUNYCODE, INTL_IDNA_VARIANT_UTS46);
70 | }
71 |
72 | private function normalizeHostName(string $hostname): string
73 | {
74 | $hostname = self::punyCode(mb_strtolower(trim($hostname)));
75 |
76 | if (!str_ends_with($hostname, '.')) {
77 | return "{$hostname}.";
78 | }
79 |
80 | return $hostname;
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/tests/Unit/Entities/MXDataTest.php:
--------------------------------------------------------------------------------
1 | target = new Hostname('google.com');
25 | $this->MXData = new MXData($this->target, $this->priority);
26 | }
27 |
28 | /**
29 | * @test
30 | */
31 | public function knowsIfEquals(): void
32 | {
33 | $this->assertTrue($this->MXData->equals($this->MXData));
34 | $this->assertFalse($this->MXData->equals(new MXData(new Hostname('boop.com'), 60)));
35 | }
36 |
37 | /**
38 | * @test
39 | */
40 | public function isArrayable(): void
41 | {
42 | $this->assertArrayableAndEquals(
43 | ['target' => (string)$this->target, 'priority' => $this->priority],
44 | $this->MXData
45 | );
46 | }
47 |
48 | /**
49 | * @test
50 | */
51 | public function isJsonSerializeable(): void
52 | {
53 | $this->assertJsonSerializeableAndEquals(
54 | ['target' => (string)$this->target, 'priority' => $this->priority],
55 | $this->MXData
56 | );
57 | }
58 |
59 | /**
60 | * @test
61 | */
62 | public function isSerializable(): void
63 | {
64 | $this->assertSerializable($this->MXData);
65 | $this->assertEquals($this->MXData, unserialize(serialize($this->MXData)));
66 | }
67 |
68 | /**
69 | * @test
70 | */
71 | public function isStringable(): void
72 | {
73 | $this->assertStringableAndEquals('60 google.com.', $this->MXData);
74 | }
75 |
76 | /**
77 | * @test
78 | */
79 | public function hasBasicGetters(): void
80 | {
81 | $this->assertSame($this->target, $this->MXData->getTarget());
82 | $this->assertSame($this->priority, $this->MXData->getPriority());
83 | }
84 | }
85 |
--------------------------------------------------------------------------------
/src/Observability/Traits/Dispatcher.php:
--------------------------------------------------------------------------------
1 | dispatcher = $dispatcher;
22 | }
23 |
24 | public function addSubscriber(EventSubscriberInterface $subscriber): void
25 | {
26 | $this->getDispatcher()->addSubscriber($subscriber);
27 | }
28 |
29 | public function addListener(string $eventName, callable $listener, int $priority = 0): void
30 | {
31 | $this->getDispatcher()->addListener($eventName, $listener, $priority);
32 | }
33 |
34 | public function dispatch(ObservableEventAbstract $event): void
35 | {
36 | call_user_func_array([$this->getDispatcher(), 'dispatch'], $this->getOrderedDispatcherArguments($event));
37 | }
38 |
39 | private function getOrderedDispatcherArguments(ObservableEventAbstract $event): array
40 | {
41 | $reflection = new ReflectionClass($this->getDispatcher());
42 | $args = [];
43 |
44 | foreach ($reflection->getMethods(ReflectionMethod::IS_PUBLIC) as $method) {
45 | if ($method->getName() !== 'dispatch') {
46 | continue;
47 | }
48 | // @codeCoverageIgnoreStart
49 | foreach ($method->getParameters() as $parameter) {
50 | $args = ($parameter->getName() === 'event')
51 | ? [$event, $event::getName()]
52 | : [$event::getName(), $event];
53 | break;
54 | }
55 | // @codeCoverageIgnoreEnd
56 | }
57 |
58 | return $args;
59 | }
60 |
61 | private function getDispatcher(): EventDispatcherInterface
62 | {
63 | if ($this->dispatcher === null) {
64 | $this->dispatcher = new EventDispatcher();
65 | }
66 |
67 | return $this->dispatcher;
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/src/Entities/SOAData.php:
--------------------------------------------------------------------------------
1 | toArray());
30 | }
31 |
32 | public function getMname(): Hostname
33 | {
34 | return $this->mname;
35 | }
36 |
37 | public function getRname(): Hostname
38 | {
39 | return $this->rname;
40 | }
41 |
42 | public function getSerial(): int
43 | {
44 | return $this->serial;
45 | }
46 |
47 | public function getRefresh(): int
48 | {
49 | return $this->refresh;
50 | }
51 |
52 | public function getRetry(): int
53 | {
54 | return $this->retry;
55 | }
56 |
57 | public function getExpire(): int
58 | {
59 | return $this->expire;
60 | }
61 |
62 | public function getMinTTL(): int
63 | {
64 | return $this->minTTL;
65 | }
66 |
67 |
68 | public function toArray(): array
69 | {
70 | return [
71 | 'mname' => (string)$this->mname,
72 | 'rname' => (string)$this->rname,
73 | 'serial' => $this->serial,
74 | 'refresh' => $this->refresh,
75 | 'retry' => $this->retry,
76 | 'expire' => $this->expire,
77 | 'minimumTTL' => $this->minTTL,
78 | ];
79 | }
80 |
81 | public function __unserialize(array $unserialized): void
82 | {
83 | $this->mname = new Hostname($unserialized['mname']);
84 | $this->rname = new Hostname($unserialized['rname']);
85 | $this->serial = $unserialized['serial'];
86 | $this->refresh = $unserialized['refresh'];
87 | $this->retry = $unserialized['retry'];
88 | $this->expire = $unserialized['expire'];
89 | $this->minTTL = $unserialized['minimumTTL'];
90 | }
91 | }
92 |
--------------------------------------------------------------------------------
/psalm.xml:
--------------------------------------------------------------------------------
1 |
2 |
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 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
--------------------------------------------------------------------------------
/tests/Integration/BaseTestAbstract.php:
--------------------------------------------------------------------------------
1 | createLocalSystemResolver());
66 | }
67 |
68 | protected function createStdIOSubscriber(): EventSubscriberInterface
69 | {
70 | return new STDIOSubscriber(new SplFileObject('php://stdout'), new SplFileObject('php://stderr'));
71 | }
72 |
73 | protected function attachTestSubscribers(ResolverAbstract $resolver): void
74 | {
75 | $subscribers = [$this->createStdIOSubscriber()];
76 |
77 | foreach ($subscribers as $subscriber) {
78 | $resolver->addSubscriber($subscriber);
79 | }
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/tests/Unit/Mappers/GoogleDNSTest.php:
--------------------------------------------------------------------------------
1 | 1, 'TTL' => 343, 'data' => '127.0.0.1', 'name' => 'yelp.com.',
16 | ],
17 | [
18 | 'type' => 5, 'TTL' => 343, 'name' => 'google.com.',
19 | ],
20 | [
21 | 'type' => 38, 'TTL' => 343, 'name' => 'google.com.',
22 | ],
23 | ];
24 |
25 | protected function setUp(): void
26 | {
27 | parent::setUp();
28 |
29 | $this->mapper = new GoogleDNS();
30 | }
31 |
32 | /**
33 | * @test
34 | */
35 | public function mapsAGoogleDNSRecordVariants(): void
36 | {
37 | $mappedRecord = $this->mapper->mapFields([
38 | 'type' => 1,
39 | 'TTL' => 365,
40 | 'data' => '127.0.0.1',
41 | 'name' => 'facebook.com.',
42 | ])->toDNSRecord();
43 |
44 |
45 | $this->assertEquals(
46 | DNSRecord::createFromPrimitives('A', 'facebook.com', 365, '127.0.0.1'),
47 | $mappedRecord
48 | );
49 |
50 | $mappedRecord = $this->mapper->mapFields([
51 | 'type' => 16,
52 | 'TTL' => 365,
53 | 'name' => 'facebook.com.',
54 | 'data' => '"txtval"',
55 | ])->toDNSRecord();
56 |
57 | $this->assertEquals(
58 | DNSRecord::createFromPrimitives('TXT', 'facebook.com', 365, null, 'IN', 'txtval'),
59 | $mappedRecord
60 | );
61 |
62 | $mappedRecord = $this->mapper->mapFields([
63 | 'type' => 12,
64 | 'TTL' => 18930,
65 | 'name' => '8.8.8.8.in-addr.arpa.',
66 | 'data' => 'dns.google.'
67 | ])->toDNSRecord();
68 |
69 | $this->assertEquals(
70 | DNSRecord::createFromPrimitives('PTR', '8.8.8.8.in-addr.arpa.', 18930, null, 'IN', 'dns.google.'),
71 | $mappedRecord
72 | );
73 | }
74 |
75 | /**
76 | * @test
77 | */
78 | public function mapsAllKindsOfGoogleDNSRecordVariants(): void
79 | {
80 | foreach (self::GOOGLE_DNS_FORMAT as $record) {
81 | $mappedRecord = $this->mapper->mapFields($record)->toDNSRecord();
82 | $this->assertInstanceOf(DNSRecord::class, $mappedRecord);
83 | }
84 | }
85 | }
86 |
--------------------------------------------------------------------------------
/tests/Unit/Entities/HostnameTest.php:
--------------------------------------------------------------------------------
1 | hostname = new Hostname('facebook.com');
18 | }
19 |
20 | /**
21 | * @test
22 | */
23 | public function hasBasicGettersAndIsStringy(): void
24 | {
25 | $this->assertSame('facebook.com.', (string)$this->hostname);
26 | $this->assertSame('facebook.com.', $this->hostname->getHostName());
27 | $this->assertSame('facebook.com', $this->hostname->getHostnameWithoutTrailingDot());
28 | }
29 |
30 | /**
31 | * @test
32 | */
33 | public function testsForEquality(): void
34 | {
35 | $facebook1 = Hostname::createFromString('facebook.com');
36 | $facebook2 = Hostname::createFromString('facebook.com');
37 | $google = Hostname::createFromString('google.com');
38 |
39 | $this->assertTrue($facebook1->equals($facebook2));
40 | $this->assertFalse($facebook2->equals($google));
41 | }
42 |
43 | /**
44 | * @test
45 | */
46 | public function doesNotAllowInvalidHostNames(): void
47 | {
48 | $this->expectException(InvalidArgumentException::class);
49 |
50 | $hostname = implode('', array_fill(0, 64, 'A'));
51 |
52 | Hostname::createFromString($hostname);
53 | }
54 |
55 | /**
56 | * @test
57 | */
58 | public function handlesIDNOperations(): void
59 | {
60 | $utf8IDN = 'ańodelgatos.com.';
61 | $IDN = Hostname::createFromString($utf8IDN);
62 |
63 | $expectedAscii = 'xn--aodelgatos-w0b.com.';
64 | $this->assertTrue($IDN->isPunycoded());
65 | $this->assertSame($expectedAscii, $IDN->getHostName());
66 | $this->assertSame($utf8IDN, $IDN->toUTF8());
67 | }
68 |
69 | /**
70 | * @test
71 | * @dataProvider validHostnamesProvider
72 | */
73 | public function createsHostnamesFromString(string $hostname): void
74 | {
75 | $this->assertInstanceOf(Hostname::class, Hostname::createFromString($hostname));
76 | }
77 |
78 | public function validHostnamesProvider(): array
79 | {
80 | return [
81 | ['google.com'],
82 | ['subdomain.google.com'],
83 | ['mandrill._domainkey.domain.com'],
84 | ['google.com.'],
85 | ];
86 | }
87 | }
88 |
--------------------------------------------------------------------------------
/src/Resolvers/Dig.php:
--------------------------------------------------------------------------------
1 | dig = $dig ?? new Dns();
45 |
46 | if ($nameserver !== null) {
47 | $this->dig = $this->dig->useNameserver((string) $nameserver);
48 | }
49 |
50 | $this->mapper = $mapper ?? new DigMapper();
51 | }
52 |
53 | protected function doQuery(Hostname $hostname, DNSRecordType $recordType): DNSRecordCollection
54 | {
55 | if (!self::isSupportedQueryType($recordType)) {
56 | return new DNSRecordCollection();
57 | }
58 |
59 | try {
60 | $response = ($recordType->equals(DNSRecordType::createANY()))
61 | ? $this->dig->getRecords((string) $hostname, self::SUPPORTED_QUERY_TYPES)
62 | : $this->dig->getRecords((string) $hostname, (string) $recordType);
63 | } catch (Throwable $e) {
64 | throw new QueryFailure($e->getMessage(), 0, $e);
65 | }
66 |
67 | return $this->mapResults($this->mapper, array_map(fn(Record $record): array => $record->toArray(), $response));
68 | }
69 |
70 | private static function isSupportedQueryType(DNSRecordType $type): bool
71 | {
72 | if ($type->isA(DNSRecordType::TYPE_ANY)) {
73 | return true;
74 | }
75 |
76 | foreach (self::SUPPORTED_QUERY_TYPES as $queryType) {
77 | if ($type->isA($queryType)) {
78 | return true;
79 | }
80 | }
81 |
82 | return false;
83 | }
84 | }
85 |
--------------------------------------------------------------------------------
/tests/Unit/Observability/Events/DNSQueryFailedTest.php:
--------------------------------------------------------------------------------
1 | resolver = $this->createMock(Resolver::class);
31 | $this->resolver->method('getName')
32 | ->willReturn('foo');
33 |
34 | $this->hostname = Hostname::createFromString('facebook.com');
35 | $this->recordType = DNSRecordType::createTXT();
36 | $this->error = new Exception();
37 |
38 | $this->DNSQueryFailed = new DNSQueryFailed(
39 | $this->resolver,
40 | $this->hostname,
41 | $this->recordType,
42 | $this->error
43 | );
44 | }
45 |
46 | /**
47 | * @test
48 | */
49 | public function hasBasicGetters(): void
50 | {
51 | $this->assertSame($this->resolver, $this->DNSQueryFailed->getResolver());
52 | $this->assertSame($this->hostname, $this->DNSQueryFailed->getHostname());
53 | $this->assertSame($this->recordType, $this->DNSQueryFailed->getRecordType());
54 | $this->assertSame($this->error, $this->DNSQueryFailed->getError());
55 | $this->assertSame(DNSQueryFailed::NAME, $this->DNSQueryFailed::getName());
56 | }
57 |
58 | /**
59 | * @test
60 | */
61 | public function isArrayable(): void
62 | {
63 | $expected = [
64 | 'resolver' => 'foo',
65 | 'hostname' => (string)$this->hostname,
66 | 'type' => (string)$this->recordType,
67 | 'error' => $this->error,
68 | ];
69 |
70 | $this->assertInstanceOf(Arrayable::class, $this->DNSQueryFailed);
71 | $this->assertInstanceOf(JsonSerializable::class, $this->error);
72 | $this->assertEquals($expected, $this->DNSQueryFailed->toArray());
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/src/Mappers/Dig.php:
--------------------------------------------------------------------------------
1 | fields['type'],
23 | $this->fields['host'],
24 | $this->fields['ttl'],
25 | $this->fields['ip'] ?? $this->fields['ipv6'] ?? null,
26 | $this->fields['class'],
27 | );
28 |
29 | return match ((string) $this->fields['type']) {
30 | 'A', 'AAAA' => $baseRecord,
31 | 'CAA' => $baseRecord
32 | ->setData(new CAAData($this->fields['flags'], $this->fields['tag'], $this->fields['value'])),
33 | 'CNAME' => $baseRecord
34 | ->setData(new CNAMEData(Hostname::createFromString($this->fields['target']))),
35 | 'MX' => $baseRecord
36 | ->setData(new MXData(Hostname::createFromString($this->fields['target']), $this->fields['pri'])),
37 | 'NS' => $baseRecord
38 | ->setData(new NSData(Hostname::createFromString($this->fields['target']))),
39 | 'SOA' => $baseRecord
40 | ->setData(new SOAData(
41 | Hostname::createFromString($this->fields['mname']),
42 | Hostname::createFromString($this->fields['rname']),
43 | $this->fields['serial'],
44 | $this->fields['refresh'],
45 | $this->fields['retry'],
46 | $this->fields['expire'],
47 | $this->fields['minimum_ttl'],
48 | )),
49 | 'SRV' => $baseRecord
50 | ->setData(new SRVData(
51 | $this->fields['pri'],
52 | $this->fields['weight'],
53 | $this->fields['port'],
54 | Hostname::createFromString($this->fields['target'])
55 | )),
56 | 'TXT' => $baseRecord
57 | ->setData(new TXTData($this->fields['txt'])),
58 | default => throw new InvalidArgumentException($this->fields['type'] . ' is not supported by dig')
59 | };
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/tests/Unit/Entities/SRVDataTest.php:
--------------------------------------------------------------------------------
1 | SRVData = new SRVData($this->priority, $this->weight, $this->port, new Hostname($this->target));
28 | }
29 |
30 | /**
31 | * @test
32 | */
33 | public function knowsIfEquals(): void
34 | {
35 | $this->assertTrue($this->SRVData->equals($this->SRVData));
36 | $this->assertFalse($this->SRVData->equals(new SRVData(1, 2, 3, new Hostname('thing.co.'))));
37 | }
38 |
39 | /**
40 | * @test
41 | */
42 | public function isArrayable(): void
43 | {
44 | $this->assertArrayableAndEquals(
45 | [
46 | 'priority' => $this->priority,
47 | 'weight' => $this->weight,
48 | 'port' => $this->port,
49 | 'target' => $this->target,
50 | ],
51 | $this->SRVData
52 | );
53 | }
54 |
55 | /**
56 | * @test
57 | */
58 | public function isJsonSerializeable(): void
59 | {
60 | $this->assertJsonSerializeableAndEquals(
61 | [
62 | 'priority' => $this->priority,
63 | 'weight' => $this->weight,
64 | 'port' => $this->port,
65 | 'target' => $this->target,
66 | ],
67 | $this->SRVData
68 | );
69 | }
70 |
71 | /**
72 | * @test
73 | */
74 | public function isSerializable(): void
75 | {
76 | $this->assertSerializable($this->SRVData);
77 | $this->assertEquals($this->SRVData, unserialize(serialize($this->SRVData)));
78 | }
79 |
80 | /**
81 | * @test
82 | */
83 | public function isStringable(): void
84 | {
85 | $this->assertStringableAndEquals('100 200 9090 target.co.', $this->SRVData);
86 | }
87 |
88 | /**
89 | * @test
90 | */
91 | public function hasBasicGetters(): void
92 | {
93 | $this->assertSame($this->priority, $this->SRVData->getPriority());
94 | $this->assertSame($this->weight, $this->SRVData->getWeight());
95 | $this->assertSame($this->port, $this->SRVData->getPort());
96 | $this->assertSame($this->target, $this->SRVData->getTarget()->__toString());
97 | }
98 | }
99 |
--------------------------------------------------------------------------------
/tests/Unit/Entities/CAADataTest.php:
--------------------------------------------------------------------------------
1 | CAAData = new CAAData($this->flags, $this->tag, $this->value);
27 | }
28 |
29 | /**
30 | * @test
31 | */
32 | public function knowsIfEquals(): void
33 | {
34 | $this->assertTrue($this->CAAData->equals($this->CAAData));
35 | $this->assertFalse($this->CAAData->equals(new CAAData(0, 'issue', 'boop.com')));
36 | }
37 |
38 | /**
39 | * @test
40 | */
41 | public function isArrayable(): void
42 | {
43 | $this->assertArrayableAndEquals(
44 | ['flags' => $this->flags, 'tag' => $this->tag, 'value' => ';'],
45 | $this->CAAData
46 | );
47 | }
48 |
49 | /**
50 | * @test
51 | */
52 | public function isJsonSerializable(): void
53 | {
54 | $this->assertJsonSerializeableAndEquals(
55 | ['flags' => $this->flags, 'tag' => $this->tag, 'value' => ';'],
56 | $this->CAAData
57 | );
58 | }
59 |
60 | /**
61 | * @test
62 | */
63 | public function isSerializable(): void
64 | {
65 | $this->assertSerializable($this->CAAData);
66 | $this->assertEquals($this->CAAData, unserialize(serialize($this->CAAData)));
67 | }
68 |
69 | /**
70 | * @test
71 | */
72 | public function isStringable(): void
73 | {
74 | $this->assertStringableAndEquals('0 issue ";"', $this->CAAData);
75 | }
76 |
77 | /**
78 | * @test
79 | */
80 | public function hasBasicGetters(): void
81 | {
82 | $this->assertSame($this->flags, $this->CAAData->getFlags());
83 | $this->assertSame($this->tag, $this->CAAData->getTag());
84 | $this->assertSame(';', $this->CAAData->getValue());
85 |
86 | $nullDefault = new CAAData(0, 'issue');
87 | $this->assertNull($nullDefault->getValue());
88 | }
89 |
90 | /**
91 | * @test
92 | */
93 | public function doesNotAllowSpaceCharactersAsValidValue(): void
94 | {
95 | $this->expectException(Exceptions\InvalidArgumentException::class);
96 | $badValue = '\'\\# 26 00 09 69 73 73 75 65 77 69 6c 64 6c 65 74 73 65 6e 63 72 79 70 74 2e 6f 72 67\'';
97 | new CAAData(0, 'issuewild', $badValue);
98 | }
99 | }
100 |
--------------------------------------------------------------------------------
/tests/Unit/Resolvers/DigTest.php:
--------------------------------------------------------------------------------
1 | dig = $this->createPartialMock(Dns::class, ['getRecords']);
31 | $this->mapper = new DigMapper();
32 | $this->nameserver = Hostname::createFromString('example.com');
33 | $this->hostname = Hostname::createFromString('facebook.com');
34 | $this->resolver = new Dig($this->dig, $this->mapper, $this->nameserver);
35 | $this->record = A::make([
36 | 'host' => 'facebook.com',
37 | 'ttl' => 123,
38 | 'class' => 'IN',
39 | 'type' => 'A',
40 | 'ip' => '192.168.1.1'
41 | ]);
42 |
43 | $this->assertSame('example.com.', $this->dig->getNameserver());
44 | }
45 |
46 | public function testQuerysSupportedRecord(): void
47 | {
48 |
49 | $this->dig->method('getRecords')
50 | ->with('facebook.com.', 'A')
51 | ->willReturn([$this->record]);
52 |
53 | $expectedRecord = new DNSRecord(
54 | DNSRecordType::createA(),
55 | $this->hostname,
56 | 123,
57 | IPAddress::createFromString('192.168.1.1')
58 | );
59 |
60 | $actual = $this->resolver->getRecords((string) $this->hostname, DNSRecordType::TYPE_A)[0];
61 | $this->assertTrue($expectedRecord->equals($actual));
62 | }
63 |
64 | public function testReturnsEmptyOnUnsupportedRecord(): void
65 | {
66 |
67 | $this->dig->expects($this->never())
68 | ->method('getRecords');
69 |
70 | $this->assertTrue($this->resolver->getRecords((string) $this->hostname, DNSRecordType::TYPE_PTR)->isEmpty());
71 | }
72 |
73 | public function testRecastsASpatieFailureAsQueryFailure(): void
74 | {
75 |
76 | $this->dig->method('getRecords')
77 | ->with('facebook.com.', 'A')
78 | ->willThrowException(new \DomainException('yo'));
79 |
80 | $this->expectException(QueryFailure::class);
81 | $this->expectExceptionMessage('yo');
82 |
83 | $this->resolver->getRecords((string) $this->hostname, DNSRecordType::TYPE_A);
84 | }
85 | }
86 |
--------------------------------------------------------------------------------
/tests/Unit/Observability/Traits/DispatcherTest.php:
--------------------------------------------------------------------------------
1 | dispatcher = $this->createMock(EventDispatcherInterface::class);
27 | $this->subscriber = $this->createMock(EventSubscriberInterface::class);
28 | $this->event = new class extends ObservableEventAbstract
29 | {
30 | public static function getName(): string
31 | {
32 | return 'the name';
33 | }
34 |
35 | public function toArray(): array
36 | {
37 | return ['beep' => 'boop'];
38 | }
39 | };
40 |
41 | $this->observableClass = new class implements Observable {
42 | use Dispatcher;
43 | };
44 | }
45 |
46 | /**
47 | * @test
48 | */
49 | public function createsAndCachesDispatcher(): void
50 | {
51 | $this->dispatcher->expects($this->never())
52 | ->method('dispatch');
53 |
54 | $this->observableClass->dispatch($this->event);
55 | }
56 |
57 | /**
58 | * @test
59 | */
60 | public function addsSubscribersAndListeners(): void
61 | {
62 | $this->observableClass->setDispatcher($this->dispatcher);
63 |
64 | $this->dispatcher->expects($this->once())
65 | ->method('addSubscriber')
66 | ->with($this->subscriber);
67 |
68 | $this->dispatcher->expects($this->once())
69 | ->method('addListener')
70 | ->with('boop', function () {
71 | });
72 |
73 | $this->observableClass->addSubscriber($this->subscriber);
74 | $this->observableClass->addListener('boop', function () {
75 | });
76 | }
77 |
78 | /**
79 | * @test
80 | */
81 | public function dispatchesObservableEvents(): void
82 | {
83 | $this->observableClass->setDispatcher($this->dispatcher);
84 |
85 | $this->dispatcher->expects($this->once())
86 | ->method('dispatch')
87 | ->with($this->event, $this->event::getName());
88 |
89 | $this->observableClass->dispatch($this->event);
90 | }
91 | }
92 |
--------------------------------------------------------------------------------
/src/Entities/DataAbstract.php:
--------------------------------------------------------------------------------
1 | toArray();
22 | }
23 |
24 | public function equals(DataAbstract $dataAbstract): bool
25 | {
26 | return (string)$this === (string)$dataAbstract;
27 | }
28 |
29 | /**
30 | * @throws \RemotelyLiving\PHPDNS\Exceptions\InvalidArgumentException
31 | */
32 | public static function createFromTypeAndString(DNSRecordType $recordType, string $data): self
33 | {
34 | if ($recordType->isA(DNSRecordType::TYPE_TXT)) {
35 | return new TXTData(trim($data, '"'));
36 | }
37 |
38 | if ($recordType->isA(DNSRecordType::TYPE_NS)) {
39 | return new NSData(new Hostname($data));
40 | }
41 |
42 | if ($recordType->isA(DNSRecordType::TYPE_CNAME)) {
43 | return new CNAMEData(new Hostname($data));
44 | }
45 |
46 | $parsed = self::parseDataToArray($data);
47 |
48 | if ($recordType->isA(DNSRecordType::TYPE_MX)) {
49 | return new MXData(new Hostname($parsed[1]), (int)$parsed[0]);
50 | }
51 |
52 | if ($recordType->isA(DNSRecordType::TYPE_SOA)) {
53 | return new SOAData(
54 | new Hostname($parsed[0]),
55 | new Hostname($parsed[1]),
56 | (int)($parsed[2] ?? 0),
57 | (int)($parsed[3] ?? 0),
58 | (int)($parsed[4] ?? 0),
59 | (int)($parsed[5] ?? 0),
60 | (int)($parsed[6] ?? 0)
61 | );
62 | }
63 |
64 | if ($recordType->isA(DNSRecordType::TYPE_CAA) && count($parsed) === 3) {
65 | return new CAAData((int)$parsed[0], (string)$parsed[1], $parsed[2]);
66 | }
67 |
68 | if ($recordType->isA(DNSRecordType::TYPE_SRV)) {
69 | return new SRVData(
70 | (int)$parsed[0] ?: 0,
71 | (int) $parsed[1] ?: 0,
72 | (int) $parsed[2] ?: 0,
73 | new Hostname($parsed[3])
74 | );
75 | }
76 |
77 | if ($recordType->isA(DNSRecordType::TYPE_PTR)) {
78 | return new PTRData(new Hostname($data));
79 | }
80 |
81 | throw new InvalidArgumentException("{$data} could not be created with type {$recordType}");
82 | }
83 |
84 | public function jsonSerialize(): array
85 | {
86 | return $this->toArray();
87 | }
88 |
89 | protected function init(): void
90 | {
91 | }
92 | private static function parseDataToArray(string $data): array
93 | {
94 | return explode(' ', $data);
95 | }
96 | }
97 |
--------------------------------------------------------------------------------
/tests/Unit/Observability/Events/DNSQueriedTest.php:
--------------------------------------------------------------------------------
1 | resolver = $this->createMock(Resolver::class);
32 | $this->resolver->method('getName')
33 | ->willReturn('foo');
34 |
35 | $this->hostname = Hostname::createFromString('facebook.com');
36 | $this->recordType = DNSRecordType::createTXT();
37 | $this->recordCollection = new DNSRecordCollection();
38 |
39 | $this->DNSQueried = new DNSQueried(
40 | $this->resolver,
41 | $this->hostname,
42 | $this->recordType,
43 | $this->recordCollection
44 | );
45 | }
46 |
47 | /**
48 | * @test
49 | */
50 | public function hasBasicGetters(): void
51 | {
52 | $this->assertSame($this->resolver, $this->DNSQueried->getResolver());
53 | $this->assertSame($this->hostname, $this->DNSQueried->getHostname());
54 | $this->assertSame($this->recordType, $this->DNSQueried->getRecordType());
55 | $this->assertSame($this->recordCollection, $this->DNSQueried->getRecordCollection());
56 | $this->assertSame(DNSQueried::NAME, $this->DNSQueried::getName());
57 | }
58 |
59 | /**
60 | * @test
61 | */
62 | public function isArrayable(): void
63 | {
64 | $record1 = $this->createMock(DNSRecordInterface::class);
65 | $record1->method('toArray')
66 | ->willReturn(['herp' => 'derp']);
67 |
68 | $record2 = $this->createMock(DNSRecordInterface::class);
69 | $record2->method('toArray')
70 | ->willReturn(['beep' => 'boop']);
71 |
72 | $expected = [
73 | 'resolver' => 'foo',
74 | 'hostname' => (string)$this->hostname,
75 | 'type' => (string)$this->recordType,
76 | 'records' => [
77 | ['herp' => 'derp'],
78 | ['beep' => 'boop'],
79 | ],
80 | 'empty' => false,
81 | ];
82 |
83 | $this->recordCollection[] = $record1;
84 | $this->recordCollection[] = $record2;
85 |
86 | $this->assertInstanceOf(Arrayable::class, $this->DNSQueried);
87 | $this->assertEquals($expected, $this->DNSQueried->toArray());
88 | }
89 | }
90 |
--------------------------------------------------------------------------------
/src/Resolvers/GoogleDNS.php:
--------------------------------------------------------------------------------
1 | self::BASE_URI,
26 | 'strict' => true,
27 | 'allow_redirects' => false,
28 | 'connect_timeout' => self::DEFAULT_TIMEOUT,
29 | 'protocols' => ['https'],
30 | 'headers' => [
31 | 'Accept' => 'application/json',
32 | ],
33 | ];
34 |
35 | private ClientInterface $http;
36 |
37 | private GoogleDNSMapper $mapper;
38 |
39 | /**
40 | * @param array $options
41 | */
42 | public function __construct(
43 | ClientInterface $http = null,
44 | GoogleDNSMapper $mapper = null,
45 | private int $consensusAttempts = 3,
46 | private array $options = self::DEFAULT_OPTIONS
47 | ) {
48 | $this->http = $http ?? new Client();
49 | $this->mapper = $mapper ?? new GoogleDNSMapper();
50 | }
51 |
52 | /**
53 | * Google DNS has consistency issues so this tries a few times to get an answer
54 | */
55 | public function hasRecord(DNSRecordInterface $record): bool
56 | {
57 | $attempts = 0;
58 |
59 | do {
60 | $hasRecord = $this->getRecords((string)$record->getHostname(), (string)$record->getType())
61 | ->has($record);
62 |
63 | ++$attempts;
64 | } while (!$hasRecord && $attempts < $this->consensusAttempts);
65 |
66 | return $hasRecord;
67 | }
68 |
69 | protected function doQuery(Hostname $hostname, DNSRecordType $recordType): DNSRecordCollection
70 | {
71 | $results = $this->doApiQuery(['name' => (string)$hostname, 'type' => (string)$recordType]);
72 |
73 | return $this->mapResults($this->mapper, $results);
74 | }
75 |
76 | /**
77 | * @throws \RemotelyLiving\PHPDNS\Resolvers\Exceptions\QueryFailure
78 | */
79 | private function doApiQuery(array $query = []): array
80 | {
81 | try {
82 | $response = $this->http->request('GET', '/resolve?' . http_build_query($query), $this->options);
83 | } catch (Throwable $e) {
84 | throw new QueryFailure("Unable to query GoogleDNS API", 0, $e);
85 | }
86 |
87 | $result = (array) json_decode((string)$response->getBody(), true);
88 |
89 | if (isset($result['Answer'])) {
90 | return $result['Answer'];
91 | }
92 |
93 | if (isset($result['Authority'])) {
94 | return $result['Authority'];
95 | }
96 |
97 | return [];
98 | }
99 | }
100 |
--------------------------------------------------------------------------------
/tests/Unit/Observability/Subscribers/STDIOSubscriberTest.php:
--------------------------------------------------------------------------------
1 | STDOut = $this->getMockBuilder(SplFileObject::class)
30 | ->setConstructorArgs(['php://memory'])
31 | ->setMethods(['fwrite'])
32 | ->getMock();
33 |
34 | $this->STDErr = $this->getMockBuilder(SplFileObject::class)
35 | ->setConstructorArgs(['php://memory'])
36 | ->setMethods(['fwrite'])
37 | ->getMock();
38 |
39 | $this->event = new class extends ObservableEventAbstract
40 | {
41 | public static function getName(): string
42 | {
43 | return 'the name';
44 | }
45 |
46 | public function toArray(): array
47 | {
48 | return ['beep' => 'boop'];
49 | }
50 | };
51 |
52 | $this->expectedOut = <<subscriber = new STDIOSubscriber($this->STDOut, $this->STDErr);
61 | }
62 |
63 | /**
64 | * @test
65 | */
66 | public function getsSubscribedEvents(): void
67 | {
68 | $this->assertEquals([
69 | DNSQueryFailed::getName() => 'onDNSQueryFailed',
70 | DNSQueried::getName() => 'onDNSQueried',
71 | DNSQueryProfiled::getName() => 'onDNSQueryProfiled',
72 | ], $this->subscriber::getSubscribedEvents());
73 | }
74 |
75 | /**
76 | * @test
77 | */
78 | public function writesToSTDErrOnQueryFailure(): void
79 | {
80 |
81 |
82 | $this->STDErr->expects($this->once())
83 | ->method('fwrite')
84 | ->with($this->expectedOut);
85 |
86 | $this->subscriber->onDNSQueryFailed($this->event);
87 | }
88 |
89 | /**
90 | * @test
91 | */
92 | public function writesToSTDOutOnQuery(): void
93 | {
94 | $this->STDOut->expects($this->once())
95 | ->method('fwrite')
96 | ->with($this->expectedOut);
97 |
98 | $this->subscriber->onDNSQueried($this->event);
99 | }
100 |
101 | /**
102 | * @test
103 | */
104 | public function writesToSTDOutOnQueryProfiled(): void
105 | {
106 | $this->STDOut->expects($this->once())
107 | ->method('fwrite')
108 | ->with($this->expectedOut);
109 |
110 | $this->subscriber->onDNSQueryProfiled($this->event);
111 | }
112 | }
113 |
--------------------------------------------------------------------------------
/tests/Unit/Mappers/DigTest.php:
--------------------------------------------------------------------------------
1 | mapper = new Dig();
18 | }
19 |
20 | /**
21 | * @test
22 | */
23 | public function willNotMapUnsupportedType(): void
24 | {
25 | $this->expectException(InvalidArgumentException::class);
26 | $this->mapper->mapFields(['type' => 'PTR', 'host' => 'google.com', 'ttl' => 1, 'class' => 'IN'])->toDNSRecord();
27 | }
28 |
29 | /**
30 | * @dataProvider recordProvider
31 | * @test
32 | */
33 | public function mapsASpatieDigRecordToDNSRecord(array $fields): void
34 | {
35 | $type = DNSRecordType::createFromString($fields['type']);
36 |
37 | $this->assertTrue($type->equals($this->mapper->mapFields($fields)->toDNSRecord()->getType()));
38 | }
39 |
40 | public function recordProvider(): array
41 | {
42 | return [
43 | 'A' => [['type' => 'A', 'host' => 'google.com', 'ttl' => 123, 'ip' => '192.168.1.1', 'class' => 'IN']],
44 | 'AAAA' => [[
45 | 'type' => 'AAAA',
46 | 'host' => 'google.com',
47 | 'ttl' => 123,
48 | 'ip' => '2001:db8:3333:4444:5555:6666:7777:8888',
49 | 'class' => 'IN'
50 | ]],
51 | 'CAA' => [[
52 | 'type' => 'CAA',
53 | 'host' => 'google.com',
54 | 'ttl' => 123,
55 | 'flags' => 3,
56 | 'class' => 'IN',
57 | 'tag' => 'yo',
58 | 'value' => 'gabbagabba',
59 | ]],
60 | 'CNAME' => [[
61 | 'type' => 'CNAME',
62 | 'host' => 'google.com',
63 | 'class' => 'IN',
64 | 'ttl' => 123,
65 | 'target' => 'www.google.com',
66 | ]],
67 | 'MX' => [[
68 | 'type' => 'MX',
69 | 'host' => 'google.com',
70 | 'ttl' => 123,
71 | 'class' => 'IN',
72 | 'target' => 'www.google.com',
73 | 'pri' => 100,
74 | ]],
75 | 'NS' => [[
76 | 'type' => 'NS',
77 | 'host' => 'google.com',
78 | 'ttl' => 123,
79 | 'class' => 'IN',
80 | 'target' => 'www.namecheap.com',
81 | ]],
82 | 'SOA' => [[
83 | 'type' => 'SOA',
84 | 'host' => 'google.com',
85 | 'ttl' => 123,
86 | 'class' => 'IN',
87 | 'mname' => 'www.google.com',
88 | 'rname' => 'google.com',
89 | 'serial' => 234234,
90 | 'refresh' => 1,
91 | 'retry' => 2,
92 | 'expire' => 3,
93 | 'minimum_ttl' => 4,
94 | ]],
95 | 'SRV' => [[
96 | 'type' => 'SRV',
97 | 'host' => 'google.com',
98 | 'ttl' => 123,
99 | 'class' => 'IN',
100 | 'target' => 'www.google.com',
101 | 'pri' => 100,
102 | 'weight' => 4,
103 | 'port' => 8080,
104 | ]],
105 | 'TXT' => [[
106 | 'type' => 'TXT',
107 | 'host' => 'google.com',
108 | 'ttl' => 123,
109 | 'class' => 'IN',
110 | 'txt' => 'to be or not to be',
111 | ]],
112 | ];
113 | }
114 | }
115 |
--------------------------------------------------------------------------------
/tests/Unit/Entities/SOADataTest.php:
--------------------------------------------------------------------------------
1 | mname = new Hostname('google.com');
35 | $this->rname = new Hostname('facebook.com');
36 | $this->SOAData = new SOAData(
37 | $this->mname,
38 | $this->rname,
39 | $this->serial,
40 | $this->refresh,
41 | $this->retry,
42 | $this->expire,
43 | $this->minTTL
44 | );
45 | }
46 |
47 | /**
48 | * @test
49 | */
50 | public function knowsIfEquals(): void
51 | {
52 | $anotherSOA = new SOAData($this->rname, $this->mname, 1, 1, 1, 1, 1);
53 | $this->assertTrue($this->SOAData->equals($this->SOAData));
54 | $this->assertFalse($this->SOAData->equals($anotherSOA));
55 | }
56 |
57 | /**
58 | * @test
59 | */
60 | public function isArrayable(): void
61 | {
62 | $this->assertArrayableAndEquals(
63 | [
64 | 'rname' => (string)$this->rname,
65 | 'mname' => (string)$this->mname,
66 | 'serial' => $this->serial,
67 | 'refresh' => $this->refresh,
68 | 'retry' => $this->retry,
69 | 'expire' => $this->expire,
70 | 'minimumTTL' => $this->minTTL,
71 | ],
72 | $this->SOAData
73 | );
74 | }
75 |
76 | /**
77 | * @test
78 | */
79 | public function isJsonSerializeable(): void
80 | {
81 | $this->assertJsonSerializeableAndEquals(
82 | [
83 | 'rname' => (string)$this->rname,
84 | 'mname' => (string)$this->mname,
85 | 'serial' => $this->serial,
86 | 'refresh' => $this->refresh,
87 | 'retry' => $this->retry,
88 | 'expire' => $this->expire,
89 | 'minimumTTL' => $this->minTTL,
90 | ],
91 | $this->SOAData
92 | );
93 | }
94 |
95 | /**
96 | * @test
97 | */
98 | public function isSerializable(): void
99 | {
100 | $this->assertSerializable($this->SOAData);
101 | $this->assertEquals($this->SOAData, unserialize(serialize($this->SOAData)));
102 | }
103 |
104 | /**
105 | * @test
106 | */
107 | public function isStringable(): void
108 | {
109 | $this->assertStringableAndEquals('google.com. facebook.com. 2342 123 321 3434 60', $this->SOAData);
110 | }
111 |
112 | /**
113 | * @test
114 | */
115 | public function hasBasicGetters(): void
116 | {
117 | $this->assertSame($this->mname, $this->SOAData->getMname());
118 | $this->assertSame($this->rname, $this->SOAData->getRname());
119 | $this->assertSame($this->serial, $this->SOAData->getSerial());
120 | $this->assertSame($this->refresh, $this->SOAData->getRefresh());
121 | $this->assertSame($this->retry, $this->SOAData->getRetry());
122 | $this->assertSame($this->expire, $this->SOAData->getExpire());
123 | $this->assertSame($this->minTTL, $this->SOAData->getMinTTL());
124 | }
125 | }
126 |
--------------------------------------------------------------------------------
/src/Resolvers/Cached.php:
--------------------------------------------------------------------------------
1 | cache->clear();
42 | }
43 |
44 | public function withEmptyResultCachingDisabled(): self
45 | {
46 | $emptyCachingDisabled = new self($this->cache, $this->resolver, $this->ttlSeconds);
47 | $emptyCachingDisabled->shouldCacheEmptyResults = false;
48 |
49 | return $emptyCachingDisabled;
50 | }
51 |
52 | protected function doQuery(Hostname $hostname, DNSRecordType $recordType): DNSRecordCollection
53 | {
54 | $cachedResult = $this->cache->getItem($this->buildCacheKey($hostname, $recordType));
55 |
56 | if ($cachedResult->isHit()) {
57 | return $this->unwrapResults($cachedResult->get());
58 | }
59 |
60 | $dnsRecords = $this->resolver->getRecords((string)$hostname, (string)$recordType);
61 | if ($dnsRecords->isEmpty() && !$this->shouldCacheEmptyResults) {
62 | return $dnsRecords;
63 | }
64 |
65 | $ttlSeconds = $this->ttlSeconds ?? $this->extractLowestTTL($dnsRecords);
66 | $cachedResult->expiresAfter($ttlSeconds);
67 | $cachedResult->set(['recordCollection' => $dnsRecords, 'timestamp' => $this->getTimeStamp()]);
68 | $this->cache->save($cachedResult);
69 |
70 | return $dnsRecords;
71 | }
72 |
73 | private function buildCacheKey(Hostname $hostname, DNSRecordType $recordType): string
74 | {
75 | return md5(sprintf(self::CACHE_KEY_TEMPLATE, self::NAMESPACE, (string)$hostname, (string)$recordType));
76 | }
77 |
78 | private function extractLowestTTL(DNSRecordCollection $recordCollection): int
79 | {
80 | $ttls = [];
81 |
82 | /** @var \RemotelyLiving\PHPDNS\Entities\DNSRecord $record */
83 | foreach ($recordCollection as $record) {
84 | /** @scrutinizer ignore-call */
85 | if ($record->getTTL() <= 0) {
86 | continue;
87 | }
88 |
89 | $ttls[] = $record->getTTL();
90 | }
91 |
92 | return count($ttls) ? min($ttls) : self::DEFAULT_CACHE_TTL;
93 | }
94 |
95 | /**
96 | * @param array $results ['recordCollection' => $recordCollection, 'timestamp' => $timeStamp]
97 | */
98 | private function unwrapResults(array $results): DNSRecordCollection
99 | {
100 | /** @var DNSRecordCollection $records */
101 | $records = $results['recordCollection'];
102 | /**
103 | * @var int $key
104 | * @var \RemotelyLiving\PHPDNS\Entities\DNSRecord $record
105 | */
106 | foreach ($records as $key => $record) {
107 | $records[$key] = $record
108 | ->setTTL(max($record->getTTL() - ($this->getTimeStamp() - (int)$results['timestamp']), 0));
109 | }
110 |
111 | return $records;
112 | }
113 | }
114 |
--------------------------------------------------------------------------------
/src/Mappers/LocalSystem.php:
--------------------------------------------------------------------------------
1 | DNSRecordType::TYPE_A,
35 | DNS_CNAME => DNSRecordType::TYPE_CNAME,
36 | DNS_HINFO => DNSRecordType::TYPE_HINFO,
37 | DNS_CAA => DNSRecordType::TYPE_CAA,
38 | DNS_MX => DNSRecordType::TYPE_MX,
39 | DNS_NS => DNSRecordType::TYPE_NS,
40 | DNS_PTR => DNSRecordType::TYPE_PTR,
41 | DNS_SOA => DNSRecordType::TYPE_SOA,
42 | DNS_TXT => DNSRecordType::TYPE_TXT,
43 | DNS_AAAA => DNSRecordType::TYPE_AAAA,
44 | DNS_SRV => DNSRecordType::TYPE_SRV,
45 | DNS_NAPTR => DNSRecordType::TYPE_NAPTR,
46 | DNS_A6 => DNSRecordType::TYPE_A6,
47 | DNS_ANY => DNSRecordType::TYPE_ANY,
48 | ];
49 | /**
50 | * @var string
51 | */
52 | private const TARGET = 'target';
53 | /**
54 | * @var string
55 | */
56 | private const PRI = 'pri';
57 | /**
58 | * @var string
59 | */
60 | private const TEMPLATE = '%s %s %s %s %s %s %s';
61 |
62 | public function toDNSRecord(): DNSRecordInterface
63 | {
64 | $IPAddress = null;
65 |
66 | if (isset($this->fields['ipv6'])) {
67 | $IPAddress = $this->fields['ipv6'];
68 | }
69 |
70 | if (isset($this->fields['ip'])) {
71 | $IPAddress = $this->fields['ip'];
72 | }
73 |
74 | return DNSRecord::createFromPrimitives(
75 | $this->fields['type'],
76 | $this->fields['host'],
77 | $this->fields['ttl'],
78 | $IPAddress,
79 | $this->fields['class'],
80 | $this->formatData($this->fields)
81 | );
82 | }
83 |
84 | public function getTypeCodeFromType(DNSRecordType $type): int
85 | {
86 | return array_flip(self::PHP_CODE_TYPE_MAP)[(string)$type] ?? DNS_ANY;
87 | }
88 |
89 | private function formatData(array $fields): ?string
90 | {
91 | if (isset($this->fields['flags'], $fields['tag'], $fields['value'])) {
92 | return "{$fields['flags']} {$fields['tag']} \"{$fields['value']}\"";
93 | }
94 |
95 | if (isset($fields['mname'])) {
96 | return sprintf(
97 | self::TEMPLATE,
98 | $fields['mname'],
99 | $fields['rname'],
100 | $fields['serial'],
101 | $fields['refresh'],
102 | $fields['retry'],
103 | $fields['expire'],
104 | $fields['minimum-ttl']
105 | );
106 | }
107 |
108 | if (isset($fields[self::TARGET], $fields[self::PRI], $fields['weight'], $fields['port'])) {
109 | return "{$fields[self::PRI]} {$fields['weight']} {$fields['port']} {$fields[self::TARGET]}";
110 | }
111 |
112 |
113 | if (isset($fields[self::TARGET], $fields[self::PRI])) {
114 | return "{$fields[self::PRI]} {$fields[self::TARGET]}";
115 | }
116 |
117 | if (isset($fields[self::TARGET])) {
118 | return $fields[self::TARGET];
119 | }
120 |
121 | if (isset($fields['txt'])) {
122 | return $fields['txt'];
123 | }
124 |
125 | return null;
126 | }
127 | }
128 |
--------------------------------------------------------------------------------
/src/Entities/DNSRecord.php:
--------------------------------------------------------------------------------
1 | recordType;
54 | }
55 |
56 | public function getHostname(): Hostname
57 | {
58 | return $this->hostname;
59 | }
60 |
61 | public function getTTL(): int
62 | {
63 | return $this->TTL;
64 | }
65 |
66 | public function getIPAddress(): ?IPAddress
67 | {
68 | return $this->IPAddress;
69 | }
70 |
71 | public function getClass(): string
72 | {
73 | return $this->class;
74 | }
75 |
76 | public function getData(): ?DataAbstract
77 | {
78 | return $this->data;
79 | }
80 |
81 | public function setData(DataAbstract $data): self
82 | {
83 |
84 | $this->data = $data;
85 | return $this;
86 | }
87 |
88 | public function setTTL(int $ttl): DNSRecordInterface
89 | {
90 | $this->TTL = $ttl;
91 | return $this;
92 | }
93 |
94 | public function toArray(): array
95 | {
96 | $formatted = [
97 | 'hostname' => (string)$this->hostname,
98 | 'type' => (string)$this->recordType,
99 | 'TTL' => $this->TTL,
100 | 'class' => $this->class,
101 | ];
102 |
103 | if ($this->IPAddress !== null) {
104 | $formatted['IPAddress'] = (string)$this->IPAddress;
105 | }
106 |
107 | if ($this->data !== null) {
108 | $formatted[self::DATA] = (string)$this->data;
109 | }
110 |
111 | return $formatted;
112 | }
113 |
114 | public function equals(DNSRecordInterface $record): bool
115 | {
116 | return $this->hostname->equals($record->getHostname())
117 | && $this->recordType->equals($record->getType())
118 | && (string)$this->data === (string)$record->getData() // could be null
119 | && (string)$this->IPAddress === (string)$record->getIPAddress(); // could be null
120 | }
121 |
122 | public function __serialize(): array
123 | {
124 | return $this->toArray();
125 | }
126 |
127 | public function __unserialize(array $unserialized): void
128 | {
129 | $rawIPAddres = $unserialized['IPAddress'] ?? null;
130 | $this->recordType = DNSRecordType::createFromString($unserialized['type']);
131 | $this->hostname = Hostname::createFromString($unserialized['hostname']);
132 | $this->TTL = (int) $unserialized['TTL'];
133 | $this->IPAddress = $rawIPAddres ? IPAddress::createFromString($rawIPAddres) : null;
134 | $this->class = $unserialized['class'];
135 | $this->data = (isset($unserialized[self::DATA]))
136 | ? DataAbstract::createFromTypeAndString($this->recordType, $unserialized[self::DATA])
137 | : null;
138 | }
139 |
140 | public function jsonSerialize(): array
141 | {
142 | return $this->toArray();
143 | }
144 | }
145 |
--------------------------------------------------------------------------------
/src/Resolvers/CloudFlare.php:
--------------------------------------------------------------------------------
1 | self::BASE_URI,
27 | 'connect_timeout' => self::DEFAULT_TIMEOUT,
28 | 'strict' => true,
29 | 'allow_redirects' => false,
30 | 'protocols' => ['https'],
31 | 'headers' => [
32 | 'Accept' => 'application/dns-json',
33 | ],
34 | ];
35 |
36 | private ClientInterface $http;
37 |
38 | private CloudFlareMapper $mapper;
39 |
40 | /**
41 | * @param array $options
42 | */
43 | public function __construct(
44 | ClientInterface $http = null,
45 | CloudFlareMapper $mapper = null,
46 | private array $options = self::DEFAULT_OPTIONS
47 | ) {
48 | $this->http = $http ?? new Client();
49 | $this->mapper = $mapper ?? new CloudFlareMapper();
50 | }
51 |
52 | protected function doQuery(Hostname $hostname, DNSRecordType $recordType): DNSRecordCollection
53 | {
54 | try {
55 | return ($recordType->isA(DNSRecordType::TYPE_ANY))
56 | ? $this->doAnyApiQuery($hostname)
57 | : $this->doApiQuery($hostname, $recordType);
58 | } catch (Throwable $e) {
59 | throw new QueryFailure("Unable to query CloudFlare API", 0, $e);
60 | }
61 | }
62 |
63 | /**
64 | * Cloudflare does not support ANY queries, so we must ask for all record types individually
65 | */
66 | private function doAnyApiQuery(Hostname $hostname): DNSRecordCollection
67 | {
68 | $results = [];
69 | $eachPromise = new EachPromise($this->generateEachTypeQuery($hostname), [
70 | 'concurrency' => 4,
71 | 'fulfilled' => function (ResponseInterface $response) use (&$results) {
72 | $results = array_merge(
73 | $results,
74 | $this->parseResult((array) json_decode((string)$response->getBody(), true))
75 | );
76 | },
77 | 'rejected' => function (Throwable $e): void {
78 | throw $e;
79 | },
80 | ]);
81 |
82 | $eachPromise->promise()->wait(true);
83 |
84 | return $this->mapResults($this->mapper, $results);
85 | }
86 |
87 | private function generateEachTypeQuery(Hostname $hostname): Generator
88 | {
89 | foreach (DNSRecordType::VALID_TYPES as $type) {
90 | if ($type === DNSRecordType::TYPE_ANY) {
91 | continue 1;
92 | }
93 |
94 | yield $this->http->requestAsync(
95 | 'GET',
96 | '/dns-query?' . http_build_query(['name' => (string)$hostname, 'type' => $type]),
97 | $this->options
98 | );
99 | }
100 | }
101 |
102 | private function doApiQuery(Hostname $hostname, DNSRecordType $type): DNSRecordCollection
103 | {
104 | $url = '/dns-query?' . http_build_query(['name' => (string)$hostname, 'type' => (string)$type]);
105 | $decoded = (array)json_decode(
106 | (string)$this->http->requestAsync('GET', $url, $this->options)->wait(true)->getBody(),
107 | true,
108 | 512,
109 | JSON_THROW_ON_ERROR
110 | );
111 |
112 | return $this->mapResults($this->mapper, $this->parseResult($decoded));
113 | }
114 |
115 | private function parseResult(array $result): array
116 | {
117 | if (isset($result['Answer'])) {
118 | return $result['Answer'];
119 | }
120 |
121 | if (isset($result['Authority'])) {
122 | return $result['Authority'];
123 | }
124 |
125 | return [];
126 | }
127 | }
128 |
--------------------------------------------------------------------------------
/tests/Unit/Mappers/LocalSystemTest.php:
--------------------------------------------------------------------------------
1 | 'AAAA',
17 | 'ttl' => 343,
18 | 'ipv6' => 'FE80:CD00:0000:0000:0000:0000:211E:729C',
19 | 'host' => 'yelp.com',
20 | 'class' => 'IN',
21 | ],
22 | [
23 | 'type' => 'A',
24 | 'ttl' => 343,
25 | 'ip' => '127.0.0.1',
26 | 'host' => 'google.com',
27 | 'class' => 'IN',
28 | ],
29 | [
30 | 'type' => 'CNAME',
31 | 'ttl' => 343,
32 | 'host' => 'google.com',
33 | 'class' => 'IN'
34 | ],
35 | [
36 | 'type' => 'NS',
37 | 'ttl' => 343,
38 | 'host' => 'google.com',
39 | 'target' => 'ns.google.com.',
40 | 'class' => 'IN'
41 | ],
42 | [
43 | 'type' => 'SOA',
44 | 'ttl' => 343,
45 | 'host' => 'google.com',
46 | 'mname' => 'ns.google.com.',
47 | 'rname' => 'dns.google.com.',
48 | 'serial' => 1234,
49 | 'refresh' => 60,
50 | 'retry' => 180,
51 | 'expire' => 320,
52 | 'minimum-ttl' => 84,
53 | 'class' => 'IN'
54 | ],
55 | [
56 | 'type' => 'MX',
57 | 'ttl' => 343,
58 | 'host' => 'google.com',
59 | 'target' => 'ns.google.com.',
60 | 'pri' => 60,
61 | 'class' => 'IN',
62 | ],
63 | [
64 | 'type' => 'TXT',
65 | 'ttl' => 343,
66 | 'host' => 'google.com',
67 | 'txt' => 'txtval',
68 | 'class' => 'IN',
69 | ],
70 | [
71 | 'type' => 'CNAME',
72 | 'class' => 'IN',
73 | 'host' => 'www.google.com',
74 | 'ttl' => 234,
75 | 'target' => 'google.com',
76 | ],
77 | [
78 | 'type' => 'CAA',
79 | 'host' => 'thing.com',
80 | 'class' => 'IN',
81 | 'value' => 'google.com',
82 | 'ttl' => 234,
83 | 'tag' => 'issue',
84 | 'flags' => 0
85 | ],
86 | [
87 | 'type' => 'SRV',
88 | 'host' => '_x-puppet._tcp.dnscheck.co.',
89 | 'target' => 'master-a.dnscheck.co.',
90 | 'pri' => 100,
91 | 'ttl' => 833,
92 | 'class' => 'IN',
93 | 'weight' => 200,
94 | 'port' => 9999,
95 | ],
96 | [
97 | 'host' => '8.8.8.8.in-addr.arpa',
98 | 'class' => 'IN',
99 | 'ttl' => 15248,
100 | 'type' => 'PTR',
101 | 'target' => 'dns.google'
102 |
103 | ]
104 | ];
105 |
106 | protected function setUp(): void
107 | {
108 | parent::setUp();
109 |
110 | $this->mapper = new LocalSystem();
111 | }
112 |
113 | /**
114 | * @test
115 | */
116 | public function mapsAGoogleDNSRecordVariants(): void
117 | {
118 | $mappedRecord = $this->mapper->mapFields([
119 | 'type' => 'A',
120 | 'ttl' => 365,
121 | 'ip' => '127.0.0.1',
122 | 'host' => 'facebook.com',
123 | 'class' => 'IN'
124 | ])->toDNSRecord();
125 |
126 | $this->assertEquals(
127 | DNSRecord::createFromPrimitives('A', 'facebook.com', 365, '127.0.0.1'),
128 | $mappedRecord
129 | );
130 | }
131 |
132 | /**
133 | * @test
134 | */
135 | public function mapsAllKindsOfLocalSystemDNSRecordVariants(): void
136 | {
137 | foreach (self::LOCAL_DNS_FORMAT as $record) {
138 | $mappedRecord = $this->mapper->mapFields($record)->toDNSRecord();
139 | $this->assertInstanceOf(DNSRecord::class, $mappedRecord);
140 | }
141 | }
142 |
143 | /**
144 | * @test
145 | */
146 | public function mapsRecordTypesToCorrespondingPHPConsts(): void
147 | {
148 | $this->assertEquals(32768, $this->mapper->getTypeCodeFromType(DNSRecordType::createTXT()));
149 | }
150 | }
151 |
--------------------------------------------------------------------------------
/tests/Unit/Entities/DNSRecordTest.php:
--------------------------------------------------------------------------------
1 | DNSARecord = DNSRecord::createFromPrimitives(
23 | 'A',
24 | 'google.com',
25 | 123,
26 | '127.0.0.1',
27 | 'AS'
28 | );
29 |
30 | $this->DNSTXTRecord = DNSRecord::createFromPrimitives(
31 | 'TXT',
32 | 'google.com',
33 | 123,
34 | null,
35 | 'AS',
36 | 'txtval'
37 | );
38 | }
39 |
40 | /**
41 | * @test
42 | */
43 | public function hasBasicGetters(): void
44 | {
45 | $this->assertSame(123, $this->DNSARecord->getTTL());
46 | $this->assertTrue(IPAddress::createFromString('127.0.0.1')->equals($this->DNSARecord->getIPAddress()));
47 | $this->assertTrue(Hostname::createFromString('google.com')->equals($this->DNSARecord->getHostname()));
48 | $this->assertTrue(DNSRecordType::createA()->equals($this->DNSARecord->getType()));
49 | $this->assertSame('AS', $this->DNSARecord->getClass());
50 | $this->assertNull($this->DNSARecord->getData());
51 | }
52 |
53 | public function hasBasicSetters(): void
54 | {
55 | $this->assertSame(123, $this->DNSARecord->getTTL());
56 | $this->assertSame(321, $this->DNSARecord->setTTL(321)->getTTL());
57 | }
58 |
59 | /**
60 | * @test
61 | */
62 | public function isArrayable(): void
63 | {
64 | $this->assertArrayableAndEquals([
65 | 'hostname' => 'google.com.',
66 | 'type' => 'A',
67 | 'TTL' => 123,
68 | 'class' => 'AS',
69 | 'IPAddress' => '127.0.0.1',
70 | ], $this->DNSARecord);
71 |
72 | $this->assertArrayableAndEquals([
73 | 'hostname' => 'google.com.',
74 | 'type' => 'TXT',
75 | 'TTL' => 123,
76 | 'class' => 'AS',
77 | 'data' => 'txtval'
78 | ], $this->DNSTXTRecord);
79 | }
80 |
81 | /**
82 | * @test
83 | */
84 | public function isJsonSerializable(): void
85 | {
86 | $this->assertJsonSerializeableAndEquals([
87 | 'hostname' => 'google.com.',
88 | 'type' => 'A',
89 | 'TTL' => 123,
90 | 'class' => 'AS',
91 | 'IPAddress' => '127.0.0.1',
92 | ], $this->DNSARecord);
93 |
94 | $this->assertJsonSerializeableAndEquals([
95 | 'hostname' => 'google.com.',
96 | 'type' => 'TXT',
97 | 'TTL' => 123,
98 | 'class' => 'AS',
99 | 'data' => 'txtval'
100 | ], $this->DNSTXTRecord);
101 | }
102 |
103 | /**
104 | * @test
105 | */
106 | public function isSerializable(): void
107 | {
108 | $this->assertSerializable($this->DNSARecord);
109 |
110 | $this->assertEquals(unserialize(serialize($this->DNSARecord)), $this->DNSARecord);
111 | $this->assertEquals(unserialize(serialize($this->DNSTXTRecord)), $this->DNSTXTRecord);
112 | }
113 |
114 | /**
115 | * @test
116 | */
117 | public function testsEquality(): void
118 | {
119 | $record2 = DNSRecord::createFromPrimitives(
120 | 'A',
121 | 'google.com',
122 | 123,
123 | '192.168.1.1',
124 | 'AS'
125 | );
126 |
127 | $record3 = DNSRecord::createFromPrimitives(
128 | 'A',
129 | 'google.com',
130 | 321,
131 | '127.0.0.1',
132 | 'AS'
133 | );
134 |
135 | $record3 = DNSRecord::createFromPrimitives(
136 | 'A',
137 | 'google.com',
138 | 321,
139 | '127.0.0.1',
140 | 'AS'
141 | );
142 |
143 | $record4 = DNSRecord::createFromPrimitives(
144 | 'TXT',
145 | 'google.com',
146 | 321,
147 | null,
148 | 'AS',
149 | 'txtval'
150 | );
151 |
152 | $this->assertTrue($this->DNSARecord->equals($record3));
153 | $this->assertFalse($this->DNSARecord->equals($record2));
154 |
155 | $this->assertTrue($this->DNSTXTRecord->equals($record4));
156 | $this->assertFalse($this->DNSTXTRecord->equals($record3));
157 | }
158 | }
159 |
--------------------------------------------------------------------------------
/src/Entities/DNSRecordCollection.php:
--------------------------------------------------------------------------------
1 | records = new ArrayIterator($records);
29 | }
30 |
31 | public function toArray(): array
32 | {
33 | return $this->records->getArrayCopy();
34 | }
35 |
36 | public function pickFirst(): ?DNSRecordInterface
37 | {
38 | $copy = $this->records->getArrayCopy();
39 |
40 | return array_shift($copy);
41 | }
42 |
43 | public function filteredByType(DNSRecordType $type): self
44 | {
45 | $fn = fn(DNSRecordInterface $record) => $record->getType()->equals($type);
46 | return new self(...array_filter($this->records->getArrayCopy(), $fn));
47 | }
48 |
49 | public function has(DNSRecordInterface $lookupRecord): bool
50 | {
51 | foreach ($this->records->getArrayCopy() as $record) {
52 | if ($lookupRecord->equals($record)) {
53 | return true;
54 | }
55 | }
56 |
57 | return false;
58 | }
59 |
60 | public function current(): ?DNSRecordInterface
61 | {
62 | return $this->records->current();
63 | }
64 |
65 | public function next(): void
66 | {
67 | $this->records->next();
68 | }
69 |
70 | public function key(): bool|int|string|null
71 | {
72 | return $this->records->key();
73 | }
74 |
75 | public function valid(): bool
76 | {
77 | return $this->records->valid();
78 | }
79 |
80 | public function rewind(): void
81 | {
82 | $this->records->rewind();
83 | }
84 |
85 | /**
86 | * @param mixed $offset
87 | */
88 | public function offsetExists($offset): bool
89 | {
90 | return $this->records->offsetExists($offset);
91 | }
92 |
93 | /**
94 | * @param mixed $offset
95 | */
96 | public function offsetGet($offset): DNSRecordInterface
97 | {
98 | return $this->records->offsetGet($offset);
99 | }
100 |
101 | /**
102 | * @param mixed $offset
103 | * @param mixed $value
104 | * @throws \RemotelyLiving\PHPDNS\Exceptions\InvalidArgumentException
105 | */
106 | public function offsetSet($offset, $value): void
107 | {
108 | if (!$value instanceof DNSRecordInterface) {
109 | throw new InvalidArgumentException('Invalid value');
110 | }
111 |
112 | $this->records->offsetSet($offset, /** @scrutinizer ignore-type */ $value);
113 | }
114 |
115 | /**
116 | * @param mixed $offset
117 | */
118 | public function offsetUnset($offset): void
119 | {
120 | $this->records->offsetUnset($offset);
121 | }
122 |
123 | public function count(): int
124 | {
125 | return $this->records->count();
126 | }
127 |
128 | public function isEmpty(): bool
129 | {
130 | return $this->count() === 0;
131 | }
132 |
133 | public function __serialize(): array
134 | {
135 | return $this->records->getArrayCopy();
136 | }
137 |
138 | public function __unserialize(array $unserialized): void
139 | {
140 | $this->records = new ArrayIterator($unserialized);
141 | }
142 |
143 | public function jsonSerialize(): array
144 | {
145 | return $this->toArray();
146 | }
147 |
148 | public function withUniqueValuesExcluded(): self
149 | {
150 | return $this->filterValues(
151 | fn(DNSRecordInterface $candidateRecord, DNSRecordCollection $remaining): bool => $remaining->has(
152 | $candidateRecord
153 | )
154 | )->withUniqueValues();
155 | }
156 |
157 | public function withUniqueValues(): self
158 | {
159 | return $this->filterValues(
160 | fn(DNSRecordInterface $candidateRecord, DNSRecordCollection $remaining): bool => !$remaining->has(
161 | $candidateRecord
162 | )
163 | );
164 | }
165 |
166 | private function filterValues(callable $eval): self
167 | {
168 | $filtered = new self();
169 | $records = $this->records->getArrayCopy();
170 |
171 | while ($record = array_shift($records)) {
172 | if ($eval($record, new self(...$records))) {
173 | $filtered[] = $record;
174 | }
175 | }
176 |
177 | return $filtered;
178 | }
179 | }
180 |
--------------------------------------------------------------------------------
/src/Resolvers/Chain.php:
--------------------------------------------------------------------------------
1 | pushResolver($resolver);
38 | }
39 | }
40 |
41 | public function pushResolver(Resolver ...$resolvers): void
42 | {
43 | foreach ($resolvers as $resolver) {
44 | $this->resolvers[] = $resolver;
45 | }
46 | }
47 |
48 | public function withAllResults(): Interfaces\Chain
49 | {
50 | $all = new self(...$this->resolvers);
51 | $all->callThroughStrategy = self::STRATEGY_ALL_RESULTS;
52 |
53 | return $all;
54 | }
55 |
56 | public function withFirstResults(): Interfaces\Chain
57 | {
58 | $first = new self(...$this->resolvers);
59 | $first->callThroughStrategy = self::STRATEGY_FIRST_TO_FIND;
60 |
61 | return $first;
62 | }
63 |
64 | public function withConsensusResults(): Interfaces\Chain
65 | {
66 | $consensus = new self(...$this->resolvers);
67 | $consensus->callThroughStrategy = self::STRATEGY_CONSENSUS;
68 |
69 | return $consensus;
70 | }
71 |
72 | public function randomly(): Interfaces\Chain
73 | {
74 | $randomized = clone $this;
75 | shuffle($randomized->resolvers);
76 |
77 | return $randomized;
78 | }
79 |
80 | public function addSubscriber(EventSubscriberInterface $subscriber): void
81 | {
82 | foreach ($this->resolvers as $resolver) {
83 | if ($resolver instanceof Observable) {
84 | $resolver->addSubscriber($subscriber);
85 | }
86 | }
87 | }
88 |
89 | public function addListener(string $eventName, callable $listener, int $priority = 0): void
90 | {
91 | foreach ($this->resolvers as $resolver) {
92 | if ($resolver instanceof Observable) {
93 | $resolver->addListener($eventName, $listener, $priority);
94 | }
95 | }
96 | }
97 |
98 | public function setLogger(LoggerInterface $logger): void
99 | {
100 | foreach ($this->resolvers as $resolver) {
101 | if ($resolver instanceof LoggerAwareInterface) {
102 | $resolver->setLogger($logger);
103 | }
104 | }
105 | }
106 |
107 | public function hasRecord(DNSRecordInterface $record): bool
108 | {
109 | foreach ($this->resolvers as $resolver) {
110 | if ($resolver->hasRecord($record)) {
111 | return true;
112 | }
113 | }
114 |
115 | return false;
116 | }
117 |
118 | protected function doQuery(Hostname $hostname, DNSRecordType $recordType): DNSRecordCollection
119 | {
120 | $merged = [];
121 |
122 | foreach ($this->resolvers as $resolver) {
123 | try {
124 | $records = $resolver->getRecords((string)$hostname, (string)$recordType);
125 | } catch (Exception $e) {
126 | $this->getLogger()->error(
127 | 'Something went wrong in the chain of resolvers',
128 | ['exception' => json_encode($e, JSON_THROW_ON_ERROR), 'resolver' => $resolver->getName()]
129 | );
130 | continue;
131 | }
132 |
133 | if ($this->callThroughStrategy === self::STRATEGY_FIRST_TO_FIND && !$records->isEmpty()) {
134 | return $records;
135 | }
136 |
137 | /** @var DNSRecord $record */
138 | foreach ($records as $record) {
139 | $merged[] = $record;
140 | }
141 | }
142 |
143 | $collection = new DNSRecordCollection(...$merged);
144 |
145 | return ($this->callThroughStrategy === self::STRATEGY_CONSENSUS)
146 | ? $collection->withUniqueValuesExcluded()
147 | : $collection->withUniqueValues();
148 | }
149 | }
150 |
--------------------------------------------------------------------------------
/tests/Unit/Resolvers/CachedTest.php:
--------------------------------------------------------------------------------
1 | timestamp = time();
42 | $this->dateTimeImmutable = $this->createMock(DateTimeImmutable::class);
43 | $this->dateTimeImmutable->method('setTimeStamp')
44 | ->willReturn($this->dateTimeImmutable);
45 | $this->dateTimeImmutable->method('getTimeStamp')
46 | ->willReturnCallback(fn() => $this->timestamp);
47 |
48 | $this->cacheItem = $this->createMock(CacheItemInterface::class);
49 | $this->cache = $this->createMock(CacheItemPoolInterface::class);
50 | $this->cache->method('getItem')
51 | ->with('a98e0fde8ccac017a92d99e669448572')
52 | ->willReturnCallback(fn() => $this->cacheItem);
53 |
54 | $this->resolver = $this->createMock(Resolver::class);
55 |
56 | $this->DNSRecord1 = DNSRecord::createFromPrimitives('A', 'example.com', 0);
57 | $this->DNSRecord2 = DNSRecord::createFromPrimitives('AAAA', 'example.com', 100);
58 | $this->DNSRecord3 = DNSRecord::createFromPrimitives('MX', 'example.com', 200);
59 | $this->DNSRecordCollection = new DNSRecordCollection(...[
60 | $this->DNSRecord1,
61 | $this->DNSRecord2,
62 | $this->DNSRecord3
63 | ]);
64 |
65 | $this->resolver->method('getRecords')
66 | ->with('example.com.', 'ANY')
67 | ->willReturnCallback(fn(): DNSRecordCollection => $this->DNSRecordCollection);
68 |
69 | $this->cachedResolver = new Cached($this->cache, $this->resolver);
70 | $this->cachedResolver->setDateTimeImmutable($this->dateTimeImmutable);
71 | }
72 |
73 | public function testCachesUsingLowestTTLOnReturnedRecordSet(): void
74 | {
75 | $this->cacheItem->method('isHit')
76 | ->willReturn(false);
77 |
78 | $this->cacheItem->expects($this->once())
79 | ->method('expiresAfter')
80 | ->with(100);
81 |
82 | $this->cacheItem->expects($this->once())
83 | ->method('set')
84 | ->with(['recordCollection' => $this->DNSRecordCollection, 'timestamp' => $this->timestamp]);
85 |
86 | $this->cache->expects($this->once())
87 | ->method('save')
88 | ->with($this->cacheItem);
89 |
90 | $this->assertEquals($this->DNSRecordCollection, $this->cachedResolver->getRecords('example.com', 'ANY'));
91 | }
92 |
93 | public function testDoesNotCacheEmptyResultsIfOptionIsSet(): void
94 | {
95 | $this->cache->expects($this->never())
96 | ->method('save');
97 |
98 | $this->DNSRecordCollection = new DNSRecordCollection();
99 |
100 | $results = $this->cachedResolver->withEmptyResultCachingDisabled()
101 | ->getRecords('example.com', 'ANY');
102 |
103 | $this->assertEquals($this->DNSRecordCollection, $results);
104 | }
105 |
106 | public function testOnHitReturnsCachedValuesAndAdjustsTTLBasedOnTimeElapsedSinceStorage(): void
107 | {
108 | $this->cacheItem->method('isHit')
109 | ->willReturn(true);
110 |
111 | $this->cacheItem->method('get')
112 | ->willReturn(['recordCollection' => $this->DNSRecordCollection, 'timestamp' => $this->timestamp - 10]);
113 |
114 | $this->cache->expects($this->never())
115 | ->method('save');
116 |
117 | $DNSRecord1 = DNSRecord::createFromPrimitives('A', 'example.com', 0);
118 | $DNSRecord2 = DNSRecord::createFromPrimitives('AAAA', 'example.com', 90);
119 | $DNSRecord3 = DNSRecord::createFromPrimitives('MX', 'example.com', 190);
120 | $expectedDNSRecordCollection = new DNSRecordCollection(...[
121 | $DNSRecord1, $DNSRecord2, $DNSRecord3
122 | ]);
123 |
124 | $this->assertEquals(
125 | $expectedDNSRecordCollection,
126 | $this->cachedResolver->getRecords('example.com', 'ANY')
127 | );
128 | }
129 | }
130 |
--------------------------------------------------------------------------------
/tests/Unit/Resolvers/LocalSystemTest.php:
--------------------------------------------------------------------------------
1 | dnsClient = $this->createMock(LocalSystemDNS::class);
27 | $this->localSystemMapper = new LocalSystemMapper();
28 | $this->localSystem = new LocalSystem($this->dnsClient);
29 | $this->assertInstanceOf(ResolverAbstract::class, $this->localSystem);
30 | }
31 |
32 | /**
33 | * @test
34 | */
35 | public function hasOrDoesNotHaveRecord() : void
36 | {
37 | $record = DNSRecord::createFromPrimitives('A', 'facebook.com', 1726, IPAddress::createFromString('192.169.1.1'));
38 |
39 | $this->dnsClient->expects($this->exactly(2))
40 | ->method('getRecord')
41 | ->with('facebook.com', 1)
42 | ->willReturnOnConsecutiveCalls(
43 | self::getEmptyResponse(),
44 | self::buildResponse('A')
45 | );
46 |
47 | $this->assertFalse($this->localSystem->hasRecord($record));
48 | $this->assertTrue($this->localSystem->hasRecord($record));
49 | }
50 |
51 | /**
52 | * @test
53 | */
54 | public function getsHostnameByAddress() : void
55 | {
56 | $expected = Hostname::createFromString('cnn.com');
57 | $IPAddress = IPAddress::createFromString('127.0.0.1');
58 |
59 | $this->dnsClient->method('getHostnameByAddress')
60 | ->with('127.0.0.1')
61 | ->willReturn('cnn.com');
62 |
63 | $this->assertEquals($expected, $this->localSystem->getHostnameByAddress($IPAddress));
64 | }
65 |
66 | /**
67 | * @test
68 | * @dataProvider dnsQueryInterfaceMessageProvider
69 | */
70 | public function getsRecords(string $method, Hostname $hostname, DNSRecordType $type, array $response, $expected)
71 | {
72 | $this->dnsClient->expects($this->once())
73 | ->method('getRecord')
74 | ->with($hostname->getHostnameWithoutTrailingDot(), $this->localSystemMapper->getTypeCodeFromType($type))
75 | ->willReturn($response);
76 |
77 | $actual = $this->localSystem->{$method}($hostname, $type);
78 |
79 | $this->assertEquals($expected, $actual);
80 | }
81 |
82 | public function dnsQueryInterfaceMessageProvider(): array
83 | {
84 | $ARecord = DNSRecord::createFromPrimitives(
85 | 'A',
86 | 'facebook.com',
87 | 375,
88 | IPAddress::createFromString('192.169.1.1')
89 | );
90 |
91 | $CNAMERecord = DNSRecord::createFromPrimitives(
92 | 'CNAME',
93 | 'facebook.com',
94 | 375,
95 | IPAddress::createFromString('192.169.1.1')
96 | );
97 |
98 | $MXRecord = DNSRecord::createFromPrimitives(
99 | 'MX',
100 | 'facebook.com',
101 | 375,
102 | IPAddress::createFromString('192.169.1.1')
103 | );
104 |
105 | $hostname = Hostname::createFromString('facebook.com');
106 |
107 | return [
108 | ['getARecords', $hostname, DNSRecordType::createA(), self::buildResponse('A'), new DNSRecordCollection($ARecord)],
109 | ['getAAAARecords',$hostname, DNSRecordType::createAAAA(), self::getEmptyResponse(), new DNSRecordCollection()],
110 | ['getCNAMERecords',$hostname, DNSRecordType::createCNAME(), self::buildResponse('CNAME'), new DNSRecordCollection($CNAMERecord)],
111 | ['getTXTRecords', $hostname, DNSRecordType::createTXT(), self::getEmptyResponse(), new DNSRecordCollection()],
112 | ['getMXRecords', $hostname, DNSRecordType::createMX(), self::buildResponse('MX'), new DNSRecordCollection($MXRecord)],
113 | ['recordTypeExists', $hostname, DNSRecordType::createMX(), self::buildResponse('MX'), true],
114 | ['recordTypeExists', $hostname, DNSRecordType::createMX(), self::getEmptyResponse(), false],
115 | ];
116 | }
117 |
118 | public static function buildResponse(string $type): array
119 | {
120 | return [
121 | [
122 | 'host' => 'facebook.com',
123 | 'class' => 'IN',
124 | 'ttl' => 375,
125 | 'type' => $type,
126 | 'ipv6' => '192.169.1.1',
127 | ],
128 | ];
129 | }
130 |
131 | public static function getEmptyResponse(): array
132 | {
133 | return [];
134 | }
135 | }
136 |
--------------------------------------------------------------------------------
/src/Entities/DNSRecordType.php:
--------------------------------------------------------------------------------
1 | DNSRecordType::TYPE_A,
47 | 5 => DNSRecordType::TYPE_CNAME,
48 | 13 => DNSRecordType::TYPE_HINFO,
49 | 257 => DNSRecordType::TYPE_CAA,
50 | 15 => DNSRecordType::TYPE_MX,
51 | 2 => DNSRecordType::TYPE_NS,
52 | 12 => DNSRecordType::TYPE_PTR,
53 | 6 => DNSRecordType::TYPE_SOA,
54 | 16 => DNSRecordType::TYPE_TXT,
55 | 28 => DNSRecordType::TYPE_AAAA,
56 | 33 => DNSRecordType::TYPE_SRV,
57 | 35 => DNSRecordType::TYPE_NAPTR,
58 | 38 => DNSRecordType::TYPE_A6,
59 | 255 => DNSRecordType::TYPE_ANY,
60 | ];
61 |
62 | private string $type;
63 |
64 | /**
65 | * @throws \RemotelyLiving\PHPDNS\Exceptions\InvalidArgumentException
66 | */
67 | public function __construct(string $type)
68 | {
69 | if (!in_array($type, self::VALID_TYPES, true)) {
70 | throw new InvalidArgumentException("{$type} is not an existing DNS record type");
71 | }
72 |
73 | $this->type = $type;
74 | }
75 |
76 | public function __toString(): string
77 | {
78 | return $this->type;
79 | }
80 |
81 | /**
82 | * @throws \RemotelyLiving\PHPDNS\Exceptions\InvalidArgumentException
83 | */
84 | public static function createFromInt(int $code): DNSRecordType
85 | {
86 | if (!isset(self::CODE_TYPE_MAP[$code])) {
87 | throw new InvalidArgumentException("{$code} is not able to be mapped to an existing DNS record type");
88 | }
89 |
90 | return new self(self::CODE_TYPE_MAP[$code]);
91 | }
92 |
93 | public static function createFromString(string $type): DNSRecordType
94 | {
95 | return new self($type);
96 | }
97 |
98 | public function toInt(): int
99 | {
100 | return array_flip(self::CODE_TYPE_MAP)[$this->type];
101 | }
102 |
103 | public function isA(string $type): bool
104 | {
105 | return $this->type === strtoupper($type);
106 | }
107 |
108 | public function equals(DNSRecordType $recordType): bool
109 | {
110 | return $this->type === (string)$recordType;
111 | }
112 |
113 | public static function createA(): self
114 | {
115 | return self::createFromString(self::TYPE_A);
116 | }
117 |
118 | public static function createCNAME(): self
119 | {
120 | return self::createFromString(self::TYPE_CNAME);
121 | }
122 |
123 | public static function createHINFO(): self
124 | {
125 | return self::createFromString(self::TYPE_HINFO);
126 | }
127 |
128 | public static function createCAA(): self
129 | {
130 | return self::createFromString(self::TYPE_CAA);
131 | }
132 |
133 | public static function createMX(): self
134 | {
135 | return self::createFromString(self::TYPE_MX);
136 | }
137 |
138 | public static function createNS(): self
139 | {
140 | return self::createFromString(self::TYPE_NS);
141 | }
142 |
143 | public static function createPTR(): self
144 | {
145 | return self::createFromString(self::TYPE_PTR);
146 | }
147 |
148 | public static function createSOA(): self
149 | {
150 | return self::createFromString(self::TYPE_SOA);
151 | }
152 |
153 | public static function createTXT(): self
154 | {
155 | return self::createFromString(self::TYPE_TXT);
156 | }
157 |
158 | public static function createAAAA(): self
159 | {
160 | return self::createFromString(self::TYPE_AAAA);
161 | }
162 |
163 | public static function createSRV(): self
164 | {
165 | return self::createFromString(self::TYPE_SRV);
166 | }
167 |
168 | public static function createNAPTR(): self
169 | {
170 | return self::createFromString(self::TYPE_NAPTR);
171 | }
172 |
173 | public static function createA6(): self
174 | {
175 | return self::createFromString(self::TYPE_A6);
176 | }
177 |
178 | public static function createANY(): self
179 | {
180 | return self::createFromString(self::TYPE_ANY);
181 | }
182 | }
183 |
--------------------------------------------------------------------------------
/tests/Unit/Entities/DataAbstractTest.php:
--------------------------------------------------------------------------------
1 | dataAbstract1 = new class extends DataAbstract implements \Stringable {
21 | public function __toString(): string
22 | {
23 | return 'dataAbstract1';
24 | }
25 |
26 | public function toArray(): array
27 | {
28 | return [];
29 | }
30 |
31 | public function __serialize(): array
32 | {
33 | return ['seralized'];
34 | }
35 |
36 | public function __unserialize(array $serialized): void
37 | {
38 | }
39 | };
40 |
41 | $this->dataAbstract2 = new class extends DataAbstract implements \Stringable {
42 | public function __toString(): string
43 | {
44 | return 'dataAbstract2';
45 | }
46 |
47 | public function toArray(): array
48 | {
49 | return [];
50 | }
51 |
52 | public function __serialize(): array
53 | {
54 | return ['seralized'];
55 | }
56 |
57 | public function __unserialize(array $serialized): void
58 | {
59 | }
60 | };
61 | }
62 |
63 | /**
64 | * @test
65 | */
66 | public function knowsIfEquals(): void
67 | {
68 | $this->assertTrue($this->dataAbstract1->equals($this->dataAbstract1));
69 | $this->assertFalse($this->dataAbstract1->equals($this->dataAbstract2));
70 | }
71 |
72 | /**
73 | * @test
74 | */
75 | public function createsDataByType(): void
76 | {
77 | /** @var \RemotelyLiving\PHPDNS\Entities\TXTData $txtData */
78 | $txtData = $this->dataAbstract1::createFromTypeAndString(DNSRecordType::createTXT(), 'value');
79 | $this->assertSame('value', $txtData->getValue());
80 |
81 | /** @var \RemotelyLiving\PHPDNS\Entities\MXData $mxData */
82 | $mxData = $this->dataAbstract1::createFromTypeAndString(DNSRecordType::createMX(), '60 target.com');
83 | $this->assertSame('target.com.', (string)$mxData->getTarget());
84 | $this->assertSame(60, $mxData->getPriority());
85 |
86 | /** @var \RemotelyLiving\PHPDNS\Entities\NSData $nsData */
87 | $nsData = $this->dataAbstract1::createFromTypeAndString(DNSRecordType::createNS(), 'target.com');
88 | $this->assertSame('target.com.', (string)$nsData->getTarget());
89 |
90 | $soaString = 'ns1.google.com. dns-admin.google.com. 224049761 900 800 1800 60';
91 | $soaData = $this->dataAbstract1::createFromTypeAndString(DNSRecordType::createSOA(), $soaString);
92 | $this->assertSame('ns1.google.com.', (string)$soaData->getMname());
93 | $this->assertSame('dns-admin.google.com.', (string)$soaData->getRname());
94 | $this->assertSame(224_049_761, $soaData->getSerial());
95 | $this->assertSame(900, $soaData->getRefresh());
96 | $this->assertSame(800, $soaData->getRetry());
97 | $this->assertSame(1800, $soaData->getExpire());
98 | $this->assertSame(60, $soaData->getMinTTL());
99 |
100 | $cnameString = 'herp.website';
101 | $cnameData = $this->dataAbstract1::createFromTypeAndString(DNSRecordType::createCNAME(), $cnameString);
102 | $this->assertSame('herp.website.', (string)$cnameData->getHostname());
103 |
104 | $caaString = '0 issue "comodoca.com"';
105 | $caaData = $this->dataAbstract1::createFromTypeAndString(DNSRecordType::createCAA(), $caaString);
106 | $this->assertSame('comodoca.com', $caaData->getValue());
107 | $this->assertSame(0, $caaData->getFlags());
108 | $this->assertSame('issue', $caaData->getTag());
109 |
110 | $srvString = '100 200 9090 target.co.';
111 | $srvData = $this->dataAbstract1::createFromTypeAndString(DNSRecordType::createSRV(), $srvString);
112 | $this->assertSame(100, $srvData->getPriority());
113 | $this->assertSame(200, $srvData->getWeight());
114 | $this->assertSame(9090, $srvData->getPort());
115 | $this->assertSame('target.co.', (string)$srvData->getTarget());
116 | }
117 |
118 | /**
119 | * @test
120 | */
121 | public function createsDataByTypeOrThrows(): void
122 | {
123 | $this->expectException(InvalidArgumentException::class);
124 | $this->dataAbstract1::createFromTypeAndString(DNSRecordType::createA(), '');
125 | }
126 |
127 | /**
128 | * @test
129 | */
130 | public function checksCAADataAndThrowsIfTooManySegments(): void
131 | {
132 | $this->expectException(InvalidArgumentException::class);
133 | // example of bad data from Cloudflare API
134 | $invalid = '0 issue \\# 26 00 09 69 73 73 75 65 77 69 6c 64 6c 65 74 73 65 6e 63 72 79 70 74 2e 6f 72 67';
135 | $this->dataAbstract1::createFromTypeAndString(DNSRecordType::createCAA(), $invalid);
136 | }
137 | }
138 |
--------------------------------------------------------------------------------
/src/Resolvers/ResolverAbstract.php:
--------------------------------------------------------------------------------
1 | name)) {
42 | $explodedClass = explode('\\', $this::class);
43 | $this->name = (string) array_pop($explodedClass);
44 | }
45 |
46 | return $this->name;
47 | }
48 |
49 | public function getARecords(string $hostname): DNSRecordCollection
50 | {
51 | return $this->getRecords($hostname, (string)DNSRecordType::createA());
52 | }
53 |
54 | public function getAAAARecords(string $hostname): DNSRecordCollection
55 | {
56 | return $this->getRecords($hostname, (string)DNSRecordType::createAAAA());
57 | }
58 |
59 | public function getCNAMERecords(string $hostname): DNSRecordCollection
60 | {
61 | return $this->getRecords($hostname, (string)DNSRecordType::createCNAME());
62 | }
63 |
64 | public function getTXTRecords(string $hostname): DNSRecordCollection
65 | {
66 | return $this->getRecords($hostname, (string)DNSRecordType::createTXT());
67 | }
68 |
69 | public function getMXRecords(string $hostname): DNSRecordCollection
70 | {
71 | return $this->getRecords($hostname, (string)DNSRecordType::createMX());
72 | }
73 |
74 | public function recordTypeExists(string $hostname, string $recordType): bool
75 | {
76 | return !$this->getRecords($hostname, $recordType)->isEmpty();
77 | }
78 |
79 | public function hasRecord(DNSRecordInterface $record): bool
80 | {
81 | return $this->getRecords((string)$record->getHostname(), (string)$record->getType())
82 | ->has($record);
83 | }
84 |
85 | /**
86 | * @throws \RemotelyLiving\PHPDNS\Resolvers\Exceptions\QueryFailure
87 | */
88 | public function getRecords(string $hostname, string $recordType = null): DNSRecordCollection
89 | {
90 | $recordType = DNSRecordType::createFromString($recordType ?? 'ANY');
91 | $hostname = Hostname::createFromString($hostname);
92 |
93 | $profile = $this->createProfile("{$this->getName()}:{$hostname}:{$recordType}");
94 | $profile->startTransaction();
95 |
96 | try {
97 | $result = ($recordType->equals(DNSRecordType::createANY()))
98 | ? $this->doQuery($hostname, $recordType)
99 | : $this->doQuery($hostname, $recordType)->filteredByType($recordType);
100 | } catch (QueryFailure $e) {
101 | $dnsQueryFailureEvent = new DNSQueryFailed($this, $hostname, $recordType, $e);
102 | $this->dispatch($dnsQueryFailureEvent);
103 | $this->getLogger()->error(
104 | 'DNS query failed',
105 | [self::EVENT => json_encode($dnsQueryFailureEvent, JSON_THROW_ON_ERROR), 'exception' => $e]
106 | );
107 |
108 | throw $e;
109 | } finally {
110 | $profile->endTransaction();
111 | $profile->samplePeakMemoryUsage();
112 | $dnsQueryProfiledEvent = new DNSQueryProfiled($profile);
113 | $this->dispatch($dnsQueryProfiledEvent);
114 | $this->getLogger()->info('DNS query profiled', [self::EVENT => json_encode($dnsQueryProfiledEvent)]);
115 | }
116 |
117 | $dnsQueriedEvent = new DNSQueried($this, $hostname, $recordType, $result);
118 | $this->dispatch($dnsQueriedEvent);
119 | $this->getLogger()->info('DNS queried', [self::EVENT => json_encode($dnsQueriedEvent, JSON_THROW_ON_ERROR)]);
120 | return $result;
121 | }
122 |
123 | public function mapResults(MapperInterface $mapper, array $results): DNSRecordCollection
124 | {
125 | $collection = new DNSRecordCollection();
126 | array_map(function (array $fields) use (&$collection, $mapper) {
127 | try {
128 | $collection[] = $mapper->mapFields($fields)->toDNSRecord();
129 | } catch (InvalidArgumentException) {
130 | $this->getLogger()->warning('Invalid fields passed to mapper', $fields);
131 | }
132 | }, $results);
133 |
134 | return $collection;
135 | }
136 |
137 | /**
138 | * @throws \RemotelyLiving\PHPDNS\Resolvers\Exceptions\QueryFailure
139 | */
140 | abstract protected function doQuery(Hostname $hostname, DNSRecordType $recordType): DNSRecordCollection;
141 | }
142 |
--------------------------------------------------------------------------------
/tests/Unit/Entities/DNSRecordCollectionTest.php:
--------------------------------------------------------------------------------
1 | dnsRecord1 = DNSRecord::createFromPrimitives(
28 | 'A',
29 | 'google.com',
30 | 123,
31 | '127.0.0.1',
32 | 'AS'
33 | );
34 |
35 | $this->dnsRecord2 = DNSRecord::createFromPrimitives(
36 | 'CNAME',
37 | 'google.com',
38 | 123,
39 | '127.0.0.1',
40 | 'IN'
41 | );
42 |
43 | $this->dnsRecordCollection = new DNSRecordCollection($this->dnsRecord1, $this->dnsRecord2);
44 | }
45 |
46 | /**
47 | * @test
48 | */
49 | public function isTraversable(): void
50 | {
51 | $this->assertTrue(true);
52 | }
53 |
54 | /**
55 | * @test
56 | */
57 | public function isSerializable(): void
58 | {
59 | $this->assertSerializable($this->dnsRecordCollection);
60 | }
61 |
62 | /**
63 | * @test
64 | */
65 | public function picksFirst(): void
66 | {
67 | $this->assertEquals($this->dnsRecord1, $this->dnsRecordCollection->pickFirst());
68 | $this->assertEquals($this->dnsRecord1, $this->dnsRecordCollection->pickFirst());
69 | $this->assertSame(0, $this->dnsRecordCollection->key());
70 | }
71 |
72 | /**
73 | * @test
74 | */
75 | public function filtersByType(): void
76 | {
77 | $filtered = $this->dnsRecordCollection->filteredByType(DNSRecordType::createCNAME());
78 | $this->assertEquals($this->dnsRecord2, iterator_to_array($filtered)[0]);
79 |
80 | $nothing = $this->dnsRecordCollection->filteredByType(DNSRecordType::createTXT());
81 |
82 | $this->assertFalse($nothing->valid());
83 | }
84 |
85 | /**
86 | * @test
87 | */
88 | public function hasARecord(): void
89 | {
90 | $notInCollection = DNSRecord::createFromPrimitives('A', 'facebook.com', 3434);
91 |
92 | $this->assertFalse($this->dnsRecordCollection->has($notInCollection));
93 | $this->assertTrue($this->dnsRecordCollection->has($this->dnsRecord1));
94 | $this->assertTrue($this->dnsRecordCollection->has($this->dnsRecord2));
95 | }
96 |
97 | /**
98 | * @test
99 | */
100 | public function isCountableTraversableIteratable(): void
101 | {
102 | $this->assertInstanceOf(Traversable::class, $this->dnsRecordCollection);
103 | $this->assertInstanceOf(Countable::class, $this->dnsRecordCollection);
104 | $this->assertInstanceOf(Iterator::class, $this->dnsRecordCollection);
105 |
106 | foreach ($this->dnsRecordCollection as $record) {
107 | $this->assertInstanceOf(DNSRecord::class, $record);
108 | }
109 |
110 | $this->assertFalse(isset($this->dnsRecordCollection[2]));
111 |
112 | $this->assertFalse($this->dnsRecordCollection->isEmpty());
113 |
114 | $this->dnsRecordCollection[2] = DNSRecord::createFromPrimitives('A', 'facebook.com', 3434);
115 |
116 | $this->assertSame(3, count($this->dnsRecordCollection));
117 |
118 | $this->assertEquals($this->dnsRecord1, $this->dnsRecordCollection->offsetGet(0));
119 |
120 | unset($this->dnsRecordCollection[0], $this->dnsRecordCollection[1], $this->dnsRecordCollection[2]);
121 |
122 | $this->assertTrue($this->dnsRecordCollection->isEmpty());
123 |
124 | $this->dnsRecordCollection->offsetSet(0, DNSRecord::createFromPrimitives('A', 'facebook.com', 3434));
125 |
126 | $this->assertEquals(
127 | DNSRecord::createFromPrimitives('A', 'facebook.com', 3434),
128 | $this->dnsRecordCollection->offsetGet(0)
129 | );
130 |
131 | $this->assertFalse((bool)$this->dnsRecordCollection->key());
132 | }
133 |
134 | public function testOnlyAllowsDNSRecordsToBeSet(): void
135 | {
136 | $this->expectException(InvalidArgumentException::class);
137 | $this->dnsRecordCollection[0] = 'boop';
138 | }
139 |
140 | /**
141 | * @test
142 | */
143 | public function isArrayable(): void
144 | {
145 | $this->assertArrayableAndEquals([$this->dnsRecord1, $this->dnsRecord2], $this->dnsRecordCollection);
146 | }
147 |
148 | /**
149 | * @test
150 | */
151 | public function isJsonSerializable(): void
152 | {
153 | $this->assertJsonSerializeableAndEquals([$this->dnsRecord1, $this->dnsRecord2], $this->dnsRecordCollection);
154 | }
155 |
156 | /**
157 | * @test
158 | */
159 | public function hasFilterMethods(): void
160 | {
161 | $expectedDupes = new DNSRecordCollection($this->dnsRecord2);
162 | $expectedUniques = new DNSRecordCollection($this->dnsRecord1, $this->dnsRecord2);
163 | $expectedOnlyOneResult = new DNSRecordCollection($this->dnsRecord1);
164 |
165 | $hasDupes = new DNSRecordCollection($this->dnsRecord1, $this->dnsRecord2, $this->dnsRecord2, $this->dnsRecord2);
166 | $hasOneResult = new DNSRecordCollection($this->dnsRecord1);
167 |
168 | $this->assertEquals($expectedDupes, $hasDupes->withUniqueValuesExcluded());
169 | $this->assertEquals($expectedUniques, $hasDupes->withUniqueValues());
170 |
171 | $this->assertEquals($expectedOnlyOneResult, $hasOneResult->withUniqueValues());
172 | $this->assertEquals(new DNSRecordCollection(), $hasOneResult->withUniqueValuesExcluded());
173 | }
174 | }
175 |
--------------------------------------------------------------------------------
/tests/Unit/Resolvers/ResolverAbstractTest.php:
--------------------------------------------------------------------------------
1 | logger = $this->createMock(LoggerInterface::class);
34 | $this->resolver = new TestResolver($this->collection, $this->error);
35 | }
36 |
37 | /**
38 | * @test
39 | */
40 | public function isAnObservableResolver() : void
41 | {
42 | $this->assertInstanceOf(ResolverAbstract::class, $this->resolver);
43 | $this->assertInstanceOf(ObservableResolver::class, $this->resolver);
44 | }
45 |
46 | /**
47 | * @test
48 | */
49 | public function mapsResultsReturnsCollection() : void
50 | {
51 | $dnsRecord = DNSRecord::createFromPrimitives('A', 'boop.com', 123);
52 | $expected = new DNSRecordCollection($dnsRecord);
53 | $results =[['type' => 'A', 'ip' => '192.168.1.1', 'class' => 'IN']];
54 | $mapper = $this->createMock(MapperInterface::class);
55 | $mapper->method('mapFields')
56 | ->with($results[0])
57 | ->willReturn($mapper);
58 |
59 | $mapper->method('toDNSRecord')
60 | ->willReturn($dnsRecord);
61 |
62 | $this->assertEquals($expected, $this->resolver->mapResults($mapper, $results));
63 |
64 | }
65 |
66 | /**
67 | * @test
68 | */
69 | public function mapsResultsAndDiscardsInvalidData() : void
70 | {
71 | $expected = new DNSRecordCollection();
72 | $results =[['type' => 'BAZ', 'ip' => '192.168.1.1', 'class' => 'IN']];
73 | $mapper = $this->createMock(MapperInterface::class);
74 | $mapper->method('mapFields')
75 | ->with($results[0])
76 | ->willReturn($mapper);
77 |
78 | $mapper->method('toDNSRecord')
79 | ->willThrowException(new InvalidArgumentException());
80 |
81 | $this->assertEquals($expected, $this->resolver->mapResults($mapper, $results));
82 |
83 | }
84 |
85 | /**
86 | * @test
87 | */
88 | public function dispatchedEventsOnSuccessfulQuery() : void
89 | {
90 | $dnsQueried = null;
91 | $perProfiled = null;
92 |
93 | $this->resolver->addListener(DNSQueried::getName(), function (DNSQueried $event) use (&$dnsQueried) {
94 | $dnsQueried = $event;
95 | });
96 |
97 | $this->resolver->addListener(DNSQueryProfiled::getName(), function (DNSQueryProfiled $event) use (&$perProfiled) {
98 | $perProfiled = $event;
99 | });
100 |
101 | $this->resolver->getRecords(Hostname::createFromString('facebook.com'));
102 |
103 | $this->assertInstanceOf(DNSQueryProfiled::class, $perProfiled);
104 | $this->assertInstanceOf(DNSQueried::class, $dnsQueried);
105 | }
106 |
107 | /**
108 | * @test
109 | */
110 | public function logsSuccessfulEvents() : void
111 | {
112 | $this->resolver->setLogger($this->logger);
113 |
114 | $this->logger->expects($this->exactly(2))
115 | ->method('info')
116 | ->willReturnCallback(function (string $message, array $context) {
117 | $this->assertStringStartsWith('DNS', $message);
118 | $this->assertArrayHasKey('event', $context);
119 | });
120 |
121 | $this->resolver->getRecords('facebook.com');
122 | }
123 |
124 | /**
125 | * @test
126 | */
127 | public function logsFailures() : void
128 | {
129 | $this->expectException(QueryFailure::class);
130 |
131 | $this->resolver = new TestResolver(null, new QueryFailure());
132 | $this->resolver->setLogger($this->logger);
133 |
134 | $this->logger->expects($this->once())
135 | ->method('info')
136 | ->willReturnCallback(function (string $message, array $context) {
137 | $this->assertStringStartsWith('DNS', $message);
138 | $this->assertArrayHasKey('event', $context);
139 | });
140 |
141 | $this->logger->expects($this->once())
142 | ->method('error')
143 | ->willReturnCallback(function (string $message, array $context) {
144 | $this->assertStringStartsWith('DNS', $message);
145 | $this->assertArrayHasKey('event', $context);
146 | });
147 |
148 | $this->resolver->getRecords('facebook.com');
149 | }
150 |
151 | /**
152 | * @test
153 | */
154 | public function dispatchedEventsOnQueryFailure() : void
155 | {
156 | $dnsQueryFailed = null;
157 | $perProfiled = null;
158 |
159 | $resolver = new TestResolver(null, new QueryFailure());
160 | $resolver->addListener(DNSQueryFailed::getName(), function (DNSQueryFailed $event) use (&$dnsQueryFailed) {
161 | $dnsQueryFailed = $event;
162 | });
163 |
164 | $resolver->addListener(DNSQueryProfiled::getName(), function (DNSQueryProfiled $event) use (&$perProfiled) {
165 | $perProfiled = $event;
166 | });
167 |
168 | try {
169 | $resolver->getRecords(Hostname::createFromString('facebook.com'));
170 | } catch (QueryFailure) {}
171 |
172 | $this->assertInstanceOf(DNSQueryProfiled::class, $perProfiled);
173 | $this->assertInstanceOf(DNSQueryFailed::class, $dnsQueryFailed);
174 | }
175 | }
176 |
--------------------------------------------------------------------------------