xref: /webtrees/app/Http/Middleware/BadBotBlocker.php (revision 1ff45046fabc22237b5d0d8e489c96f031fc598d)
1089dadacSGreg Roach<?php
2089dadacSGreg Roach
3089dadacSGreg Roach/**
4089dadacSGreg Roach * webtrees: online genealogy
5d11be702SGreg Roach * Copyright (C) 2023 webtrees development team
6089dadacSGreg Roach * This program is free software: you can redistribute it and/or modify
7089dadacSGreg Roach * it under the terms of the GNU General Public License as published by
8089dadacSGreg Roach * the Free Software Foundation, either version 3 of the License, or
9089dadacSGreg Roach * (at your option) any later version.
10089dadacSGreg Roach * This program is distributed in the hope that it will be useful,
11089dadacSGreg Roach * but WITHOUT ANY WARRANTY; without even the implied warranty of
12089dadacSGreg Roach * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13089dadacSGreg Roach * GNU General Public License for more details.
14089dadacSGreg Roach * You should have received a copy of the GNU General Public License
1589f7189bSGreg Roach * along with this program. If not, see <https://www.gnu.org/licenses/>.
16089dadacSGreg Roach */
17089dadacSGreg Roach
18089dadacSGreg Roachdeclare(strict_types=1);
19089dadacSGreg Roach
20089dadacSGreg Roachnamespace Fisharebest\Webtrees\Http\Middleware;
21089dadacSGreg Roach
22089dadacSGreg Roachuse Fig\Http\Message\StatusCodeInterface;
236b9cb339SGreg Roachuse Fisharebest\Webtrees\Registry;
24b55cbc6bSGreg Roachuse Fisharebest\Webtrees\Validator;
25d2d58874SGreg Roachuse GuzzleHttp\Client;
26d2d58874SGreg Roachuse GuzzleHttp\Exception\GuzzleException;
27089dadacSGreg Roachuse Iodev\Whois\Loaders\CurlLoader;
28089dadacSGreg Roachuse Iodev\Whois\Modules\Asn\AsnRouteInfo;
29089dadacSGreg Roachuse Iodev\Whois\Whois;
30089dadacSGreg Roachuse IPLib\Address\AddressInterface;
3169675509SGreg Roachuse IPLib\Factory as IPFactory;
32089dadacSGreg Roachuse IPLib\Range\RangeInterface;
33089dadacSGreg Roachuse Psr\Http\Message\ResponseInterface;
34089dadacSGreg Roachuse Psr\Http\Message\ServerRequestInterface;
35089dadacSGreg Roachuse Psr\Http\Server\MiddlewareInterface;
36089dadacSGreg Roachuse Psr\Http\Server\RequestHandlerInterface;
37089dadacSGreg Roachuse Throwable;
38089dadacSGreg Roach
39b7e8616fSGreg Roachuse function array_filter;
40089dadacSGreg Roachuse function array_map;
41089dadacSGreg Roachuse function assert;
42089dadacSGreg Roachuse function gethostbyaddr;
43089dadacSGreg Roachuse function gethostbyname;
44b7e8616fSGreg Roachuse function preg_match_all;
45b7e8616fSGreg Roachuse function random_int;
46089dadacSGreg Roachuse function response;
47dec352c1SGreg Roachuse function str_contains;
48dec352c1SGreg Roachuse function str_ends_with;
49089dadacSGreg Roach
50089dadacSGreg Roach/**
51089dadacSGreg Roach * Middleware to block bad robots before they waste our valuable CPU cycles.
52089dadacSGreg Roach */
53089dadacSGreg Roachclass BadBotBlocker implements MiddlewareInterface
54089dadacSGreg Roach{
55d2d58874SGreg Roach    private const REGEX_OCTET = '(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)';
56d2d58874SGreg Roach    private const REGEX_IPV4  = '/\\b' . self::REGEX_OCTET . '(?:\\.' . self::REGEX_OCTET . '){3}\\b/';
57d2d58874SGreg Roach
58089dadacSGreg Roach    // Cache whois requests.  Try to avoid all caches expiring at the same time.
59089dadacSGreg Roach    private const WHOIS_TTL_MIN = 28 * 86400;
60089dadacSGreg Roach    private const WHOIS_TTL_MAX = 35 * 86400;
61089dadacSGreg Roach    private const WHOIS_TIMEOUT = 5;
62089dadacSGreg Roach
63ffa287a1SGreg Roach    // Bad robots - SEO optimisers, advertisers, etc.  This list is shared with robots.txt.
64ffa287a1SGreg Roach    public const BAD_ROBOTS = [
65089dadacSGreg Roach        'admantx',
66be5f8e6aSGreg Roach        'Adsbot',
67089dadacSGreg Roach        'AhrefsBot',
687fa18cfdSGreg Roach        'Amazonbot', // Until it understands crawl-delay and noindex / nofollow
691dc9522fSGreg Roach        'AntBot', // Aggressive crawler
70227c6666SGreg Roach        'AspiegelBot',
710036e960SGreg Roach        'Awario', // Brand management
72af07e945SGreg Roach        'Barkrowler', // Crawler for babbar.tech
73a10ff261SGreg Roach        'BLEXBot',
74af07e945SGreg Roach        'Bytespider', // Aggressive crawler from Bytedance/TikTok
750d515f58SGreg Roach        'CCBot', // Used to train a number of LLMs
76af07e945SGreg Roach        'CensysInspect', // Vulnerability scanner
770d515f58SGreg Roach        'ChatGPT-User', // Used by ChatGPT during operation
788d25fa6cSGreg Roach        'ClaudeBot', // Collects training data for LLMs
793a3594e9SGreg Roach        'DataForSeoBot', // https://dataforseo.com/dataforseo-bot
80089dadacSGreg Roach        'DotBot',
81af07e945SGreg Roach        'Expanse', // Another pointless crawler
820d515f58SGreg Roach        'FacebookBot', // Collects training data for Facebook's LLM translator.
831dc9522fSGreg Roach        'fidget-spinner-bot', // Agressive crawler
84af07e945SGreg Roach        'Foregenix', // Vulnerability scanner
8593ab9735SGreg Roach        'FriendlyCrawler', // Collects training data for LLMs
86af07e945SGreg Roach        'Go-http-client', // Crawler library used by many bots
870d515f58SGreg Roach        'Google-Extended', // Collects training data for Google Bard
88970c4733SGreg Roach        'GPTBot', // Collects training data for ChatGPT
89089dadacSGreg Roach        'Grapeshot',
90f3d48b69SGreg Roach        'Honolulu-bot', // Aggressive crawer, no info available
91089dadacSGreg Roach        'ia_archiver',
92af07e945SGreg Roach        'internet-measurement', // Driftnet
93af07e945SGreg Roach        'IonCrawl',
94af07e945SGreg Roach        'Java', // Crawler library used by many bots
95c8614595SGreg Roach        'linabot', // Aggressive crawer, no info available
9603bad539SGreg Roach        'Linguee',
9710d27708SGreg Roach        'MegaIndex.ru',
98089dadacSGreg Roach        'MJ12bot',
99d5bb02daSGreg Roach        'netEstate NE',
1000d515f58SGreg Roach        'Omgilibot', // Collects training data for LLMs
101227c6666SGreg Roach        'panscient',
102be5f8e6aSGreg Roach        'PetalBot',
103aa4d6f53SGreg Roach        'phxbot', // Badly written crawler
104089dadacSGreg Roach        'proximic',
105af07e945SGreg Roach        'python-requests', // Crawler library used by many bots
106af07e945SGreg Roach        'Scrapy', // Scraping tool
10710d27708SGreg Roach        'SeekportBot', // Pretends to be a search engine - but isn't
108089dadacSGreg Roach        'SemrushBot',
109f4b15485SGreg Roach        'serpstatbot',
110d5bb02daSGreg Roach        'SEOkicks',
111d5bb02daSGreg Roach        'SiteKiosk',
1121dc9522fSGreg Roach        'test-bot', // Agressive crawler
11345d54b04SGreg Roach        'TinyTestBot',
114be5f8e6aSGreg Roach        'Turnitin',
1157d9d7ecaSGreg Roach        'wp_is_mobile', // Nothing to do with wordpress
116089dadacSGreg Roach        'XoviBot',
11752567a36SGreg Roach        'YisouSpider',
118a10ff261SGreg Roach        'ZoominfoBot',
119089dadacSGreg Roach    ];
120089dadacSGreg Roach
121089dadacSGreg Roach    /**
1225c20d904SGreg Roach     * Some search engines use reverse/forward DNS to verify the IP address.
123089dadacSGreg Roach     *
124891c4176SGreg Roach     * @see https://developer.amazon.com/support/amazonbot
125089dadacSGreg Roach     * @see https://support.google.com/webmasters/answer/80553?hl=en
126089dadacSGreg Roach     * @see https://www.bing.com/webmaster/help/which-crawlers-does-bing-use-8c184ec0
127089dadacSGreg Roach     * @see https://www.bing.com/webmaster/help/how-to-verify-bingbot-3905dc26
128089dadacSGreg Roach     * @see https://yandex.com/support/webmaster/robot-workings/check-yandex-robots.html
12977d0194eSGreg Roach     * @see https://www.mojeek.com/bot.html
13077d0194eSGreg Roach     * @see https://support.apple.com/en-gb/HT204683
131089dadacSGreg Roach     */
1325c20d904SGreg Roach    private const ROBOT_REV_FWD_DNS = [
133891c4176SGreg Roach        'Amazonbot'        => ['.crawl.amazon.com'],
13477d0194eSGreg Roach        'Applebot'         => ['.applebot.apple.com'],
135089dadacSGreg Roach        'BingPreview'      => ['.search.msn.com'],
136089dadacSGreg Roach        'Google'           => ['.google.com', '.googlebot.com'],
137d5bb02daSGreg Roach        'Mail.RU_Bot'      => ['.mail.ru'],
138e47c3c91SGreg Roach        'MicrosoftPreview' => ['.search.msn.com'],
139e47c3c91SGreg Roach        'MojeekBot'        => ['.mojeek.com'],
1408d25fa6cSGreg Roach        'Qwantify'         => ['.qwant.com'],
141089dadacSGreg Roach        'Sogou'            => ['.crawl.sogou.com'],
142089dadacSGreg Roach        'Yahoo'            => ['.crawl.yahoo.net'],
143089dadacSGreg Roach        'Yandex'           => ['.yandex.ru', '.yandex.net', '.yandex.com'],
144e47c3c91SGreg Roach        'bingbot'          => ['.search.msn.com'],
145e47c3c91SGreg Roach        'msnbot'           => ['.search.msn.com'],
146089dadacSGreg Roach    ];
147089dadacSGreg Roach
148089dadacSGreg Roach    /**
1495c20d904SGreg Roach     * Some search engines only use reverse DNS to verify the IP address.
1505c20d904SGreg Roach     *
1515c20d904SGreg Roach     * @see https://help.baidu.com/question?prod_id=99&class=0&id=3001
1521ed9b76dSGreg Roach     * @see https://napoveda.seznam.cz/en/full-text-search/seznambot-crawler
153a9d55ce6SGreg Roach     * @see https://www.ionos.de/terms-gtc/faq-crawler
1545c20d904SGreg Roach     */
1555c20d904SGreg Roach    private const ROBOT_REV_ONLY_DNS = [
1566a8ee1d2SGreg Roach        'Baiduspider' => ['.baidu.com', '.baidu.jp'],
1571ed9b76dSGreg Roach        'FreshBot'    => ['.seznam.cz'],
158a9d55ce6SGreg Roach        'IonCrawl'    => ['.1und1.org'],
159d5bb02daSGreg Roach        'Neevabot'    => ['.neeva.com'],
1608e1afc64SGreg Roach        'SeznamBot'   => ['.seznam.cz'],
1615c20d904SGreg Roach    ];
1625c20d904SGreg Roach
1635c20d904SGreg Roach    /**
164089dadacSGreg Roach     * Some search engines operate from designated IP addresses.
165089dadacSGreg Roach     *
166ad3143ccSGreg Roach     * @see https://www.apple.com/go/applebot
167089dadacSGreg Roach     * @see https://help.duckduckgo.com/duckduckgo-help-pages/results/duckduckbot
168089dadacSGreg Roach     */
169089dadacSGreg Roach    private const ROBOT_IPS = [
170813eb6c8SGreg Roach        'AppleBot'    => [
171813eb6c8SGreg Roach            '17.0.0.0/8',
172813eb6c8SGreg Roach        ],
173089dadacSGreg Roach        'Ask Jeeves'  => [
174089dadacSGreg Roach            '65.214.45.143',
175089dadacSGreg Roach            '65.214.45.148',
176089dadacSGreg Roach            '66.235.124.192',
177089dadacSGreg Roach            '66.235.124.7',
178089dadacSGreg Roach            '66.235.124.101',
179089dadacSGreg Roach            '66.235.124.193',
180089dadacSGreg Roach            '66.235.124.73',
181089dadacSGreg Roach            '66.235.124.196',
182089dadacSGreg Roach            '66.235.124.74',
183089dadacSGreg Roach            '63.123.238.8',
184089dadacSGreg Roach            '202.143.148.61',
185089dadacSGreg Roach        ],
186089dadacSGreg Roach        'DuckDuckBot' => [
187089dadacSGreg Roach            '23.21.227.69',
188089dadacSGreg Roach            '50.16.241.113',
189089dadacSGreg Roach            '50.16.241.114',
190089dadacSGreg Roach            '50.16.241.117',
191089dadacSGreg Roach            '50.16.247.234',
192089dadacSGreg Roach            '52.204.97.54',
193089dadacSGreg Roach            '52.5.190.19',
194089dadacSGreg Roach            '54.197.234.188',
195089dadacSGreg Roach            '54.208.100.253',
196089dadacSGreg Roach            '54.208.102.37',
197089dadacSGreg Roach            '107.21.1.8',
198089dadacSGreg Roach        ],
199089dadacSGreg Roach    ];
200089dadacSGreg Roach
201089dadacSGreg Roach    /**
202d2d58874SGreg Roach     * Some search engines operate from designated IP addresses.
203d2d58874SGreg Roach     *
204d2d58874SGreg Roach     * @see https://bot.seekport.com/
205d2d58874SGreg Roach     */
206d2d58874SGreg Roach    private const ROBOT_IP_FILES = [
207d2d58874SGreg Roach        'SeekportBot' => 'https://bot.seekport.com/seekportbot_ips.txt',
208d2d58874SGreg Roach    ];
209d2d58874SGreg Roach
210d2d58874SGreg Roach    /**
211089dadacSGreg Roach     * Some search engines operate from within a designated autonomous system.
212089dadacSGreg Roach     *
213089dadacSGreg Roach     * @see https://developers.facebook.com/docs/sharing/webmasters/crawler
214cc7171a0SGreg Roach     * @see https://www.facebook.com/peering/
215089dadacSGreg Roach     */
216cc7171a0SGreg Roach    private const ROBOT_ASNS = [
217cc7171a0SGreg Roach        'facebook' => ['AS32934', 'AS63293'],
218cc7171a0SGreg Roach        'twitter'  => ['AS13414'],
219089dadacSGreg Roach    ];
220089dadacSGreg Roach
221089dadacSGreg Roach    /**
222089dadacSGreg Roach     * @param ServerRequestInterface  $request
223089dadacSGreg Roach     * @param RequestHandlerInterface $handler
224089dadacSGreg Roach     *
225089dadacSGreg Roach     * @return ResponseInterface
226089dadacSGreg Roach     */
227089dadacSGreg Roach    public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
228089dadacSGreg Roach    {
229b55cbc6bSGreg Roach        $ua      = Validator::serverParams($request)->string('HTTP_USER_AGENT', '');
230b55cbc6bSGreg Roach        $ip      = Validator::attributes($request)->string('client-ip');
2314a8d2484SGreg Roach        $address = IPFactory::parseAddressString($ip);
232089dadacSGreg Roach        assert($address instanceof AddressInterface);
233089dadacSGreg Roach
234dec352c1SGreg Roach        foreach (self::BAD_ROBOTS as $robot) {
235dec352c1SGreg Roach            if (str_contains($ua, $robot)) {
236089dadacSGreg Roach                return $this->response();
237089dadacSGreg Roach            }
238dec352c1SGreg Roach        }
239089dadacSGreg Roach
2405c20d904SGreg Roach        foreach (self::ROBOT_REV_FWD_DNS as $robot => $valid_domains) {
241dec352c1SGreg Roach            if (str_contains($ua, $robot) && !$this->checkRobotDNS($ip, $valid_domains, false)) {
2425c20d904SGreg Roach                return $this->response();
2435c20d904SGreg Roach            }
2445c20d904SGreg Roach        }
2455c20d904SGreg Roach
2465c20d904SGreg Roach        foreach (self::ROBOT_REV_ONLY_DNS as $robot => $valid_domains) {
247dec352c1SGreg Roach            if (str_contains($ua, $robot) && !$this->checkRobotDNS($ip, $valid_domains, true)) {
248089dadacSGreg Roach                return $this->response();
249089dadacSGreg Roach            }
250089dadacSGreg Roach        }
251089dadacSGreg Roach
252d2d58874SGreg Roach        foreach (self::ROBOT_IPS as $robot => $valid_ip_ranges) {
253dec352c1SGreg Roach            if (str_contains($ua, $robot)) {
254d2d58874SGreg Roach                foreach ($valid_ip_ranges as $ip_range) {
255d2d58874SGreg Roach                    $range = IPFactory::parseRangeString($ip_range);
256d2d58874SGreg Roach
257d2d58874SGreg Roach                    if ($range instanceof RangeInterface && $range->contains($address)) {
258d2d58874SGreg Roach                        continue 2;
259d2d58874SGreg Roach                    }
260d2d58874SGreg Roach                }
261d2d58874SGreg Roach
262d2d58874SGreg Roach                return $this->response();
263d2d58874SGreg Roach            }
264d2d58874SGreg Roach        }
265d2d58874SGreg Roach
266d2d58874SGreg Roach        foreach (self::ROBOT_IP_FILES as $robot => $url) {
267d2d58874SGreg Roach            if (str_contains($ua, $robot)) {
268d2d58874SGreg Roach                $valid_ip_ranges = $this->fetchIpRangesForUrl($robot, $url);
269d2d58874SGreg Roach
270d2d58874SGreg Roach                foreach ($valid_ip_ranges as $ip_range) {
271d2d58874SGreg Roach                    $range = IPFactory::parseRangeString($ip_range);
272813eb6c8SGreg Roach
273813eb6c8SGreg Roach                    if ($range instanceof RangeInterface && $range->contains($address)) {
274813eb6c8SGreg Roach                        continue 2;
275813eb6c8SGreg Roach                    }
276813eb6c8SGreg Roach                }
277813eb6c8SGreg Roach
278089dadacSGreg Roach                return $this->response();
279089dadacSGreg Roach            }
280089dadacSGreg Roach        }
281089dadacSGreg Roach
282cc7171a0SGreg Roach        foreach (self::ROBOT_ASNS as $robot => $asns) {
283cc7171a0SGreg Roach            foreach ($asns as $asn) {
284dec352c1SGreg Roach                if (str_contains($ua, $robot)) {
285089dadacSGreg Roach                    foreach ($this->fetchIpRangesForAsn($asn) as $range) {
286089dadacSGreg Roach                        if ($range->contains($address)) {
287089dadacSGreg Roach                            continue 2;
288089dadacSGreg Roach                        }
289089dadacSGreg Roach                    }
290089dadacSGreg Roach
291089dadacSGreg Roach                    return $this->response();
292089dadacSGreg Roach                }
293089dadacSGreg Roach            }
294cc7171a0SGreg Roach        }
295089dadacSGreg Roach
296617057d4SGreg Roach        // Allow sites to block access from entire networks.
297b55cbc6bSGreg Roach        $block_asn = Validator::attributes($request)->string('block_asn', '');
298b55cbc6bSGreg Roach        preg_match_all('/(AS\d+)/', $block_asn, $matches);
299b55cbc6bSGreg Roach
300617057d4SGreg Roach        foreach ($matches[1] as $asn) {
301617057d4SGreg Roach            foreach ($this->fetchIpRangesForAsn($asn) as $range) {
302617057d4SGreg Roach                if ($range->contains($address)) {
303617057d4SGreg Roach                    return $this->response();
304617057d4SGreg Roach                }
305617057d4SGreg Roach            }
306617057d4SGreg Roach        }
307089dadacSGreg Roach
308089dadacSGreg Roach        return $handler->handle($request);
309089dadacSGreg Roach    }
310089dadacSGreg Roach
311089dadacSGreg Roach    /**
312089dadacSGreg Roach     * Check that an IP address belongs to a robot operator using a forward/reverse DNS lookup.
313089dadacSGreg Roach     *
314089dadacSGreg Roach     * @param string        $ip
315089dadacSGreg Roach     * @param array<string> $valid_domains
3165c20d904SGreg Roach     * @param bool          $reverse_only
317089dadacSGreg Roach     *
318089dadacSGreg Roach     * @return bool
319089dadacSGreg Roach     */
3205c20d904SGreg Roach    private function checkRobotDNS(string $ip, array $valid_domains, bool $reverse_only): bool
321089dadacSGreg Roach    {
322089dadacSGreg Roach        $host = gethostbyaddr($ip);
323089dadacSGreg Roach
324dec352c1SGreg Roach        if ($host === false) {
325089dadacSGreg Roach            return false;
326089dadacSGreg Roach        }
327089dadacSGreg Roach
328dec352c1SGreg Roach        foreach ($valid_domains as $domain) {
329dec352c1SGreg Roach            if (str_ends_with($host, $domain)) {
3305c20d904SGreg Roach                return $reverse_only || $ip === gethostbyname($host);
331089dadacSGreg Roach            }
332dec352c1SGreg Roach        }
333dec352c1SGreg Roach
334dec352c1SGreg Roach        return false;
335dec352c1SGreg Roach    }
336089dadacSGreg Roach
337089dadacSGreg Roach    /**
338089dadacSGreg Roach     * Perform a whois search for an ASN.
339089dadacSGreg Roach     *
340e5766395SGreg Roach     * @param string $asn The autonomous system number to query
341089dadacSGreg Roach     *
342089dadacSGreg Roach     * @return array<RangeInterface>
343089dadacSGreg Roach     */
344089dadacSGreg Roach    private function fetchIpRangesForAsn(string $asn): array
345089dadacSGreg Roach    {
3466b9cb339SGreg Roach        return Registry::cache()->file()->remember('whois-asn-' . $asn, static function () use ($asn): array {
347*1ff45046SGreg Roach            $mapper = static fn (AsnRouteInfo $route_info): RangeInterface|null => IPFactory::parseRangeString($route_info->route ?: $route_info->route6);
348273a564eSGreg Roach
349089dadacSGreg Roach            try {
350089dadacSGreg Roach                $loader = new CurlLoader(self::WHOIS_TIMEOUT);
351089dadacSGreg Roach                $whois  = new Whois($loader);
352089dadacSGreg Roach                $info   = $whois->loadAsnInfo($asn);
353273a564eSGreg Roach                $routes = $info->routes;
354273a564eSGreg Roach                $ranges = array_map($mapper, $routes);
355089dadacSGreg Roach
356089dadacSGreg Roach                return array_filter($ranges);
35728d026adSGreg Roach            } catch (Throwable) {
358089dadacSGreg Roach                return [];
359089dadacSGreg Roach            }
360089dadacSGreg Roach        }, random_int(self::WHOIS_TTL_MIN, self::WHOIS_TTL_MAX));
361089dadacSGreg Roach    }
362089dadacSGreg Roach
363089dadacSGreg Roach    /**
364d2d58874SGreg Roach     * Fetch a list of IP addresses from a remote file.
365d2d58874SGreg Roach     *
366d2d58874SGreg Roach     * @param string $ua
367d2d58874SGreg Roach     * @param string $url
368d2d58874SGreg Roach     *
369d2d58874SGreg Roach     * @return array<string>
370d2d58874SGreg Roach     */
371d2d58874SGreg Roach    private function fetchIpRangesForUrl(string $ua, string $url): array
372d2d58874SGreg Roach    {
373d2d58874SGreg Roach        return Registry::cache()->file()->remember('url-ip-list-' . $ua, static function () use ($url): array {
374d2d58874SGreg Roach            try {
375d2d58874SGreg Roach                $client   = new Client();
376d2d58874SGreg Roach                $response = $client->get($url, ['timeout' => 5]);
377d2d58874SGreg Roach                $contents = $response->getBody()->getContents();
378d2d58874SGreg Roach
379d2d58874SGreg Roach                preg_match_all(self::REGEX_IPV4, $contents, $matches);
380d2d58874SGreg Roach
381d2d58874SGreg Roach                return $matches[0];
382d2d58874SGreg Roach            } catch (GuzzleException) {
383d2d58874SGreg Roach                return [];
384d2d58874SGreg Roach            }
385d2d58874SGreg Roach        }, random_int(self::WHOIS_TTL_MIN, self::WHOIS_TTL_MAX));
386d2d58874SGreg Roach    }
387d2d58874SGreg Roach
388d2d58874SGreg Roach    /**
389089dadacSGreg Roach     * @return ResponseInterface
390089dadacSGreg Roach     */
391089dadacSGreg Roach    private function response(): ResponseInterface
392089dadacSGreg Roach    {
393089dadacSGreg Roach        return response('Not acceptable', StatusCodeInterface::STATUS_NOT_ACCEPTABLE);
394089dadacSGreg Roach    }
395089dadacSGreg Roach}
396