Passed
Push — main ( 7b027b...045415 )
by Alexey
03:36 queued 13s
created

DohMiddleware::appendDnsRecord()   B

Complexity

Conditions 8
Paths 12

Size

Total Lines 52
Code Lines 28

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 21
CRAP Score 8.2621

Importance

Changes 0
Metric Value
cc 8
eloc 28
c 0
b 0
f 0
nc 12
nop 3
dl 0
loc 52
ccs 21
cts 25
cp 0.84
crap 8.2621
rs 8.4444

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
declare(strict_types=1);
4
5
/*
6
 * Copyright (c) Ne-Lexa
7
 *
8
 * For the full copyright and license information, please view
9
 * the LICENSE file that was distributed with this source code.
10
 *
11
 * @see https://github.com/Ne-Lexa/guzzle-doh-middleware
12
 */
13
14
namespace Nelexa\Doh;
15
16
use GuzzleHttp\Exception\GuzzleException;
17
use GuzzleHttp\Promise\PromiseInterface;
18
use Nelexa\Doh\Storage\DnsRecord;
19
use Nelexa\Doh\Storage\StorageFactory;
20
use Nelexa\Doh\Storage\StorageInterface;
21
use Psr\Http\Message\RequestInterface;
22
use Psr\Http\Message\ResponseInterface;
23
use Psr\Log\LoggerInterface;
24
use Psr\Log\NullLogger;
25
26
class DohMiddleware
27
{
28
    public const OPTION_DOH_ENABLED = 'doh';
29
30
    public const OPTION_DOH_TTL = 'doh_ttl';
31
32
    public const OPTION_DOH_SHUFFLE = 'doh_shuffle';
33
34
    public const HEADER_RESPONSE_RESOLVED_IPS = 'X-DoH-Ips';
35
36
    public const HEADER_RESPONSE_CACHE_HIT = 'X-DoH-Cache-Hit';
37
38
    public const HEADER_RESPONSE_CACHE_TTL = 'X-DoH-Cache-TTL';
39
40
    private const DEFAULT_OPTIONS = [
41
        self::OPTION_DOH_ENABLED => true,
42
        self::OPTION_DOH_TTL => null,
43
        self::OPTION_DOH_SHUFFLE => false,
44
    ];
45
46
    /** @var \Closure(\Psr\Http\Message\RequestInterface, array): \GuzzleHttp\Promise\PromiseInterface */
47
    private $nextHandler;
48
49
    /** @var \Nelexa\Doh\DomainResolver */
50
    private $domainResolver;
51
52
    /** @var \Psr\Log\LoggerInterface */
53
    private $logger;
54
55
    /**
56
     * @psalm-param \Closure(\Psr\Http\Message\RequestInterface, array $options): \GuzzleHttp\Promise\PromiseInterface $nextHandler
57
     *
58
     * @param \Closure                             $nextHandler
59
     * @param \Nelexa\Doh\Storage\StorageInterface $storage
60
     * @param string[]                             $dohServers
61
     * @param \Psr\Log\LoggerInterface             $logger
62
     * @param bool                                 $debug
63
     */
64 14
    private function __construct(
65
        \Closure $nextHandler,
66
        StorageInterface $storage,
67
        array $dohServers,
68
        LoggerInterface $logger,
69
        bool $debug = false
70
    ) {
71 14
        $this->nextHandler = $nextHandler;
72 14
        $this->domainResolver = new DomainResolver($storage, $dohServers, $debug);
73 14
        $this->logger = $logger;
74
    }
75
76
    /**
77
     * @param RequestInterface $request
78
     * @param array            $options
79
     *
80
     * @return PromiseInterface
81
     */
82 14
    public function __invoke(RequestInterface $request, array $options): PromiseInterface
83
    {
84 14
        $options += self::DEFAULT_OPTIONS;
85 14
        $domainName = $request->getUri()->getHost();
86
87
        if (
88 14
            !\function_exists('curl_init')
89 14
            || $options[self::OPTION_DOH_ENABLED] === false
90 14
            || $this->isIpOrLocalDomainName($domainName)
91
        ) {
92 1
            return ($this->nextHandler)($request, $options);
93
        }
94
95 13
        $dnsRecord = $this->resolve($domainName, $options);
96
97 13
        if ($dnsRecord !== null && $dnsRecord->count() > 0) {
98 13
            return $this->appendDnsRecord($request, $options, $dnsRecord);
99
        }
100
101
        $this->logger->warning(sprintf('DoH client could not resolve ip addresses for %s domain', $domainName));
102
103
        return ($this->nextHandler)($request, $options);
104
    }
105
106
    /**
107
     * @param \Psr\SimpleCache\CacheInterface|\Psr\Cache\CacheItemPoolInterface|StorageInterface|null $cache
108
     * @param string[]                                                                                $dohServers
109
     * @param \Psr\Log\LoggerInterface|null                                                           $logger
110
     * @param bool                                                                                    $debug
111
     *
112
     * @return callable
113
     */
114 14
    public static function create(
115
        $cache = null,
116
        array $dohServers = [],
117
        ?LoggerInterface $logger = null,
118
        bool $debug = false
119
    ): callable {
120 14
        $storage = StorageFactory::create($cache);
121
122 14
        return static function (\Closure $handler) use ($storage, $dohServers, $logger, $debug) {
123
            /** @psalm-var \Closure(\Psr\Http\Message\RequestInterface, array $options): \GuzzleHttp\Promise\PromiseInterface $handler */
124 14
            return new self($handler, $storage, $dohServers, $logger ?? new NullLogger(), $debug);
125
        };
126
    }
127
128 13
    private function resolve(string $domainName, array $options): ?DnsRecord
129
    {
130
        /** @psalm-suppress InvalidCatch */
131
        try {
132 13
            return $this->domainResolver->resolveDomain($domainName, $options);
133
        } catch (GuzzleException $e) {
134
            $this->logger->error(
135
                sprintf('[DoH] Error resolving %s domain', $domainName),
136
                [
137
                    'domain' => $domainName,
138
                    'exception' => $e,
139
                ]
140
            );
141
        }
142
143
        return null;
144
    }
145
146 13
    private function appendDnsRecord(RequestInterface $request, array $options, DnsRecord $dnsRecord): PromiseInterface
147
    {
148
        /** @var array<int, mixed> $curlOptions */
149
        $curlOptions = [
150
            \CURLOPT_DNS_USE_GLOBAL_CACHE => false, // disable global cache
151
        ];
152
153 13
        $ipAddresses = $dnsRecord->getData();
154
155 13
        if ($options[self::OPTION_DOH_SHUFFLE]) {
156
            if ($this->isSupportShuffleIps()) {
157
                /**
158
                 * @noinspection PhpElementIsNotAvailableInCurrentPhpVersionInspection
159
                 * @psalm-suppress UndefinedConstant
160
                 */
161
                $curlOptions[\CURLOPT_DNS_SHUFFLE_ADDRESSES] = true;
162
            } else {
163
                shuffle($ipAddresses);
164
            }
165
        }
166
167 13
        if (!$this->isSupportMultipleIps()) {
168 13
            $ipAddresses = \array_slice($ipAddresses, 0, 1);
169
        }
170
171 13
        $domainName = $dnsRecord->getDomainName();
172 13
        $ipAddressesString = implode(',', $ipAddresses);
173 13
        $curlOptions[\CURLOPT_RESOLVE] = [
174 13
            $domainName . ':80:' . $ipAddressesString,
175 13
            $domainName . ':443:' . $ipAddressesString,
176
        ];
177
178 13
        $port = $request->getUri()->getPort();
179
180 13
        if ($port !== null && $port !== 80 && $port !== 443) {
181
            $curlOptions[\CURLOPT_RESOLVE][] = $domainName . ':' . $port . ':' . $ipAddressesString;
182
        }
183
184 13
        $this->logger->debug(sprintf('[DoH] Set ip addresses %s for domain %s', $ipAddressesString, $domainName));
185
186 13
        $options['curl'] = $curlOptions;
187
188 13
        return ($this->nextHandler)($request, $options)->then(
189 13
            static function (ResponseInterface $response) use ($ipAddressesString, $dnsRecord) {
190 13
                if ($dnsRecord->cacheHit) {
191 3
                    $cacheTtl = max(0, $dnsRecord->getExpiredAt()->getTimestamp() - time());
192 3
                    $response = $response->withHeader(self::HEADER_RESPONSE_CACHE_TTL, (string) $cacheTtl);
193
                }
194
195
                return $response
196 13
                    ->withHeader(self::HEADER_RESPONSE_CACHE_HIT, var_export($dnsRecord->cacheHit, true))
197 13
                    ->withHeader(self::HEADER_RESPONSE_RESOLVED_IPS, $ipAddressesString)
198
                ;
199
            }
200
        );
201
    }
202
203
    /**
204
     * @see https://curl.haxx.se/libcurl/c/CURLOPT_RESOLVE.html Support for providing multiple IP
205
     *                                                          addresses per entry was added in 7.59.0.
206
     *
207
     * @return bool
208
     */
209 13
    private function isSupportMultipleIps(): bool
210
    {
211 13
        return version_compare((string) curl_version()['version'], '7.59.0', '>=');
0 ignored issues
show
Bug Best Practice introduced by
The expression return version_compare((...sion'], '7.59.0', '>=') could return the type integer which is incompatible with the type-hinted return boolean. Consider adding an additional type-check to rule them out.
Loading history...
212
    }
213
214
    private function isSupportShuffleIps(): bool
215
    {
216
        return \PHP_VERSION_ID >= 70300
217
            && version_compare(
218
                (string) curl_version()['version'],
219
                '7.60.0',
220
                '>='
221
            );
222
    }
223
224 13
    private function isIpOrLocalDomainName(string $domainName): bool
225
    {
226 13
        return filter_var($domainName, \FILTER_VALIDATE_IP)
227 13
            || strcasecmp($domainName, 'localhost') === 0;
228
    }
229
}
230