├── bin ├── php-whois └── php-whois.php ├── .gitignore ├── src └── Iodev │ └── Whois │ ├── Configs │ ├── module.asn.servers.json │ ├── module.tld.parser.auto.json │ ├── module.tld.parser.common.json │ ├── module.tld.parser.block.json │ └── module.tld.parser.indent.json │ ├── Modules │ ├── ModuleType.php │ ├── Asn │ │ ├── AsnResponse.php │ │ ├── AsnInfo.php │ │ ├── AsnRouteInfo.php │ │ ├── AsnServer.php │ │ ├── AsnParser.php │ │ └── AsnModule.php │ ├── Tld │ │ ├── TldResponse.php │ │ ├── TldParser.php │ │ ├── Parsers │ │ │ ├── AutoParser.php │ │ │ ├── IndentParser.php │ │ │ ├── RdapParser.php │ │ │ ├── CommonParser.php │ │ │ └── BlockParser.php │ │ ├── TldInfo.php │ │ ├── TldServer.php │ │ └── TldModule.php │ └── Module.php │ ├── Exceptions │ ├── WhoisException.php │ ├── ServerMismatchException.php │ └── ConnectionException.php │ ├── Loaders │ ├── ILoader.php │ ├── MemcachedLoader.php │ ├── SocketLoader.php │ └── CurlLoader.php │ ├── IFactory.php │ ├── Config.php │ ├── Helpers │ ├── TextHelper.php │ ├── DomainHelper.php │ ├── GroupFilter.php │ ├── GroupTrait.php │ ├── DateHelper.php │ ├── GroupSelector.php │ ├── GroupHelper.php │ └── ParserHelper.php │ ├── DataObject.php │ ├── Whois.php │ └── Factory.php ├── .gitattributes ├── Dockerfile ├── CODE_OF_CONDUCT.md ├── LICENSE ├── composer.json ├── docker-compose.yml └── README.md /bin/php-whois: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | '', 21 | 'text' => '', 22 | 'host' => '', 23 | 'asn' => '', 24 | ]; 25 | } 26 | -------------------------------------------------------------------------------- /src/Iodev/Whois/Modules/Tld/TldResponse.php: -------------------------------------------------------------------------------- 1 | '', 22 | 'text' => '', 23 | 'host' => '', 24 | 'domain' => '', 25 | 'httpCode' => 0, 26 | ]; 27 | } 28 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Code of Conduct 2 | 3 | As contributors and maintainers of the project, we pledge to respect everyone who contributes by posting issues, updating documentation, submitting pull requests, providing feedback in comments, and any other activities. 4 | 5 | Communication must be constructive and never resort to personal attacks, trolling, public or private harassment, insults, or other unprofessional conduct. 6 | 7 | If any member of the community violates this code of conduct, the maintainers of the project may take action, removing issues, comments, and PRs or blocking accounts as deemed appropriate. 8 | -------------------------------------------------------------------------------- /src/Iodev/Whois/IFactory.php: -------------------------------------------------------------------------------- 1 | type = strval($type); 18 | $this->loader = $loader; 19 | } 20 | 21 | /** @var string */ 22 | protected $type; 23 | 24 | /** @var ILoader */ 25 | protected $loader; 26 | 27 | /** 28 | * @return string 29 | */ 30 | public function getType() 31 | { 32 | return $this->type; 33 | } 34 | 35 | /** 36 | * @return ILoader 37 | */ 38 | public function getLoader() 39 | { 40 | return $this->loader; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Iodev/Whois/Config.php: -------------------------------------------------------------------------------- 1 | response = $response; 24 | } 25 | 26 | /** @var AsnResponse */ 27 | protected $response; 28 | 29 | /** @var array */ 30 | protected $dataDefault = [ 31 | "asn" => "", 32 | "routes" => [], 33 | ]; 34 | 35 | /** 36 | * @return AsnResponse 37 | */ 38 | public function getResponse(): AsnResponse 39 | { 40 | return $this->response; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2017 Sergey Sedyshev (i.o.developer@gmail.com) and other contributors 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. -------------------------------------------------------------------------------- /src/Iodev/Whois/Modules/Asn/AsnRouteInfo.php: -------------------------------------------------------------------------------- 1 | "", 37 | "route6" => "", 38 | "descr" => "", 39 | "origin" => "", 40 | "mntBy" => "", 41 | "changed" => "", 42 | "source" => "", 43 | ]; 44 | 45 | /** @var array */ 46 | protected $dataAlias = [ 47 | "mntBy" => "mnt-by", 48 | ]; 49 | } 50 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cccyun/php-whois", 3 | "description": "PHP WHOIS provides parsed and raw whois lookup of domains and ASN routes. PHP 5.4+ and 7+ compatible ", 4 | "keywords": [ 5 | "php", 6 | "whois", 7 | "query", 8 | "tld", 9 | "domain", 10 | "lookup", 11 | "info", 12 | "asn", 13 | "routes", 14 | "parser", 15 | "црщшы" 16 | ], 17 | "type": "library", 18 | "license": "MIT", 19 | "homepage": "https://github.com/netcccyun/php-whois", 20 | "authors": [ 21 | { 22 | "name": "caihong", 23 | "email": "admin@cccyun.cn" 24 | } 25 | ], 26 | "require" : { 27 | "php" : ">=7.2", 28 | "ext-curl": "*", 29 | "ext-mbstring": "*", 30 | "ext-json": "*", 31 | "symfony/polyfill-intl-idn": "^1.27" 32 | }, 33 | "require-dev" : { 34 | "phpunit/phpunit": "^8.0" 35 | }, 36 | "autoload": { 37 | "psr-4": { 38 | "Iodev\\": "src/Iodev/" 39 | } 40 | }, 41 | "autoload-dev": { 42 | "psr-4": { 43 | "": [ "tests/" ] 44 | } 45 | }, 46 | "scripts": { 47 | "test": [ 48 | "phpunit --bootstrap tests/bootstrap.php tests" 49 | ] 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/Iodev/Whois/Loaders/MemcachedLoader.php: -------------------------------------------------------------------------------- 1 | loader = $l; 16 | $this->memcached = $m; 17 | $this->keyPrefix = $keyPrefix; 18 | $this->ttl = $ttl; 19 | } 20 | 21 | /** @var ILoader */ 22 | private $loader; 23 | 24 | /** @var Memcached */ 25 | private $memcached; 26 | 27 | /** @var string */ 28 | private $keyPrefix; 29 | 30 | /** @var int */ 31 | private $ttl; 32 | 33 | /** 34 | * @param string $whoisHost 35 | * @param string $query 36 | * @return string 37 | * @throws ConnectionException 38 | * @throws WhoisException 39 | */ 40 | public function loadText($whoisHost, $query) 41 | { 42 | $key = $this->keyPrefix . md5(serialize([$whoisHost, $query])); 43 | $val = $this->memcached->get($key); 44 | if ($val) { 45 | return unserialize($val); 46 | } 47 | $val = $this->loader->loadText($whoisHost, $query); 48 | $this->memcached->set($key, serialize($val), $this->ttl); 49 | return $val; 50 | } 51 | } -------------------------------------------------------------------------------- /src/Iodev/Whois/Modules/Tld/TldParser.php: -------------------------------------------------------------------------------- 1 | options; 29 | } 30 | 31 | /** 32 | * @param string $key 33 | * @param mixed $def 34 | * @return mixed 35 | */ 36 | public function getOption($key, $def = null) 37 | { 38 | return array_key_exists($key, $this->options) ? $this->options[$key] : $def; 39 | } 40 | 41 | /** 42 | * @param array $options 43 | * @return $this 44 | */ 45 | public function setOptions($options) 46 | { 47 | $this->options = is_array($options) ? $options : []; 48 | return $this; 49 | } 50 | 51 | /** 52 | * @return string 53 | */ 54 | abstract public function getType(); 55 | 56 | /** 57 | * @param array $cfg 58 | * @return $this 59 | */ 60 | abstract public function setConfig($cfg); 61 | 62 | /** 63 | * @param TldResponse $response 64 | * @return TldInfo 65 | */ 66 | abstract public function parseResponse(TldResponse $response); 67 | } 68 | -------------------------------------------------------------------------------- /src/Iodev/Whois/DataObject.php: -------------------------------------------------------------------------------- 1 | data = $data; 18 | } 19 | 20 | /** @var array */ 21 | protected $data; 22 | 23 | /** @var array */ 24 | protected $dataDefault = []; 25 | 26 | /** @var array */ 27 | protected $dataAlias = []; 28 | 29 | /** 30 | * @param string $key 31 | * @return mixed 32 | */ 33 | public function __get($key) 34 | { 35 | $default = $this->dataDefault[$key] ?? null; 36 | $key = $this->dataAlias[$key] ?? $key; 37 | return $this->get($key, $default); 38 | } 39 | 40 | /** 41 | * @param $key 42 | * @param mixed $default 43 | * @return mixed 44 | */ 45 | public function get($key, $default = null) 46 | { 47 | return $this->data[$key] ?? $default; 48 | } 49 | 50 | /** 51 | * @return array 52 | */ 53 | public function getData(): array 54 | { 55 | return $this->data; 56 | } 57 | 58 | /** 59 | * @return array 60 | */ 61 | public function toArray(): array 62 | { 63 | $data = []; 64 | foreach ($this->dataDefault as $key => $default) { 65 | $data[$key] = $this->__get($key); 66 | } 67 | return $data; 68 | } 69 | 70 | public function jsonSerialize(): mixed 71 | { 72 | return $this->toArray(); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/Iodev/Whois/Modules/Asn/AsnServer.php: -------------------------------------------------------------------------------- 1 | host = strval($host); 25 | if (empty($this->host)) { 26 | throw new InvalidArgumentException("Host must be specified"); 27 | } 28 | $this->parser = $parser; 29 | $this->queryFormat = strval(isset($queryFormat) ? $queryFormat : self::DEFAULT_QUERY_FORMAT); 30 | } 31 | 32 | /** @var string */ 33 | private $host; 34 | 35 | /** @var AsnParser */ 36 | private $parser; 37 | 38 | /** @var string */ 39 | private $queryFormat; 40 | 41 | /** 42 | * @return string 43 | */ 44 | public function getHost() 45 | { 46 | return $this->host; 47 | } 48 | 49 | /** 50 | * @return AsnParser 51 | */ 52 | public function getParser() 53 | { 54 | return $this->parser; 55 | } 56 | 57 | /** 58 | * @return string 59 | */ 60 | public function getQueryFormat() 61 | { 62 | return $this->queryFormat; 63 | } 64 | 65 | /** 66 | * @param string $asn 67 | * @return string 68 | */ 69 | public function buildQuery($asn) 70 | { 71 | return sprintf($this->queryFormat, $asn); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/Iodev/Whois/Modules/Tld/Parsers/AutoParser.php: -------------------------------------------------------------------------------- 1 | parsers; 39 | } 40 | 41 | /** 42 | * @param TldParser[] $parsers 43 | * @return $this 44 | */ 45 | public function setParsers(array $parsers) 46 | { 47 | foreach ($parsers as $parser) { 48 | $this->addParser($parser); 49 | } 50 | return $this; 51 | } 52 | 53 | /** 54 | * @param TldParser $parser 55 | * @return $this 56 | */ 57 | public function addParser(TldParser $parser) 58 | { 59 | $this->parsers[] = $parser; 60 | return $this; 61 | } 62 | 63 | /** 64 | * @param TldResponse $response 65 | * @return TldInfo 66 | */ 67 | public function parseResponse(TldResponse $response) 68 | { 69 | $bestInfo = null; 70 | $bestVal = 0; 71 | foreach ($this->parsers as $parser) { 72 | $info = $parser->setOptions($this->options)->parseResponse($response); 73 | if (!$info) { 74 | continue; 75 | } 76 | $val = $info->calcValuation(); 77 | if ($val > $bestVal) { 78 | $bestVal = $val; 79 | $bestInfo = $info; 80 | } 81 | } 82 | return $bestInfo; 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/Iodev/Whois/Modules/Asn/AsnParser.php: -------------------------------------------------------------------------------- 1 | parseBlocks($response->text) as $block) { 20 | if (count($block) > 1) { 21 | $routes[] = $this->createAsnRouteInfo($block); 22 | } 23 | } 24 | if (count($routes) == 0) { 25 | return null; 26 | } 27 | return $this->createAsnInfo($response, $routes); 28 | } 29 | 30 | /** 31 | * @param string $content 32 | * @return array 33 | */ 34 | protected function parseBlocks($content): array 35 | { 36 | return array_map([$this, 'parseBlock'], preg_split('~(\r\n|\r|\n){2,}~ui', $content)); 37 | } 38 | 39 | /** 40 | * @param string $block 41 | * @return array 42 | */ 43 | protected function parseBlock($block): array 44 | { 45 | $dict = []; 46 | foreach (ParserHelper::splitLines($block) as $line) { 47 | $kv = explode(':', $line, 2); 48 | if (count($kv) == 2) { 49 | list($k, $v) = $kv; 50 | $k = trim($k); 51 | $v = trim($v); 52 | $dict[$k] = empty($dict[$k]) ? $v : "{$dict[$k]}\n{$v}"; 53 | } 54 | } 55 | return $dict; 56 | } 57 | 58 | /** 59 | * @param array $block 60 | * @return AsnRouteInfo 61 | */ 62 | protected function createAsnRouteInfo(array $block): AsnRouteInfo 63 | { 64 | return new AsnRouteInfo($block); 65 | } 66 | 67 | /** 68 | * @param AsnResponse $response 69 | * @param AsnRouteInfo[] $routes 70 | * @return AsnInfo 71 | */ 72 | protected function createAsnInfo(AsnResponse $response, array $routes): AsnInfo 73 | { 74 | return new AsnInfo($response, [ 75 | 'asn' => $response->asn, 76 | 'routes' => $routes, 77 | ]); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/Iodev/Whois/Helpers/DomainHelper.php: -------------------------------------------------------------------------------- 1 | = 255) { 33 | return ""; 34 | } 35 | $cor = self::correct($domain); 36 | return idn_to_ascii($cor); 37 | } 38 | 39 | /** 40 | * @param string $domain 41 | * @return string 42 | */ 43 | public static function toUnicode($domain) 44 | { 45 | if (empty($domain) || strlen($domain) >= 255) { 46 | return ""; 47 | } 48 | $cor = self::correct($domain); 49 | return idn_to_utf8($cor); 50 | } 51 | 52 | /** 53 | * @param string $domain 54 | * @return string 55 | */ 56 | public static function filterAscii($domain) 57 | { 58 | if (empty($domain) || strlen($domain) >= 255) { 59 | return ""; 60 | } 61 | $domain = self::correct($domain); 62 | // Pick first part before space 63 | $domain = explode(" ", $domain)[0]; 64 | // All symbols must be valid 65 | if (preg_match('~[^-.\da-z]+~ui', $domain)) { 66 | return ""; 67 | } 68 | return $domain; 69 | } 70 | 71 | /** 72 | * @param string $domain 73 | * @return string 74 | */ 75 | private static function correct($domain) 76 | { 77 | $domain = trim($domain); 78 | // Fix for .UZ whois response 79 | while (preg_match('~\bnot\.defined\.?\b~ui', $domain)) { 80 | $domain = preg_replace('~\bnot\.defined\.?\b~ui', '', $domain); 81 | } 82 | return rtrim(preg_replace('~\s*\.\s*~ui', '.', $domain), ".-\t "); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/Iodev/Whois/Loaders/SocketLoader.php: -------------------------------------------------------------------------------- 1 | setTimeout($timeout); 16 | } 17 | 18 | /** @var int */ 19 | private $timeout; 20 | 21 | /** @var string|bool */ 22 | private $origEnv = false; 23 | 24 | /** 25 | * @return int 26 | */ 27 | public function getTimeout() 28 | { 29 | return $this->timeout; 30 | } 31 | 32 | /** 33 | * @param int $seconds 34 | * @return $this 35 | */ 36 | public function setTimeout($seconds) 37 | { 38 | $this->timeout = max(0, (int)$seconds); 39 | return $this; 40 | } 41 | 42 | /** 43 | * @param string $whoisHost 44 | * @param string $query 45 | * @return string 46 | * @throws ConnectionException 47 | * @throws WhoisException 48 | */ 49 | public function loadText($whoisHost, $query) 50 | { 51 | if (!gethostbyname($whoisHost)) { 52 | throw new ConnectionException("Host is unreachable: $whoisHost"); 53 | } 54 | $errno = null; 55 | $errstr = null; 56 | $handle = @fsockopen($whoisHost, 43, $errno, $errstr, $this->timeout); 57 | if (!$handle) { 58 | throw new ConnectionException($errstr, $errno); 59 | } 60 | 61 | stream_set_timeout($handle, $this->timeout); 62 | 63 | if (false === fwrite($handle, $query)) { 64 | throw new ConnectionException("Query cannot be written"); 65 | } 66 | $text = ""; 67 | while (!feof($handle)) { 68 | $chunk = fread($handle, 8192); 69 | if (false === $chunk || stream_get_meta_data($handle)['timed_out']) { 70 | throw new ConnectionException("Response chunk cannot be read"); 71 | } 72 | $text .= $chunk; 73 | } 74 | fclose($handle); 75 | 76 | return $this->validateResponse(TextHelper::toUtf8($text)); 77 | } 78 | 79 | /** 80 | * @param string $text 81 | * @return mixed 82 | * @throws WhoisException 83 | */ 84 | private function validateResponse($text) 85 | { 86 | if (preg_match('~^WHOIS\s+.*?LIMIT\s+EXCEEDED~ui', $text, $m)) { 87 | throw new WhoisException($m[0]); 88 | } 89 | return $text; 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/Iodev/Whois/Configs/module.tld.parser.common.json: -------------------------------------------------------------------------------- 1 | { 2 | "headerKey": "__HEADER__", 3 | "domainKeys": [ 4 | "~^(complete\\s+)?domain([_\\s]*name)?(\\s+\\(.+?\\))?$~ui", 5 | "~^(Dominio|query)$~ui" 6 | ], 7 | "whoisServerKeys": [ 8 | "~^(registrar\\s+)?whois([_\\s]*server)?$~ui" 9 | ], 10 | "nameServersKeys": [ 11 | "~^(Domain\\s+)?\\s*name[_\\s]*servers?(\\s+\\(.+?\\))?$~ui", 12 | "~^(Domain|dns)\\s+(name\\s*)?servers(in\\s+listed\\s+order)?$~ui", 13 | "~^(nserver|name\\s+server\\s+handle|host\\s?name|dns|name)$~ui", 14 | ["~^(primary|secondary|third|fourth)\\s+server(\\s+hostname)?$~ui"], 15 | ["~^ns_name_\\d+$~ui"] 16 | ], 17 | "nameServersKeysGroups": [ 18 | [ 19 | "~^(ns\\s+1|primary\\s+server(\\s+hostname)?|ns_name_01|domain\\s+server\\s+1)$~ui", 20 | "~^(ns\\s+2|secondary\\s+server(\\s+hostname)?|ns_name_02|domain\\s+server\\s+2)$~ui", 21 | "~^(ns\\s+3|third\\s+server(\\s+hostname)?|ns_name_03|domain\\s+server\\s+3)$~ui", 22 | "~^(ns\\s+4|fourth\\s+server(\\s+hostname)?|ns_name_04|domain\\s+server\\s+4)$~ui" 23 | ] 24 | ], 25 | "dnssecKeys": [ 26 | "~^dnssec$~ui" 27 | ], 28 | "creationDateKeys": [ 29 | "~^(domain\\s+)?(creation|registration)\\s*date$~ui", 30 | "~^domain\\s+(created|registered)$~ui", 31 | "~^(record\\s+)?created|registered(\\s+(on|date))?$~ui", 32 | "~^registration|activation(\\s+time)?$~ui", 33 | "~^(Fecha\\s+de\\s+registro|Relevant\\s+dates)$~ui" 34 | ], 35 | "expirationDateKeys": [ 36 | "~^(domain|registry|registrar|registrar\\s+registration)?\\s*(expiration|expires|expiry|exp)[-\\s]*(date|time|on)$~ui", 37 | "~^Record\\s+will\\s+expire\\s+on|expiry|expires|expire|expiration|paid-till|renewal\\s+date|renewal|valid\\s+until|validity$~ui", 38 | "~^(Fecha\\s+de\\s+vencimiento|Relevant\\s+Dates)$~ui" 39 | ], 40 | "updatedDateKeys": [ 41 | "~^(Modified|Modification)[-_\\s]Date$~ui", 42 | "~^(Domain)?[-_\\s]?(Date)?[-_\\s]?(Last)?[-_\\s]?Modified$~ui", 43 | "~^(Record|Domain)?[-_\\s]?(Last)?[-_\\s]?(Modified|Updated|Update)[-_\\s]?(Date|On)?$~ui", 44 | "~^Changed[-_\\s]?(Date)?$~ui", 45 | "~^Derniere\\s+modification$~ui" 46 | ], 47 | "ownerKeys": [ 48 | "~^Owner[-_\\s](Orgname|Organization)$~ui", 49 | "~^Owner([-_\\s]Name)?$~ui", 50 | "~^(Registrant)?[-_\\s]?(Internationalized|International|Contact)?[-_\\s]?(Organization|Organisation|Organizacion)[-_\\s]?(Loc|Name)?$~ui", 51 | "~^Registrant[-_\\s]?(Name)?$~ui", 52 | "~^Domain[-_\\s]Holder[-_\\s]?(Handle)?$~ui", 53 | "~^Holder(-c)?$~ui", 54 | "~^Org[-_\\s]?(Name)?$~ui", 55 | "~^Tech[-_\\s]Organization$~ui", 56 | "~^Admin[-_\\s]Organization$~ui", 57 | "~^Contact[-_\\s]Name$~ui", 58 | "~^(Name|Last[-_\\s]Name|First[-_\\s]Name|Descr)$~ui" 59 | ], 60 | "registrarKeys": [ 61 | "~^(Current|Sponsoring)?[-_\\s]?Registr?ar[-_\\s]?(Name|Organization|Handle|Created)?$~ui", 62 | "~^Authorized[\\s]Agency$~ui" 63 | ], 64 | "statesKeys": [ 65 | "~^(Domain|Registry|Registration|Ren|Epp)[-_\\s]?(Status|State)$~ui", 66 | "status", 67 | "state", 68 | "query_status" 69 | ], 70 | "notRegisteredStatesDict": { 71 | "not registered": 1, 72 | "no object found": 1, 73 | "not allowed": 1, 74 | "available": 1, 75 | "free": 1, 76 | "220 available": 1, 77 | "510 domain is not managed by this register": 1, 78 | "440 request denied": 1 79 | } 80 | } -------------------------------------------------------------------------------- /src/Iodev/Whois/Loaders/CurlLoader.php: -------------------------------------------------------------------------------- 1 | setTimeout($timeout); 16 | $this->options = []; 17 | } 18 | 19 | /** @var int */ 20 | private $timeout; 21 | 22 | /** @var array */ 23 | private $options; 24 | 25 | /** 26 | * @return int 27 | */ 28 | public function getTimeout() 29 | { 30 | return $this->timeout; 31 | } 32 | 33 | /** 34 | * @param int $seconds 35 | * @return $this 36 | */ 37 | public function setTimeout($seconds) 38 | { 39 | $this->timeout = max(0, (int)$seconds); 40 | return $this; 41 | } 42 | 43 | /** 44 | * @return array 45 | */ 46 | public function getOptions() 47 | { 48 | return $this->options; 49 | } 50 | 51 | /** 52 | * @param array $opts 53 | * @return $this 54 | */ 55 | public function setOptions(array $opts) 56 | { 57 | $this->options = $opts; 58 | return $this; 59 | } 60 | 61 | /** 62 | * @param array $opts 63 | * @return $this 64 | */ 65 | public function replaceOptions(array $opts) 66 | { 67 | $this->options = array_replace($this->options, $opts); 68 | return $this; 69 | } 70 | 71 | /** 72 | * @param string $whoisHost 73 | * @param string $query 74 | * @return string 75 | * @throws ConnectionException 76 | * @throws WhoisException 77 | */ 78 | public function loadText($whoisHost, $query) 79 | { 80 | if (!gethostbynamel($whoisHost)) { 81 | throw new ConnectionException("Host is unreachable: $whoisHost"); 82 | } 83 | $input = fopen('php://temp','r+'); 84 | if (!$input) { 85 | throw new ConnectionException('Query stream not created'); 86 | } 87 | fwrite($input, $query); 88 | rewind($input); 89 | 90 | $curl = curl_init(); 91 | if (!$curl) { 92 | throw new ConnectionException('Curl not created'); 93 | } 94 | curl_setopt_array($curl, array_replace($this->options, [ 95 | CURLOPT_TIMEOUT => $this->timeout, 96 | CURLOPT_RETURNTRANSFER => true, 97 | CURLOPT_PROTOCOLS => CURLPROTO_TELNET, 98 | CURLOPT_URL => "telnet://$whoisHost:43", 99 | CURLOPT_INFILE => $input, 100 | ])); 101 | 102 | $result = curl_exec($curl); 103 | $errstr = curl_error($curl); 104 | $errno = curl_errno($curl); 105 | curl_close($curl); 106 | fclose($input); 107 | 108 | if ($result === false) { 109 | throw new ConnectionException($errstr, $errno); 110 | } 111 | return $this->validateResponse(TextHelper::toUtf8($result)); 112 | } 113 | 114 | /** 115 | * @param string $text 116 | * @return mixed 117 | * @throws WhoisException 118 | */ 119 | private function validateResponse($text) 120 | { 121 | if (preg_match('~^WHOIS\s+.*?LIMIT\s+EXCEEDED~ui', $text, $m)) { 122 | throw new WhoisException($m[0]); 123 | } 124 | return $text; 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /src/Iodev/Whois/Helpers/GroupFilter.php: -------------------------------------------------------------------------------- 1 | groups, 20 | $domain, 21 | $domainKeys, 22 | $this->matchFirstOnly 23 | ); 24 | return $this->setGroups($groups); 25 | } 26 | 27 | /** 28 | * @param array $subsets 29 | * @return $this 30 | */ 31 | public function filterHasSubsetOf($subsets) 32 | { 33 | $subsets = GroupHelper::renderSubsets($subsets, $this->subsetParams); 34 | $groups = GroupHelper::findGroupsHasSubsetOf( 35 | $this->groups, 36 | $subsets, 37 | $this->ignoreCase, 38 | $this->matchFirstOnly 39 | ); 40 | return $this->setGroups($groups); 41 | } 42 | 43 | /** 44 | * @param array $subsetKeys 45 | * @return $this 46 | */ 47 | public function filterHasSubsetKeyOf($subsetKeys) 48 | { 49 | $subsets = []; 50 | foreach ($subsetKeys as $k) { 51 | $subsets[] = [ $k => '' ]; 52 | } 53 | $groups = GroupHelper::findGroupsHasSubsetOf( 54 | $this->groups, 55 | $subsets, 56 | $this->ignoreCase, 57 | $this->matchFirstOnly 58 | ); 59 | return $this->setGroups($groups); 60 | } 61 | 62 | /** 63 | * @return $this 64 | */ 65 | public function filterHasHeader() 66 | { 67 | $groups = GroupHelper::findGroupsHasSubsetOf( 68 | $this->groups, 69 | [[ $this->headerKey => '' ]], 70 | $this->ignoreCase, 71 | $this->matchFirstOnly 72 | ); 73 | return $this->setGroups($groups); 74 | } 75 | 76 | /** 77 | * Replaces special empty values by NULL 78 | * @param array $extraDict 79 | * @return $this 80 | */ 81 | public function handleEmpty($extraDict = []) 82 | { 83 | foreach ($this->groups as $index => &$group) { 84 | foreach ($group as $k => &$v) { 85 | if (is_array($v)) { 86 | foreach ($v as &$subVal) { 87 | if (is_string($subVal) && !empty($extraDict[(string)$subVal])) { 88 | $subVal = null; 89 | } 90 | } 91 | } elseif (!empty($extraDict[(string)$v])) { 92 | $v = null; 93 | } 94 | } 95 | } 96 | return $this; 97 | } 98 | 99 | /** 100 | * @return GroupSelector 101 | */ 102 | public function toSelector() 103 | { 104 | return $this->createSelector() 105 | ->setGroups($this->groups) 106 | ->useIgnoreCase($this->ignoreCase) 107 | ->useMatchFirstOnly($this->matchFirstOnly) 108 | ->setHeaderKey($this->headerKey) 109 | ->setSubsetParams($this->subsetParams); 110 | } 111 | 112 | /** 113 | * @return GroupSelector 114 | */ 115 | protected function createSelector(): GroupSelector 116 | { 117 | return new GroupSelector(); 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /src/Iodev/Whois/Helpers/GroupTrait.php: -------------------------------------------------------------------------------- 1 | groups); 41 | } 42 | 43 | /** 44 | * @return array 45 | */ 46 | public function getFirstGroup() 47 | { 48 | return count($this->groups) ? $this->groups[0] : null; 49 | } 50 | 51 | /** 52 | * @return array 53 | */ 54 | public function getGroups() 55 | { 56 | return $this->groups; 57 | } 58 | 59 | /** 60 | * @param array $groups 61 | * @return $this 62 | */ 63 | public function setGroups($groups) 64 | { 65 | $this->groups = $groups; 66 | return $this; 67 | } 68 | 69 | /** 70 | * @param array $group 71 | * @return $this 72 | */ 73 | public function setOneGroup($group) 74 | { 75 | $this->groups = $group ? [ $group ] : []; 76 | return $this; 77 | } 78 | 79 | /** 80 | * @return $this 81 | */ 82 | public function useFirstGroup() 83 | { 84 | return $this->setOneGroup($this->getFirstGroup()); 85 | } 86 | 87 | /** 88 | * @param array $group 89 | * @return $this 90 | */ 91 | public function useFirstGroupOr($group) 92 | { 93 | $first = $this->getFirstGroup(); 94 | return $this->setOneGroup(empty($first) ? $group : $first); 95 | } 96 | 97 | /** 98 | * @return $this 99 | */ 100 | public function mergeGroups() 101 | { 102 | $finalGroup = []; 103 | foreach ($this->groups as $group) { 104 | $finalGroup = array_merge_recursive($finalGroup, $group); 105 | } 106 | $this->groups = [ $finalGroup ]; 107 | return $this; 108 | } 109 | 110 | /** 111 | * @param string $key 112 | * @return $this 113 | */ 114 | public function setHeaderKey($key) 115 | { 116 | $this->headerKey = $key; 117 | return $this; 118 | } 119 | 120 | /** 121 | * @param array $keys 122 | * @return $this 123 | */ 124 | public function setDomainKeys($keys) 125 | { 126 | $this->domainKeys = $keys; 127 | return $this; 128 | } 129 | 130 | /** 131 | * @param array $params 132 | * @return $this 133 | */ 134 | public function setSubsetParams($params) 135 | { 136 | $this->subsetParams = $params; 137 | return $this; 138 | } 139 | 140 | /** 141 | * @param bool $val 142 | * @return $this 143 | */ 144 | public function useMatchFirstOnly($val) 145 | { 146 | $this->matchFirstOnly = (bool)$val; 147 | return $this; 148 | } 149 | 150 | /** 151 | * @param bool $val 152 | * @return $this 153 | */ 154 | public function useIgnoreCase($val) 155 | { 156 | $this->ignoreCase = (bool)$val; 157 | return $this; 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /src/Iodev/Whois/Whois.php: -------------------------------------------------------------------------------- 1 | loader = $loader; 26 | } 27 | 28 | /** @var IFactory */ 29 | private $factory; 30 | 31 | /** @var ILoader */ 32 | private $loader; 33 | 34 | /** @var TldModule */ 35 | private $tldModule; 36 | 37 | /** @var AsnModule */ 38 | private $asnModule; 39 | 40 | /** 41 | * @param IFactory $factory 42 | * @return $this 43 | */ 44 | public function setFactory(IFactory $factory) 45 | { 46 | $this->factory = $factory; 47 | return $this; 48 | } 49 | 50 | /** 51 | * @return IFactory 52 | */ 53 | public function getFactory(): IFactory 54 | { 55 | return $this->factory ?: Factory::get(); 56 | } 57 | 58 | /** 59 | * @return ILoader 60 | */ 61 | public function getLoader() 62 | { 63 | return $this->loader; 64 | } 65 | 66 | /** 67 | * @return TldModule 68 | */ 69 | public function getTldModule() 70 | { 71 | $this->tldModule = $this->tldModule ?: $this->getFactory()->createTldModule($this); 72 | return $this->tldModule; 73 | } 74 | 75 | /** 76 | * @return AsnModule 77 | */ 78 | public function getAsnModule() 79 | { 80 | $this->asnModule = $this->asnModule ?: $this->getFactory()->createAsnModule($this); 81 | return $this->asnModule; 82 | } 83 | 84 | /** 85 | * @param string $domain 86 | * @return bool 87 | * @throws ServerMismatchException 88 | * @throws ConnectionException 89 | * @throws WhoisException 90 | */ 91 | public function isDomainAvailable($domain) 92 | { 93 | return $this->getTldModule()->isDomainAvailable($domain); 94 | } 95 | 96 | /** 97 | * @param string $domain 98 | * @return TldResponse 99 | * @throws ServerMismatchException 100 | * @throws ConnectionException 101 | * @throws WhoisException 102 | */ 103 | public function lookupDomain($domain) 104 | { 105 | return $this->getTldModule()->lookupDomain($domain); 106 | } 107 | 108 | /** 109 | * @param string $domain 110 | * @return TldInfo 111 | * @throws ServerMismatchException 112 | * @throws ConnectionException 113 | * @throws WhoisException 114 | */ 115 | public function loadDomainInfo($domain) 116 | { 117 | return $this->getTldModule()->loadDomainInfo($domain); 118 | } 119 | 120 | /** 121 | * @param string $asn 122 | * @return AsnResponse 123 | * @throws ConnectionException 124 | * @throws WhoisException 125 | */ 126 | public function lookupAsn($asn) 127 | { 128 | return $this->getAsnModule()->lookupAsn($asn); 129 | } 130 | 131 | /** 132 | * @param string $asn 133 | * @return AsnInfo 134 | * @throws ConnectionException 135 | * @throws WhoisException 136 | */ 137 | public function loadAsnInfo($asn) 138 | { 139 | return $this->getAsnModule()->loadAsnInfo($asn); 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /src/Iodev/Whois/Modules/Asn/AsnModule.php: -------------------------------------------------------------------------------- 1 | servers; 32 | } 33 | 34 | /** 35 | * @param AsnServer[] $servers 36 | * @return $this 37 | */ 38 | public function addServers($servers) 39 | { 40 | return $this->setServers(array_merge($this->servers, $servers)); 41 | } 42 | 43 | /** 44 | * @param AsnServer[] $servers 45 | * @return $this 46 | */ 47 | public function setServers($servers) 48 | { 49 | $this->servers = $servers; 50 | return $this; 51 | } 52 | 53 | /** 54 | * @param string $asn 55 | * @param AsnServer $server 56 | * @return AsnResponse 57 | * @throws ConnectionException 58 | * @throws WhoisException 59 | */ 60 | public function lookupAsn($asn, ?AsnServer $server = null) 61 | { 62 | if ($server) { 63 | return $this->loadResponse($asn, $server); 64 | } 65 | list ($resp, ) = $this->loadData($asn); 66 | return $resp; 67 | } 68 | 69 | /** 70 | * @param $asn 71 | * @param AsnServer $server 72 | * @return AsnInfo 73 | * @throws ConnectionException 74 | * @throws WhoisException 75 | */ 76 | public function loadAsnInfo($asn, ?AsnServer $server = null) 77 | { 78 | if ($server) { 79 | $resp = $this->loadResponse($asn, $server); 80 | return $server->getParser()->parseResponse($resp); 81 | } 82 | list (, $info) = $this->loadData($asn); 83 | return $info; 84 | } 85 | 86 | /** 87 | * @param string $asn 88 | * @return array 89 | * @throws ConnectionException 90 | * @throws WhoisException 91 | */ 92 | private function loadData($asn) 93 | { 94 | $response = null; 95 | $info = null; 96 | $error = null; 97 | foreach ($this->servers as $s) { 98 | try { 99 | $response = $this->loadResponse($asn, $s); 100 | $info = $s->getParser()->parseResponse($response); 101 | if ($info) { 102 | break; 103 | } 104 | } catch (ConnectionException $e) { 105 | $error = $e; 106 | } 107 | } 108 | if (!$response && $error) { 109 | throw $error; 110 | } 111 | return [$response, $info]; 112 | } 113 | 114 | /** 115 | * @param string $asn 116 | * @param AsnServer $server 117 | * @return AsnResponse 118 | * @throws ConnectionException 119 | * @throws WhoisException 120 | */ 121 | private function loadResponse($asn, AsnServer $server) 122 | { 123 | $host = $server->getHost(); 124 | $query = $server->buildQuery($asn); 125 | return new AsnResponse([ 126 | 'asn' => $asn, 127 | 'host' => $host, 128 | 'query' => $query, 129 | 'text' => $this->getLoader()->loadText($host, $query), 130 | ]); 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /src/Iodev/Whois/Helpers/DateHelper.php: -------------------------------------------------------------------------------- 1 | '01', 73 | 'january' => '01', 74 | 'feb' => '02', 75 | 'february' => '02', 76 | 'mar' => '03', 77 | 'march' => '03', 78 | 'apr' => '04', 79 | 'april' => '04', 80 | 'may' => '05', 81 | 'jun' => '06', 82 | 'june' => '06', 83 | 'jul' => '07', 84 | 'july' => '07', 85 | 'aug' => '08', 86 | 'august' => '08', 87 | 'sep' => '09', 88 | 'september' => '09', 89 | 'oct' => '10', 90 | 'october' => '10', 91 | 'nov' => '11', 92 | 'november' => '11', 93 | 'dec' => '12', 94 | 'december' => '12', 95 | ]; 96 | return $mond[strtolower($mon)]; 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/Iodev/Whois/Modules/Tld/Parsers/IndentParser.php: -------------------------------------------------------------------------------- 1 | isAutofix ? TldParser::INDENT_AUTOFIX : TldParser::INDENT; 25 | } 26 | 27 | /** 28 | * @param string $line 29 | * @param string[] $commentChars 30 | * @return bool 31 | */ 32 | public static function validateLine($line, $commentChars = ['%']) 33 | { 34 | if ($line && in_array($line[0], $commentChars)) { 35 | return false; 36 | } 37 | $trimmed = trim($line); 38 | if (strlen($line) == strlen($trimmed)) { 39 | return !preg_match('~^\*.*\*$~ui', $trimmed); 40 | } 41 | return true; 42 | } 43 | 44 | /** 45 | * @param string $line 46 | * @return bool 47 | */ 48 | public static function validateStopline($line) 49 | { 50 | return trim($line) != '--'; 51 | } 52 | 53 | /** 54 | * @param array $block 55 | * @return bool 56 | */ 57 | public static function validateBlock($block) 58 | { 59 | foreach ($block as $line) { 60 | $clean = preg_replace('~\w+://[-\w/\.#@?&:=%]+|\d\d:\d\d:\d\d~ui', '', $line); 61 | if (strpos($clean, ':') !== false) { 62 | return true; 63 | } 64 | } 65 | return false; 66 | } 67 | 68 | /** 69 | * @param string $line 70 | * @return int 71 | */ 72 | public static function biasIndent($line) 73 | { 74 | $trimmed = rtrim($line); 75 | $len = strlen($trimmed); 76 | return ($len > 0 && $trimmed[$len - 1] == ':') ? -1 : 0; 77 | } 78 | 79 | /** 80 | * @param string $text 81 | * @return array 82 | */ 83 | protected function groupsFromText($text) 84 | { 85 | $groups = []; 86 | $lines = ParserHelper::splitLines($text); 87 | if ($this->isAutofix) { 88 | $lines = ParserHelper::autofixTldLines($lines); 89 | $lines = ParserHelper::removeInnerEmpties($lines, [__CLASS__, 'biasIndent']); 90 | } 91 | $lines = array_filter($lines, [__CLASS__, 'validateLine']); 92 | $blocks = ParserHelper::linesToSpacedBlocks($lines, [__CLASS__, 'validateStopline']); 93 | //$blocks = array_filter($blocks, [__CLASS__, 'validateBlock']); 94 | foreach ($blocks as $block) { 95 | $nodes = ParserHelper::blockToIndentedNodes($block, [__CLASS__, 'biasIndent'], 2); 96 | $dict = ParserHelper::nodesToDict($nodes); 97 | $groups[] = ParserHelper::dictToGroup($dict, $this->headerKey); 98 | } 99 | $groups = ParserHelper::joinParentlessGroups($groups); 100 | return $groups; 101 | } 102 | 103 | /** 104 | * @param GroupFilter $rootFilter 105 | * @param GroupFilter $primaryFilter 106 | * @return array 107 | */ 108 | protected function parseStates(GroupFilter $rootFilter, GroupFilter $primaryFilter) 109 | { 110 | return $rootFilter->cloneMe() 111 | ->useMatchFirstOnly(true) 112 | ->filterHasSubsetOf($this->secondaryStatesSubsets) 113 | ->toSelector() 114 | ->selectItems(parent::parseStates($rootFilter, $primaryFilter)) 115 | ->selectKeys($this->statesKeys) 116 | ->mapStates() 117 | ->removeEmpty() 118 | ->removeDuplicates() 119 | ->getAll(); 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /src/Iodev/Whois/Modules/Tld/Parsers/RdapParser.php: -------------------------------------------------------------------------------- 1 | $v) { 29 | $this->{$k} = $v; 30 | } 31 | return $this; 32 | } 33 | 34 | /** 35 | * @param TldResponse $response 36 | * @return TldInfo 37 | */ 38 | public function parseResponse(TldResponse $response) 39 | { 40 | if ($response->httpCode != 200 || !$response->text) { 41 | return null; 42 | } 43 | $arr = json_decode($response->text, true); 44 | if (json_last_error() !== JSON_ERROR_NONE) { 45 | return null; 46 | } 47 | 48 | $nameServers = []; 49 | if (!empty($arr['nameservers'])) { 50 | foreach ($arr['nameservers'] as $ns) { 51 | if (!empty($ns['ldhName']) && !in_array($ns['ldhName'], $nameServers)) { 52 | $nameServers[] = $ns['ldhName']; 53 | } 54 | } 55 | } 56 | $dnssec = isset($arr['secureDNS']['delegationSigned']) && $arr['secureDNS']['delegationSigned'] ? 'signedDelegation' : 'unsigned'; 57 | $creationDate = 0; 58 | $expirationDate = 0; 59 | $updatedDate = 0; 60 | foreach ($arr['events'] as $event) { 61 | if (isset($event['eventAction'])) { 62 | if ($event['eventAction'] == 'registration') { 63 | $creationDate = strtotime($event['eventDate']); 64 | } elseif ($event['eventAction'] == 'expiration') { 65 | $expirationDate = strtotime($event['eventDate']); 66 | } elseif ($event['eventAction'] == 'last changed') { 67 | $updatedDate = strtotime($event['eventDate']); 68 | } 69 | } 70 | } 71 | $states = $arr['status'] ?? []; 72 | 73 | $registrantName = null; 74 | $registrarName = null; 75 | if (isset($arr['entities']) && is_array($arr['entities'])) { 76 | foreach ($arr['entities'] as $entity) { 77 | if (isset($entity['roles']) && is_array($entity['roles'])) { 78 | if (in_array('registrant', $entity['roles'])) { 79 | $registrantName = $this->getVcardName($entity); 80 | } 81 | if (in_array('registrar', $entity['roles'])) { 82 | $registrarName = $this->getVcardName($entity); 83 | } 84 | } 85 | } 86 | } 87 | 88 | $data = [ 89 | "parserType" => $this->getType(), 90 | "domainName" => strtolower($arr['ldhName'] ?? ''), 91 | "whoisServer" => '', 92 | "nameServers" => $nameServers, 93 | "dnssec" => $dnssec, 94 | "creationDate" => $creationDate, 95 | "expirationDate" => $expirationDate, 96 | "updatedDate" => $updatedDate, 97 | "owner" => $registrantName, 98 | "registrar" => $registrarName, 99 | "states" => $states, 100 | ]; 101 | $info = new TldInfo($response, $data); 102 | return $info; 103 | } 104 | 105 | private function getVcardName($entity) 106 | { 107 | if (isset($entity['vcardArray'][1]) && is_array($entity['vcardArray'][1])) { 108 | foreach ($entity['vcardArray'][1] as $vcardItem) { 109 | if (isset($vcardItem[0]) && $vcardItem[0] === 'fn') { 110 | return $vcardItem[3]; 111 | } 112 | } 113 | } 114 | return null; 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /src/Iodev/Whois/Modules/Tld/TldInfo.php: -------------------------------------------------------------------------------- 1 | response = $response; 40 | $this->extra = $extra; 41 | } 42 | 43 | /** @var array */ 44 | protected $dataDefault = [ 45 | "parserType" => "", 46 | "domainName" => "", 47 | "whoisServer" => "", 48 | "nameServers" => [], 49 | "creationDate" => 0, 50 | "expirationDate" => 0, 51 | "updatedDate" => 0, 52 | "states" => [], 53 | "owner" => "", 54 | "registrar" => "", 55 | "dnssec" => "", 56 | ]; 57 | 58 | /** @var TldResponse */ 59 | protected $response; 60 | 61 | /** @var array */ 62 | protected $extra; 63 | 64 | /** 65 | * @return TldResponse 66 | */ 67 | public function getResponse() 68 | { 69 | return $this->response; 70 | } 71 | 72 | /** 73 | * @return array 74 | */ 75 | public function getExtra() 76 | { 77 | return $this->extra; 78 | } 79 | 80 | /** 81 | * @param string $key 82 | * @param mixed $default 83 | * @return mixed 84 | */ 85 | public function getExtraVal($key, $default = null) 86 | { 87 | return $this->extra[$key] ?? $default; 88 | } 89 | 90 | /** 91 | * @return string 92 | */ 93 | public function getDomainNameUnicode() 94 | { 95 | return DomainHelper::toUnicode($this->domainName); 96 | } 97 | 98 | /** 99 | * @param array|null $keys 100 | * @return bool 101 | */ 102 | public function isEmpty($keys = null) 103 | { 104 | $empty = true; 105 | $keys = $keys ? $keys : array_keys($this->data); 106 | foreach ($keys as $key) { 107 | $empty = $empty && empty($this->data[$key]); 108 | } 109 | return $empty; 110 | } 111 | 112 | /** 113 | * @param array $badFirstStatesDict 114 | * @return bool 115 | */ 116 | public function isValuable($badFirstStatesDict = []) 117 | { 118 | $states = $this->states; 119 | $firstState = empty($states) ? '' : reset($states); 120 | $firstState = mb_strtolower(trim($firstState)); 121 | if (!empty($badFirstStatesDict[$firstState])) { 122 | return false; 123 | } 124 | $primaryKeys = ['domainName']; 125 | $secondaryKeys = [ 126 | "states", 127 | "nameServers", 128 | "owner", 129 | "creationDate", 130 | "expirationDate", 131 | "updatedDate", 132 | "registrar", 133 | ]; 134 | return !$this->isEmpty($primaryKeys) && !$this->isEmpty($secondaryKeys); 135 | } 136 | 137 | /** 138 | * @return int 139 | */ 140 | public function calcValuation() 141 | { 142 | $weights = [ 143 | 'domainName' => 100, 144 | 'nameServers' => 20, 145 | 'creationDate' => 6, 146 | 'expirationDate' => 6, 147 | 'updatedDate' => 6, 148 | 'states' => 4, 149 | 'owner' => 4, 150 | 'registrar' => 3, 151 | 'whoisServer' => 2, 152 | 'dnssec' => 2, 153 | ]; 154 | $sum = 0; 155 | foreach ($this->data as $k => $v) { 156 | if (!empty($v) && !empty($weights[$k])) { 157 | $w = $weights[$k]; 158 | $sum += is_array($v) ? $w * count($v) : $w; 159 | } 160 | } 161 | return $sum; 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /bin/php-whois.php: -------------------------------------------------------------------------------- 1 | createWhois(); 86 | $result = $whois->lookupDomain($domain); 87 | 88 | var_dump($result); 89 | } 90 | 91 | function info(string $domain, array $options = []) 92 | { 93 | $options = array_replace([ 94 | 'host' => null, 95 | 'parser' => null, 96 | 'file' => null, 97 | ], $options); 98 | 99 | echo implode("\n", [ 100 | ' action: info', 101 | " domain: '{$domain}'", 102 | sprintf(" options: %s", json_encode($options, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)), 103 | '', 104 | '', 105 | ]); 106 | 107 | $loader = null; 108 | if ($options['file']) { 109 | $loader = new \Iodev\Whois\Loaders\FakeSocketLoader(); 110 | $loader->text = file_get_contents($options['file']); 111 | } 112 | 113 | $tld = Factory::get()->createWhois($loader)->getTldModule(); 114 | $domain = DomainHelper::toAscii($domain); 115 | $servers = $tld->matchServers($domain); 116 | 117 | if (!empty($options['host'])) { 118 | $host = $options['host']; 119 | $filteredServers = array_filter($servers, function (\Iodev\Whois\Modules\Tld\TldServer $server) use ($host) { 120 | return $server->getHost() == $host; 121 | }); 122 | if (count($filteredServers) == 0 && count($servers) > 0) { 123 | $filteredServers = [$servers[0]]; 124 | } 125 | $servers = array_map(function (\Iodev\Whois\Modules\Tld\TldServer $server) use ($host) { 126 | return new \Iodev\Whois\Modules\Tld\TldServer( 127 | $server->getZone(), 128 | $host, 129 | $server->isCentralized(), 130 | $server->getParser(), 131 | $server->getQueryFormat(), 132 | $server->isRdap() 133 | ); 134 | }, $filteredServers); 135 | } 136 | 137 | if (!empty($options['parser'])) { 138 | try { 139 | $parser = Factory::get()->createTldParser($options['parser']); 140 | } catch (\Throwable $e) { 141 | echo "\nCannot create TLD parser with type '{$options['parser']}'\n\n"; 142 | throw $e; 143 | } 144 | $servers = array_map(function (\Iodev\Whois\Modules\Tld\TldServer $server) use ($parser) { 145 | return new \Iodev\Whois\Modules\Tld\TldServer( 146 | $server->getZone(), 147 | $server->getHost(), 148 | $server->isCentralized(), 149 | $parser, 150 | $server->getQueryFormat(), 151 | $server->isRdap() 152 | ); 153 | }, $servers); 154 | } 155 | 156 | [, $info] = $tld->loadDomainData($domain, $servers); 157 | 158 | var_dump($info); 159 | } 160 | 161 | main($argv); 162 | 163 | 164 | -------------------------------------------------------------------------------- /src/Iodev/Whois/Helpers/GroupSelector.php: -------------------------------------------------------------------------------- 1 | items); 20 | } 21 | 22 | /** 23 | * @return array 24 | */ 25 | public function getAll() 26 | { 27 | return $this->items; 28 | } 29 | 30 | /** 31 | * First item 32 | * @param mixed $default 33 | * @return mixed 34 | */ 35 | public function getFirstItem($default = null) 36 | { 37 | return empty($this->items) ? $default : reset($this->items); 38 | } 39 | 40 | /** 41 | * First non-array value 42 | * @param mixed $default 43 | * @return mixed 44 | */ 45 | public function getFirst($default = null) 46 | { 47 | $first = $this->getFirstItem(); 48 | while (is_array($first)) { 49 | $first = count($first) > 0 ? reset($first) : null; 50 | } 51 | return $first !== null ? $first : $default; 52 | } 53 | 54 | /** 55 | * @return $this 56 | */ 57 | public function clean() 58 | { 59 | $this->items = []; 60 | return $this; 61 | } 62 | 63 | /** 64 | * @param array $items 65 | * @return $this 66 | */ 67 | public function selectItems($items) 68 | { 69 | $this->items = array_merge($this->items, $items); 70 | return $this; 71 | } 72 | 73 | /** 74 | * @param string[] $keys 75 | * @return $this 76 | */ 77 | public function selectKeys($keys) 78 | { 79 | foreach ($this->groups as $group) { 80 | $matches = GroupHelper::matchKeys($group, $keys, $this->matchFirstOnly); 81 | foreach ($matches as $match) { 82 | if (is_array($match)) { 83 | $this->items = array_merge($this->items, $match); 84 | } else { 85 | $this->items[] = $match; 86 | } 87 | } 88 | } 89 | return $this; 90 | } 91 | 92 | /** 93 | * @param array $keyGroups 94 | * @return $this 95 | */ 96 | public function selectKeyGroups($keyGroups) 97 | { 98 | foreach ($keyGroups as $keyGroup) { 99 | foreach ($keyGroup as $key) { 100 | $this->selectKeys([ $key ]); 101 | } 102 | } 103 | return $this; 104 | } 105 | 106 | /** 107 | * @return $this 108 | */ 109 | public function removeEmpty() 110 | { 111 | $this->items = array_filter($this->items); 112 | return $this; 113 | } 114 | 115 | /** 116 | * @return $this 117 | */ 118 | public function removeDuplicates() 119 | { 120 | $this->items = array_unique($this->items); 121 | return $this; 122 | } 123 | 124 | public function sort(int $flags = SORT_REGULAR): self 125 | { 126 | sort($this->items, $flags); 127 | return $this; 128 | } 129 | 130 | /** 131 | * @return $this 132 | */ 133 | public function mapDomain() 134 | { 135 | foreach ($this->items as &$item) { 136 | if ($item && preg_match('~([-\pL\d]+\.)+[-\pL\d]+~ui', $item, $m)) { 137 | $item = DomainHelper::filterAscii(DomainHelper::toAscii($m[0])); 138 | } else { 139 | $item = ''; 140 | } 141 | } 142 | return $this; 143 | } 144 | 145 | /** 146 | * @return $this 147 | */ 148 | public function mapAsciiServer() 149 | { 150 | foreach ($this->items as &$item) { 151 | $raw = is_string($item) ? trim($item, '.') : ''; 152 | $item = DomainHelper::filterAscii(DomainHelper::toAscii($raw)); 153 | if ($item && !preg_match('~^([-\pL\d]+\.)+[-\pL\d]+$~ui', $item)) { 154 | if (!preg_match('~^[a-z\d]+-norid$~ui', $item)) { 155 | $item = ''; 156 | } 157 | } 158 | } 159 | return $this; 160 | } 161 | 162 | /** 163 | * @param bool $inverseMMDD 164 | * @return $this 165 | */ 166 | public function mapUnixTime($inverseMMDD = false) 167 | { 168 | $this->items = array_map(function($item) use ($inverseMMDD) { 169 | return is_string($item) ? DateHelper::parseDate($item, $inverseMMDD) : 0; 170 | }, $this->items); 171 | return $this; 172 | } 173 | 174 | /** 175 | * @param bool $removeExtra 176 | * @return $this 177 | */ 178 | public function mapStates($removeExtra = true) 179 | { 180 | $states = []; 181 | foreach ($this->items as $item) { 182 | foreach (ParserHelper::parseStates($item, $removeExtra) as $k => $state) { 183 | if (is_int($k) && is_string($state)) { 184 | $states[] = $state; 185 | } 186 | } 187 | } 188 | $this->items = $states; 189 | return $this; 190 | } 191 | } 192 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | services: 4 | 5 | php-7.2: 6 | volumes: 7 | - php72:/workdir 8 | - "./bin:/workdir/bin:ro" 9 | - "./src:/workdir/src:ro" 10 | - "./tests:/workdir/tests:ro" 11 | - "./composer.json:/workdir/composer.json:ro" 12 | build: 13 | context: . 14 | args: 15 | IMAGE: php:7.2-cli-alpine 16 | PACKAGES: git zip unzip icu-dev 17 | command: '/run-tests.sh' 18 | 19 | php-7.2_intl: 20 | volumes: 21 | - php72_intl:/workdir 22 | - "./bin:/workdir/bin:ro" 23 | - "./src:/workdir/src:ro" 24 | - "./tests:/workdir/tests:ro" 25 | - "./composer.json:/workdir/composer.json:ro" 26 | build: 27 | context: . 28 | args: 29 | IMAGE: php:7.2-cli-alpine 30 | PACKAGES: git zip unzip icu-dev 31 | PHPMODS: intl 32 | command: '/run-tests.sh' 33 | 34 | 35 | php-7.3: 36 | volumes: 37 | - php73:/workdir 38 | - "./bin:/workdir/bin:ro" 39 | - "./src:/workdir/src:ro" 40 | - "./tests:/workdir/tests:ro" 41 | - "./composer.json:/workdir/composer.json:ro" 42 | build: 43 | context: . 44 | args: 45 | IMAGE: php:7.3-cli-alpine 46 | PACKAGES: git zip unzip icu-dev 47 | command: '/run-tests.sh' 48 | 49 | php-7.3_intl: 50 | volumes: 51 | - php73_intl:/workdir 52 | - "./bin:/workdir/bin:ro" 53 | - "./src:/workdir/src:ro" 54 | - "./tests:/workdir/tests:ro" 55 | - "./composer.json:/workdir/composer.json:ro" 56 | build: 57 | context: . 58 | args: 59 | IMAGE: php:7.3-cli-alpine 60 | PACKAGES: git zip unzip icu-dev 61 | PHPMODS: intl 62 | command: '/run-tests.sh' 63 | 64 | 65 | php-7.4: 66 | volumes: 67 | - php74:/workdir 68 | - "./bin:/workdir/bin:ro" 69 | - "./src:/workdir/src:ro" 70 | - "./tests:/workdir/tests:ro" 71 | - "./composer.json:/workdir/composer.json:ro" 72 | build: 73 | context: . 74 | args: 75 | IMAGE: php:7.4-cli-alpine 76 | PACKAGES: git zip unzip icu-dev 77 | command: '/run-tests.sh' 78 | 79 | php-7.4_intl: 80 | volumes: 81 | - php74_intl:/workdir 82 | - "./bin:/workdir/bin:ro" 83 | - "./src:/workdir/src:ro" 84 | - "./tests:/workdir/tests:ro" 85 | - "./composer.json:/workdir/composer.json:ro" 86 | build: 87 | context: . 88 | args: 89 | IMAGE: php:7.4-cli-alpine 90 | PACKAGES: git zip unzip icu-dev 91 | PHPMODS: intl 92 | command: '/run-tests.sh' 93 | 94 | 95 | php-8.0: 96 | volumes: 97 | - php80:/workdir 98 | - "./bin:/workdir/bin:ro" 99 | - "./src:/workdir/src:ro" 100 | - "./tests:/workdir/tests:ro" 101 | - "./composer.json:/workdir/composer.json:ro" 102 | build: 103 | context: . 104 | args: 105 | IMAGE: php:8.0-cli-alpine 106 | PACKAGES: git zip unzip icu-dev 107 | command: '/run-tests.sh' 108 | 109 | php-8.0_intl: 110 | volumes: 111 | - php80_intl:/workdir 112 | - "./bin:/workdir/bin:ro" 113 | - "./src:/workdir/src:ro" 114 | - "./tests:/workdir/tests:ro" 115 | - "./composer.json:/workdir/composer.json:ro" 116 | build: 117 | context: . 118 | args: 119 | IMAGE: php:8.0-cli-alpine 120 | PACKAGES: git zip unzip icu-dev 121 | PHPMODS: intl 122 | command: '/run-tests.sh' 123 | 124 | 125 | php-8.1: 126 | volumes: 127 | - php81:/workdir 128 | - "./bin:/workdir/bin:ro" 129 | - "./src:/workdir/src:ro" 130 | - "./tests:/workdir/tests:ro" 131 | - "./composer.json:/workdir/composer.json:ro" 132 | build: 133 | context: . 134 | args: 135 | IMAGE: php:8.1-cli-alpine 136 | PACKAGES: git zip unzip icu-dev 137 | command: '/run-tests.sh' 138 | 139 | php-8.1_intl: 140 | volumes: 141 | - php81_intl:/workdir 142 | - "./bin:/workdir/bin:ro" 143 | - "./src:/workdir/src:ro" 144 | - "./tests:/workdir/tests:ro" 145 | - "./composer.json:/workdir/composer.json:ro" 146 | build: 147 | context: . 148 | args: 149 | IMAGE: php:8.1-cli-alpine 150 | PACKAGES: git zip unzip icu-dev 151 | PHPMODS: intl 152 | command: '/run-tests.sh' 153 | 154 | 155 | php-8.2: 156 | volumes: 157 | - php82:/workdir 158 | - "./bin:/workdir/bin:ro" 159 | - "./src:/workdir/src:ro" 160 | - "./tests:/workdir/tests:ro" 161 | - "./composer.json:/workdir/composer.json:ro" 162 | build: 163 | context: . 164 | args: 165 | IMAGE: php:8.2-cli-alpine 166 | PACKAGES: git zip unzip icu-dev 167 | command: '/run-tests.sh' 168 | 169 | php-8.2_intl: 170 | volumes: 171 | - php82_intl:/workdir 172 | - "./bin:/workdir/bin:ro" 173 | - "./src:/workdir/src:ro" 174 | - "./tests:/workdir/tests:ro" 175 | - "./composer.json:/workdir/composer.json:ro" 176 | build: 177 | context: . 178 | args: 179 | IMAGE: php:8.2-cli-alpine 180 | PACKAGES: git zip unzip icu-dev 181 | PHPMODS: intl 182 | command: '/run-tests.sh' 183 | 184 | 185 | volumes: 186 | php72: 187 | php72_intl: 188 | php73: 189 | php73_intl: 190 | php74: 191 | php74_intl: 192 | php80: 193 | php80_intl: 194 | php81: 195 | php81_intl: 196 | php82: 197 | php82_intl: 198 | 199 | -------------------------------------------------------------------------------- /src/Iodev/Whois/Configs/module.tld.parser.block.json: -------------------------------------------------------------------------------- 1 | { 2 | "headerKey": "__HEADER__", 3 | "reservedDomainKeys": [ 4 | "~^(reserved\\s+name|blocking|stop-list)$~ui" 5 | ], 6 | "domainKeys": [ 7 | "~^(complete\\s+)?domain([_\\s]*name)?(\\s+\\(.+?\\))?$~ui", 8 | "~^(Dominio|query)$~ui" 9 | ], 10 | "whoisServerKeys": [ 11 | "~^(registrar\\s+)?whois([_\\s]*server)?$~ui" 12 | ], 13 | "nameServersKeys": [ 14 | "~^(Domain\\s+)?\\s*name[_\\s]*servers?(\\s+\\(.+?\\))?$~ui", 15 | "~^(Domain|dns)\\s+(name\\s*)?servers(in\\s+listed\\s+order)?$~ui", 16 | "~^(nserver|name\\s+server\\s+handle|host\\s?name|dns|name)$~ui", 17 | "~^(host\\s?name|dns|name)$~ui", 18 | "~^(primary|secondary|third|fourth)\\s+server(\\s+hostname)?$~ui", 19 | "~^ns_name_\\d+$~ui" 20 | ], 21 | "nameServersKeysGroups": [ 22 | [ 23 | "~^(ns\\s+1|primary\\s+server(\\s+hostname)?|ns_name_01)$~ui", 24 | "~^(ns\\s+2|secondary\\s+server(\\s+hostname)?|ns_name_02)$~ui", 25 | "~^(ns\\s+3|third\\s+server(\\s+hostname)?|ns_name_03)$~ui", 26 | "~^(ns\\s+4|fourth\\s+server(\\s+hostname)?|ns_name_04)$~ui" 27 | ] 28 | ], 29 | "dnssecKeys": [ 30 | "~^dnssec$~ui" 31 | ], 32 | "creationDateKeys": [ 33 | "~^(domain\\s+)?(creation|registration)\\s*date$~ui", 34 | "~^domain\\s+(created|registered)$~ui", 35 | "~^(record\\s+)?created|registered(\\s+(on|date))?$~ui", 36 | "~^registration|activation(\\s+time)?$~ui", 37 | "~^(Fecha\\s+de\\s+registro|Relevant\\s+dates)$~ui" 38 | ], 39 | "expirationDateKeys": [ 40 | "~^(domain|registry|registrar|registrar\\s+registration)?\\s*(expiration|expires|expiry|exp)[-\\s]*(date|time|on)$~ui", 41 | "~^Record\\s+will\\s+expire\\s+on|expiry|expires|expire|expiration|paid-till|renewal\\s+date|renewal|valid\\s+until|validity$~ui", 42 | "~^(Fecha\\s+de\\s+vencimiento|Relevant\\s+Dates)$~ui" 43 | ], 44 | "updatedDateKeys": [ 45 | "~^(Modified|Modification)[-_\\s]Date$~ui", 46 | "~^(domain)?[-_\\s]?(date)?[-_\\s]?(last)?[-_\\s]?modified$~ui", 47 | "~^(Record|Domain)?[-_\\s]?Last[-_\\s](Modified|Updated|Update)[-_\\s]?(Date|On)?$~ui", 48 | "~^(Updated|Changed)[-_\\s]Date$~ui", 49 | "~^Derniere\\s+modification$~ui" 50 | ], 51 | "updatedDateExtraKeys": [ 52 | "~^Updated|Changed$~ui" 53 | ], 54 | "ownerKeys": [ 55 | "~^Owner[-_\\s](Orgname|Organization)$~ui", 56 | "~^Owner([-_\\s]Name)?$~ui", 57 | "~^(Registrant)?[-_\\s]?(Internationalized|International|Contact)?[-_\\s]?(Organization|Organisation|Organizacion)[-_\\s]?(Loc|Name)?$~ui", 58 | "~^Registrant[-_\\s]?(Name)?$~ui", 59 | "~^Domain[-_\\s]Holder[-_\\s]?(Handle)?$~ui", 60 | "~^Holder(-c)?$~ui", 61 | "~^Org[-_\\s]?(Name)?$~ui", 62 | "~^Tech[-_\\s]Organization$~ui", 63 | "~^Admin[-_\\s]Organization$~ui", 64 | "~^Contact[-_\\s]Name$~ui", 65 | "~^(Name|Last[-_\\s]Name|First[-_\\s]Name|Descr)$~ui" 66 | ], 67 | "registrarKeys": [ 68 | "~^(Current|Sponsoring)?[-_\\s]?Registr?ar[-_\\s]?(Name|Organization|Handle|Created)?$~ui", 69 | "~^Authorized[\\s]Agency$~ui" 70 | ], 71 | "statesKeys": [ 72 | "~^(Domain|Registry|Registration|Ren|Epp)[-_\\s]?(Status|State)$~ui", 73 | "status", 74 | "state", 75 | "query_status" 76 | ], 77 | "notRegisteredStatesDict": { 78 | "not registered": 1, 79 | "no object found": 1, 80 | "not allowed": 1, 81 | "available": 1, 82 | "free": 1, 83 | "220 available": 1, 84 | "510 domain is not managed by this register": 1, 85 | "440 request denied": 1 86 | }, 87 | "reservedDomainSubsets": [ 88 | {"~^(reserved\\s+name|blocking|stop-list)$~ui": ""} 89 | ], 90 | "domainSubsets": [ 91 | {"__HEADER__": "domain", "name": "$domain"}, 92 | {"__HEADER__": "domain", "name": "$domainUnicode"}, 93 | {"~^(complete\\s+)?(domain|domainname|domain[\\s_]+?name|dominio|query)\\b~ui": "~\\b$domain\\b~ui"}, 94 | {"~^(complete\\s+)?(domain|domainname|domain[\\s_]+?name|dominio|query)\\b~ui": "~\\b$domainUnicode\\b~ui"} 95 | ], 96 | "primarySubsets": [ 97 | {"admin-c": "", "tech-c": "", "nserver": "", "status": ""} 98 | ], 99 | "statesSubsets": [ 100 | {"domain status": ""} 101 | ], 102 | "nameServersSubsets": [ 103 | {"__HEADER__": "~^((Domain\\s+)?(Name|Dns)\\s*Servers?)(\\s+Information)?(\\s+In\\s+Listed\\s+Order)?$~ui"}, 104 | {"__HEADER__": "~^Domain\\s+INFORMATION$~ui"}, 105 | {"~^(nameserver|ns_name_\\d+|nserver|Name\\s+Server\\s+Handle|Domain\\s+Nameservers|Hostname|dns)$~ui": ""} 106 | ], 107 | "nameServersSparsedSubsets": [ 108 | {"~^(primary|secondary|third|fourth)\\s+server(\\s+hostname)?$~ui": ""} 109 | ], 110 | "ownerSubsets": [ 111 | {"__HEADER__": "~^(Registrant|Owner\\s+Contact|Holder|Organization\\s+Using\\s+Domain\\s+Name|Administrativ\\s+contact|Technical\\s+contact)$~ui"}, 112 | {"__HEADER__": "~^(Tech-C|TITULAR)$~ui", "~^Organi[sz]a[tc]ion$~ui": ""}, 113 | {"Contact": "Registrant", "Organization": ""} 114 | ], 115 | "registrarSubsets": [ 116 | {"__HEADER__": "~^Registrar(\\s+Information)?$~ui"}, 117 | {"__HEADER__": "~^(Zone-C|billing-c)$~ui", "~^Organisation|person$~ui": ""}, 118 | {"__HEADER__": "Billing Contact"}, 119 | {"__HEADER__": "CONTACTO FINANCIERO", "Organizacion": ""}, 120 | {"Contact": "Billing", "Organization": ""}, 121 | {"~^(current\\s+)?registr?ar(_name)?(\\s+created)?$~ui": ""} 122 | ], 123 | "registrarReservedSubsets": [ 124 | {"nsset": "", "~^(billing-c|tech-c)$~ui": ""}, 125 | {"~^(billing-c|tech-c)$~ui": ""} 126 | ], 127 | "registrarReservedKeys": [ 128 | "billing-c", 129 | "tech-c" 130 | ], 131 | "contactSubsets": [ 132 | {"~^(nic-hdl(-br)?|contact|norid\\s+handle)$~ui": "$id"} 133 | ], 134 | "contactOrgKeys": [ 135 | "~^(International)?[-_\\s]?(Organi[zs]a[tc]ion|Org)[-_\\s]?(Loc|Name)?$~ui", 136 | "name", 137 | "contact", 138 | "role", 139 | "address", 140 | "person" 141 | ], 142 | "registrarGroupKeys": [ 143 | "~^(Current|Sponsoring)?[-_\\s]?Registr?ar[-_\\s]?(Name|Organization|Handle|Created)?$~ui", 144 | "~^(International)?[-_\\s]?(Organi[zs]a[tc]ion|Org)[-_\\s]?(Loc|Name)?$~ui", 145 | "~^(International\\sName|Name|Address|Person|Billing-c)$~ui" 146 | ] 147 | } 148 | -------------------------------------------------------------------------------- /src/Iodev/Whois/Configs/module.tld.parser.indent.json: -------------------------------------------------------------------------------- 1 | { 2 | "headerKey": "__HEADER__", 3 | "reservedDomainKeys": [ 4 | "~^(reserved\\s+name|blocking|stop-list)$~ui" 5 | ], 6 | "domainKeys": [ 7 | "~^(complete\\s+)?domain([_\\s]*name)?(\\s+\\(.+?\\))?$~ui", 8 | "~^(Dominio|query)$~ui" 9 | ], 10 | "whoisServerKeys": [ 11 | "~^(registrar\\s+)?whois([_\\s]*server)?$~ui" 12 | ], 13 | "nameServersKeys": [ 14 | "~^(Domain\\s+)?\\s*name[_\\s]*servers?(\\s+\\(.+?\\))?$~ui", 15 | "~^(Domain|dns)\\s+(name\\s*)?servers(in\\s+listed\\s+order)?$~ui", 16 | "~^(nserver|name\\s+server)\\s+(handle|information)$~ui", 17 | "~^(host\\s?name|dns|name)$~ui", 18 | "~^(primary|secondary|third|fourth)\\s+server(\\s+hostname)?$~ui", 19 | "~^ns_name_\\d+$~ui" 20 | ], 21 | "nameServersKeysGroups": [ 22 | [ 23 | "~^(ns\\s+1|primary\\s+server(\\s+hostname)?|ns_name_01)$~ui", 24 | "~^(ns\\s+2|secondary\\s+server(\\s+hostname)?|ns_name_02)$~ui", 25 | "~^(ns\\s+3|third\\s+server(\\s+hostname)?|ns_name_03)$~ui", 26 | "~^(ns\\s+4|fourth\\s+server(\\s+hostname)?|ns_name_04)$~ui" 27 | ] 28 | ], 29 | "dnssecKeys": [ 30 | "~^dnssec$~ui" 31 | ], 32 | "creationDateKeys": [ 33 | "~^(domain\\s+)?(creation|registration)\\s*date$~ui", 34 | "~^domain\\s+(created|registered)$~ui", 35 | "~^(record\\s+)?created|registered(\\s+(on|date))?$~ui", 36 | "~^registration|activation(\\s+time)?$~ui", 37 | "~^(Fecha\\s+de\\s+registro|Relevant\\s+dates)$~ui" 38 | ], 39 | "expirationDateKeys": [ 40 | "~^(domain|registry|registrar|registrar\\s+registration)?\\s*(expiration|expires|expiry|exp)[-\\s]*(date|time|on)$~ui", 41 | "~^Record\\s+will\\s+expire\\s+on|expiry|expires|expire|expiration|paid-till|renewal\\s+date|renewal|valid\\s+until|validity$~ui", 42 | "~^(Fecha\\s+de\\s+vencimiento|Relevant\\s+Dates)$~ui" 43 | ], 44 | "updatedDateKeys": [ 45 | "~^(Modified|Modification)[-_\\s]Date$~ui", 46 | "~^(domain)?[-_\\s]?(date)?[-_\\s]?(last)?[-_\\s]?modified$~ui", 47 | "~^(Record|Domain)?[-_\\s]?Last[-_\\s](Modified|Updated|Update)[-_\\s]?(Date|On)?$~ui", 48 | "~^(Updated|Changed)[-_\\s]Date$~ui", 49 | "~^Derniere\\s+modification$~ui" 50 | ], 51 | "updatedDateExtraKeys": [ 52 | "~^Updated|Changed$~ui" 53 | ], 54 | "ownerKeys": [ 55 | "~^Owner[-_\\s](Orgname|Organization)$~ui", 56 | "~^Owner([-_\\s]Name)?$~ui", 57 | "~^(Registrant)?[-_\\s]?(Internationalized|International|Contact)?[-_\\s]?(Organization|Organisation|Organizacion)[-_\\s]?(Loc|Name)?$~ui", 58 | "~^Registrant[-_\\s]?(Name)?$~ui", 59 | "~^Domain[-_\\s]Holder[-_\\s]?(Handle)?$~ui", 60 | "~^Holder(-c)?$~ui", 61 | "~^Org[-_\\s]?(Name)?$~ui", 62 | "~^Tech[-_\\s]Organization$~ui", 63 | "~^Admin[-_\\s]Organization$~ui", 64 | "~^Contact[-_\\s]Name$~ui", 65 | "~^(Name|Last[-_\\s]Name|First[-_\\s]Name|Descr)$~ui" 66 | ], 67 | "registrarKeys": [ 68 | "~^(Current|Sponsoring)?[-_\\s]?Registr?ar[-_\\s]?(Name|Organization|Handle|Created)?$~ui", 69 | "~^Authorized[\\s]Agency$~ui" 70 | ], 71 | "statesKeys": [ 72 | "~^(Domain|Registry|Registration|Ren|Epp)[-_\\s]?(Status|State)$~ui", 73 | "status", 74 | "state", 75 | "flags", 76 | "query_status" 77 | ], 78 | "notRegisteredStatesDict": { 79 | "not registered": 1, 80 | "no object found": 1, 81 | "not allowed": 1, 82 | "available": 1, 83 | "free": 1, 84 | "220 available": 1, 85 | "510 domain is not managed by this register": 1, 86 | "440 request denied": 1 87 | }, 88 | "reservedDomainSubsets": [ 89 | {"~^(reserved\\s+name|blocking|stop-list)$~ui": ""} 90 | ], 91 | "domainSubsets": [ 92 | {"__HEADER__": "domain", "name": "$domain"}, 93 | {"__HEADER__": "domain", "name": "$domainUnicode"}, 94 | {"__HEADER__": "Domain Information"}, 95 | {"~^(complete\\s+)?(domain|domainname|domain[\\s_]+?name|dominio|query)\\b~ui": "~\\b$domain\\b~ui"}, 96 | {"~^(complete\\s+)?(domain|domainname|domain[\\s_]+?name|dominio|query)\\b~ui": "~\\b$domainUnicode\\b~ui"} 97 | ], 98 | "primarySubsets": [ 99 | {"admin-c": "", "tech-c": "", "nserver": "", "status": ""} 100 | ], 101 | "statesSubsets": [ 102 | {"domain status": ""}, 103 | {"registration status": ""} 104 | ], 105 | "secondaryStatesSubsets": [ 106 | {"__HEADER__": "Flags"}, 107 | {"flags": ""} 108 | ], 109 | "nameServersSubsets": [ 110 | {"__HEADER__": "~^((Domain\\s+)?(Name|Dns)\\s*Servers?)(\\s+Information)?(\\s+In\\s+Listed\\s+Order)?$~ui"}, 111 | {"__HEADER__": "~^Domain\\s+INFORMATION$~ui"}, 112 | {"~^(nameserver|ns_name_\\d+|nserver|Name\\s+Server\\s+Handle|Domain\\s+Nameservers|Hostname|dns)$~ui": ""} 113 | ], 114 | "nameServersSparsedSubsets": [ 115 | {"~^(primary|secondary|third|fourth)\\s+server(\\s+hostname)?$~ui": ""} 116 | ], 117 | "ownerSubsets": [ 118 | {"__HEADER__": "~^(Registrant|Owner\\s+Contact|Holder|Organization\\s+Using\\s+Domain\\s+Name|Administrativ\\s+contact|Technical\\s+contact)$~ui"}, 119 | {"__HEADER__": "~^(Tech-C|TITULAR)$~ui", "~^Organi[sz]a[tc]ion$~ui": ""}, 120 | {"Contact": "Registrant", "Organization": ""}, 121 | {"Registrant": ""} 122 | ], 123 | "registrarSubsets": [ 124 | {"__HEADER__": "~^Registrar(\\s+Information)?$~ui"}, 125 | {"__HEADER__": "~^(Zone-C|billing-c)$~ui", "~^Organisation|person$~ui": ""}, 126 | {"__HEADER__": "Billing Contact"}, 127 | {"__HEADER__": "CONTACTO FINANCIERO", "Organizacion": ""}, 128 | {"__HEADER__": "Details", "Registrar": ""}, 129 | {"registrar": ""}, 130 | {"Contact": "Billing", "Organization": ""}, 131 | {"~^(current\\s+)?registr?ar(_name)?(\\s+created)?$~ui": ""} 132 | ], 133 | "registrarReservedSubsets": [ 134 | {"nsset": "", "~^(billing-c|tech-c)$~ui": ""}, 135 | {"~^(billing-c|tech-c)$~ui": ""} 136 | ], 137 | "registrarReservedKeys": [ 138 | "billing-c", 139 | "tech-c" 140 | ], 141 | "contactSubsets": [ 142 | {"~^(nic-hdl(-br)?|contact|norid\\s+handle)$~ui": "$id"} 143 | ], 144 | "contactOrgKeys": [ 145 | "~^(International)?[-_\\s]?(Organi[zs]a[tc]ion|Org)[-_\\s]?(Loc|Name)?$~ui", 146 | "International Name", 147 | "name", 148 | "contact", 149 | "role", 150 | "address", 151 | "person" 152 | ], 153 | "registrarGroupKeys": [ 154 | "~^(Current|Sponsoring)?[-_\\s]?Registr?ar[-_\\s]?(Name|Organization|Handle|Created)?$~ui", 155 | "~^(International)?[-_\\s]?(Organi[zs]a[tc]ion|Org)[-_\\s]?(Loc|Name)?$~ui", 156 | "~^(International\\sName|Name|Address|Person|Billing-c)$~ui" 157 | ] 158 | } -------------------------------------------------------------------------------- /src/Iodev/Whois/Modules/Tld/TldServer.php: -------------------------------------------------------------------------------- 1 | createTldSever($data, $defaultParser); 27 | } 28 | 29 | /** 30 | * @param array $dataList 31 | * @param TldParser $defaultParser 32 | * @return TldServer[] 33 | */ 34 | public static function fromDataList($dataList, ?TldParser $defaultParser = null) 35 | { 36 | return Factory::get()->createTldSevers($dataList, $defaultParser); 37 | } 38 | 39 | /** 40 | * @param string $zone Must starts from '.' 41 | * @param string $host 42 | * @param bool $centralized 43 | * @param TldParser $parser 44 | * @param string $queryFormat 45 | */ 46 | public function __construct($zone, $host, $centralized, ?TldParser $parser, $queryFormat = null, $rdap = false) 47 | { 48 | $this->uid = ++self::$counter; 49 | $this->zone = strval($zone); 50 | if (empty($this->zone)) { 51 | throw new InvalidArgumentException("Zone must be specified"); 52 | } 53 | $this->zone = ($this->zone[0] == '.') ? $this->zone : ".{$this->zone}"; 54 | $this->inverseZoneParts = array_reverse(explode('.', $this->zone)); 55 | array_pop($this->inverseZoneParts); 56 | 57 | $this->host = strval($host); 58 | if (empty($this->host)) { 59 | throw new InvalidArgumentException("Host must be specified"); 60 | } 61 | $this->centralized = (bool)$centralized; 62 | $this->parser = $parser; 63 | $this->queryFormat = !empty($queryFormat) ? strval($queryFormat) : "%s\r\n"; 64 | $this->rdap = (bool)$rdap; 65 | if ($this->rdap) { 66 | $this->parser = Factory::get()->createTldParser(TldParser::RDAP); 67 | } 68 | } 69 | 70 | /** @var string */ 71 | protected $uid; 72 | 73 | /** @var string */ 74 | protected $zone; 75 | 76 | /** @var string[] */ 77 | protected $inverseZoneParts; 78 | 79 | /** @var bool */ 80 | protected $centralized; 81 | 82 | /** @var string */ 83 | protected $host; 84 | 85 | /** @var TldParser */ 86 | protected $parser; 87 | 88 | /** @var string */ 89 | protected $queryFormat; 90 | 91 | /** @var bool */ 92 | protected $rdap; 93 | 94 | /** 95 | * @return string 96 | */ 97 | public function getId() 98 | { 99 | return $this->uid; 100 | } 101 | 102 | /** 103 | * @return bool 104 | */ 105 | public function isCentralized() 106 | { 107 | return (bool)$this->centralized; 108 | } 109 | 110 | /** 111 | * @param string $domain 112 | * @return bool 113 | */ 114 | public function isDomainZone($domain) 115 | { 116 | return $this->matchDomainZone($domain) > 0; 117 | } 118 | 119 | /** 120 | * @param string $domain 121 | * @return int 122 | */ 123 | public function matchDomainZone($domain) 124 | { 125 | $domainParts = explode('.', $domain); 126 | if ($this->zone === '.' && count($domainParts) === 1) { 127 | return 1; 128 | } 129 | array_shift($domainParts); 130 | $domainCount = count($domainParts); 131 | $zoneCount = count($this->inverseZoneParts); 132 | if (count($domainParts) < $zoneCount) { 133 | return 0; 134 | } 135 | $i = -1; 136 | while (++$i < $zoneCount) { 137 | $zonePart = $this->inverseZoneParts[$i]; 138 | $domainPart = $domainParts[$domainCount - $i - 1]; 139 | if ($zonePart != $domainPart && $zonePart != '*') { 140 | return 0; 141 | } 142 | } 143 | return $zoneCount; 144 | } 145 | 146 | /** 147 | * @return string 148 | */ 149 | public function getZone() 150 | { 151 | return $this->zone; 152 | } 153 | 154 | /** 155 | * @return string 156 | */ 157 | public function getHost() 158 | { 159 | return $this->host; 160 | } 161 | 162 | /** 163 | * @return TldParser 164 | */ 165 | public function getParser() 166 | { 167 | return $this->parser; 168 | } 169 | 170 | /** 171 | * @return string 172 | */ 173 | public function getQueryFormat() 174 | { 175 | return $this->queryFormat; 176 | } 177 | 178 | /** 179 | * @return bool 180 | */ 181 | public function isRdap() 182 | { 183 | return (bool)$this->rdap; 184 | } 185 | 186 | /** 187 | * @param string $domain 188 | * @param bool $strict 189 | * @return string 190 | */ 191 | public function buildDomainQuery($domain, $strict = false) 192 | { 193 | $query = sprintf($this->queryFormat, $domain); 194 | return $strict ? "=$query" : $query; 195 | } 196 | 197 | /** 198 | * @param string $domain 199 | * @return array 200 | */ 201 | public function getRdapData($domain) 202 | { 203 | $domain = strtolower(trim($domain)); 204 | if (substr($this->host, -1) !== '/') { 205 | $this->host .= '/'; 206 | } 207 | $url = $this->host . 'domain/' . urlencode($domain); 208 | 209 | $ch = curl_init(); 210 | curl_setopt($ch, CURLOPT_URL, $url); 211 | curl_setopt($ch, CURLOPT_TIMEOUT, 10); 212 | curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); 213 | curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false); 214 | curl_setopt($ch, CURLOPT_USERAGENT, "PHP RDAP Client"); 215 | curl_setopt($ch, CURLOPT_HTTPHEADER, [ 216 | 'Accept: application/rdap+json', 217 | ]); 218 | curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); 219 | $result = curl_exec($ch); 220 | $errstr = curl_error($ch); 221 | $errno = curl_errno($ch); 222 | $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); 223 | curl_close($ch); 224 | 225 | if ($errno) { 226 | throw new ConnectionException($errstr, $errno); 227 | } 228 | 229 | return [$httpCode, $result]; 230 | } 231 | } 232 | -------------------------------------------------------------------------------- /src/Iodev/Whois/Modules/Tld/Parsers/CommonParser.php: -------------------------------------------------------------------------------- 1 | 1 ]; 59 | 60 | /** @var string */ 61 | protected $emptyValuesDict = [ 62 | "" => 1, 63 | "not.defined." => 1, 64 | ]; 65 | 66 | /** 67 | * @return string 68 | */ 69 | public function getType() 70 | { 71 | return $this->isFlat ? TldParser::COMMON_FLAT : TldParser::COMMON; 72 | } 73 | 74 | /** 75 | * @param array $cfg 76 | * @return $this 77 | */ 78 | public function setConfig($cfg) 79 | { 80 | foreach ($cfg as $k => $v) { 81 | $this->{$k} = $v; 82 | } 83 | return $this; 84 | } 85 | 86 | /** 87 | * @param TldResponse $response 88 | * @return TldInfo 89 | */ 90 | public function parseResponse(TldResponse $response) 91 | { 92 | $rootFilter = $this->filterFrom($response); 93 | $sel = $rootFilter->toSelector(); 94 | $data = [ 95 | "parserType" => $this->getType(), 96 | 97 | "domainName" => $sel->clean() 98 | ->selectKeys($this->domainKeys) 99 | ->mapDomain() 100 | ->removeEmpty() 101 | ->getFirst(''), 102 | 103 | "whoisServer" => $sel->clean() 104 | ->selectKeys($this->whoisServerKeys) 105 | ->mapAsciiServer() 106 | ->removeEmpty() 107 | ->getFirst(''), 108 | 109 | "nameServers" => $sel->clean() 110 | ->selectKeys($this->nameServersKeys) 111 | ->selectKeyGroups($this->nameServersKeysGroups) 112 | ->mapAsciiServer() 113 | ->removeEmpty() 114 | ->removeDuplicates(11) 115 | ->getAll(), 116 | 117 | "dnssec" => $sel->clean() 118 | ->selectKeys($this->dnssecKeys) 119 | ->removeEmpty() 120 | ->sort(SORT_ASC) 121 | ->getFirst(''), 122 | 123 | "creationDate" => $sel->clean() 124 | ->selectKeys($this->creationDateKeys) 125 | ->mapUnixTime($this->getOption('inversedDateMMDD', false)) 126 | ->getFirst(''), 127 | 128 | "expirationDate" => $sel->clean() 129 | ->selectKeys($this->expirationDateKeys) 130 | ->mapUnixTime($this->getOption('inversedDateMMDD', false)) 131 | ->getFirst(''), 132 | 133 | "updatedDate" => $sel->clean() 134 | ->selectKeys($this->updatedDateKeys) 135 | ->mapUnixTime($this->getOption('inversedDateMMDD', false)) 136 | ->getFirst(''), 137 | 138 | "owner" => $sel->clean() 139 | ->selectKeys($this->ownerKeys) 140 | ->getFirst(''), 141 | 142 | "registrar" => $sel->clean() 143 | ->selectKeys($this->registrarKeys) 144 | ->getFirst(''), 145 | 146 | "states" => $sel->clean() 147 | ->selectKeys($this->statesKeys) 148 | ->mapStates() 149 | ->removeEmpty() 150 | ->removeDuplicates() 151 | ->getAll(), 152 | ]; 153 | $info = $this->createDomainInfo($response, $data, [ 154 | 'groups' => $rootFilter->getGroups(), 155 | 'rootFilter' => $rootFilter, 156 | ]); 157 | return $info->isValuable($this->notRegisteredStatesDict) ? $info : null; 158 | } 159 | 160 | /** 161 | * @param TldResponse $response 162 | * @param array $data 163 | * @param array $options 164 | * @return TldInfo 165 | */ 166 | protected function createDomainInfo(TldResponse $response, array $data, $options = []) 167 | { 168 | return new TldInfo($response, $data, $options); 169 | } 170 | 171 | /** 172 | * @return GroupFilter 173 | */ 174 | protected function createGroupFilter(): GroupFilter 175 | { 176 | return new GroupFilter(); 177 | } 178 | 179 | /** 180 | * @param TldResponse $response 181 | * @return GroupFilter 182 | */ 183 | protected function filterFrom(TldResponse $response) 184 | { 185 | $groups = $this->groupsFromText($response->text); 186 | $filter = $this->createGroupFilter() 187 | ->setGroups($groups) 188 | ->useIgnoreCase(true) 189 | ->useMatchFirstOnly(true) 190 | ->handleEmpty($this->emptyValuesDict); 191 | 192 | if ($this->isFlat) { 193 | return $filter->mergeGroups(); 194 | } 195 | return $filter->filterIsDomain($response->domain, $this->domainKeys) 196 | ->useFirstGroup(); 197 | } 198 | 199 | /** 200 | * @param string $text 201 | * @return array 202 | */ 203 | protected function groupsFromText($text) 204 | { 205 | $lines = ParserHelper::splitLines($text); 206 | return ParserHelper::linesToGroups($lines, $this->headerKey); 207 | } 208 | } 209 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PHP WHOIS 2 | 3 | [![PHP version](https://img.shields.io/badge/php-%3E%3D7.2-8892BF.svg)](https://secure.php.net/) 4 | [![Packagist](https://img.shields.io/packagist/v/cccyun/php-whois.svg)](https://packagist.org/packages/cccyun/php-whois) 5 | 6 | PHP WHOIS client implementation. Sends the queries directly to the WHOIS services. 7 | 8 | ## Use case 9 | * Raw and parsed domain lookup 10 | * Raw and parsed ASN routes lookup 11 | * Direct queries to TLD/ASN hosts 12 | * Extending and customizing the default hosts, parsers, etc. 13 | * Proxying via CurlLoader 14 | 15 | ## Installation 16 | 17 | ##### System requirements: 18 | * PHP >= __7.2__ (old versions supports __5.4+__) 19 | * php-curl 20 | * php-mbstring 21 | * Open port __43__ in firewall 22 | 23 | Optional: 24 | * php-intl 25 | * php-memcached + Memcached server 26 | 27 | ##### Project requirements: 28 | * PSR-4 autoloader 29 | 30 | ##### Composer: 31 | ```` 32 | composer require cccyun/php-whois 33 | ```` 34 | or composer.json: 35 | ```` 36 | "require": { 37 | "cccyun/php-whois": "^4.0" 38 | } 39 | ```` 40 | 41 | 42 | ## Usage 43 | 44 | ### Domain lookup 45 | 46 | ##### How to get summary about domain: 47 | ```php 48 | createWhois(); 54 | 55 | // Checking availability 56 | if ($whois->isDomainAvailable("google.com")) { 57 | print "Bingo! Domain is available! :)"; 58 | } 59 | 60 | // Supports Unicode (converts to punycode) 61 | if ($whois->isDomainAvailable("почта.рф")) { 62 | print "Bingo! Domain is available! :)"; 63 | } 64 | 65 | // Getting raw-text lookup 66 | $response = $whois->lookupDomain("google.com"); 67 | print $response->text; 68 | 69 | // Getting parsed domain info 70 | $info = $whois->loadDomainInfo("google.com"); 71 | print_r([ 72 | 'Domain created' => date("Y-m-d", $info->creationDate), 73 | 'Domain expires' => date("Y-m-d", $info->expirationDate), 74 | 'Domain owner' => $info->owner, 75 | ]); 76 | 77 | ``` 78 | 79 | ##### Exceptions on domain lookup: 80 | ```php 81 | createWhois(); 90 | $info = $whois->loadDomainInfo("google.com"); 91 | if (!$info) { 92 | print "Null if domain available"; 93 | exit; 94 | } 95 | print $info->domainName . " expires at: " . date("d.m.Y H:i:s", $info->expirationDate); 96 | } catch (ConnectionException $e) { 97 | print "Disconnect or connection timeout"; 98 | } catch (ServerMismatchException $e) { 99 | print "TLD server (.com for google.com) not found in current server hosts"; 100 | } catch (WhoisException $e) { 101 | print "Whois server responded with error '{$e->getMessage()}'"; 102 | } 103 | ``` 104 | 105 | ##### Proxy with SOCKS5: 106 | ```php 107 | replaceOptions([ 114 | CURLOPT_PROXYTYPE => CURLPROXY_SOCKS5, 115 | CURLOPT_PROXY => "127.0.0.1:1080", 116 | //CURLOPT_PROXYUSERPWD => "user:pass", 117 | ]); 118 | $whois = Factory::get()->createWhois($loader); 119 | 120 | var_dump([ 121 | 'ya.ru' => $whois->loadDomainInfo('ya.ru'), 122 | 'google.de' => $whois->loadDomainInfo('google.de'), 123 | ]); 124 | ``` 125 | 126 | ##### TLD hosts customization: 127 | ```php 128 | createWhois(); 134 | 135 | // Define custom whois host 136 | $customServer = new TldServer(".custom", "whois.nic.custom", false, Factory::get()->createTldParser()); 137 | 138 | // Or define the same via assoc way 139 | $customServer = TldServer::fromData([ 140 | "zone" => ".custom", 141 | "host" => "whois.nic.custom", 142 | ]); 143 | 144 | // Add custom server to existing whois instance 145 | $whois->getTldModule()->addServers([$customServer]); 146 | 147 | // Now it can be utilized 148 | $info = $whois->loadDomainInfo("google.custom"); 149 | 150 | var_dump($info); 151 | ``` 152 | 153 | ##### TLD default/fallback servers: 154 | ```php 155 | createWhois(); 161 | 162 | // Add default servers 163 | $matchedServers = $whois->getTldModule() 164 | ->addServers(TldServer::fromDataList([ 165 | ['zone' => '.*.net', 'host' => 'localhost'], 166 | ['zone' => '.uk.*', 'host' => 'localhost'], 167 | ['zone' => '.*', 'host' => 'localhost'], 168 | ])) 169 | ->matchServers('some.uk.net'); 170 | 171 | foreach ($matchedServers as $s) { 172 | echo "{$s->getZone()} {$s->getHost()}\n"; 173 | } 174 | 175 | // Matched servers + custom defaults: 176 | // 177 | // .uk.net whois.centralnic.com 178 | // .uk.net whois.centralnic.net 179 | // .uk.* localhost 180 | // .*.net localhost 181 | // .net whois.crsnic.net 182 | // .net whois.verisign-grs.com 183 | // .* localhost 184 | ``` 185 | 186 | ### ASN lookup 187 | 188 | ##### How to get summary using ASN number: 189 | ```php 190 | createWhois(); 195 | 196 | // Getting raw-text lookup 197 | $response = $whois->lookupAsn("AS32934"); 198 | print $response->text; 199 | 200 | // Getting parsed ASN info 201 | $info = $whois->loadAsnInfo("AS32934"); 202 | foreach ($info->routes as $route) { 203 | print_r([ 204 | 'route IPv4' => $route->route, 205 | 'route IPv6' => $route->route6, 206 | 'description' => $route->descr, 207 | ]); 208 | } 209 | 210 | ``` 211 | 212 | ### Response caching 213 | Some TLD hosts are very limited for frequent requests. Use cache if in your case requests are repeating. 214 | ```php 215 | addServer('127.0.0.1', 11211); 223 | $loader = new MemcachedLoader(new SocketLoader(), $m); 224 | 225 | $whois = Factory::get()->createWhois($loader); 226 | // do something... 227 | ``` 228 | 229 | 230 | ## Develompment 231 | Supported php versions are configured in `docker-compose.yml` 232 | 233 | Common use cases: 234 | 1. Set up & run all tests: `docker compose up --build` 235 | 2. Run tests under specific php version: `docker compose up php-8.2_intl --build` 236 | 3. Run scripts: `docker compose run php-8.2_intl bin/php-whois info google.com` 237 | 238 | Also see **TESTS.md** 239 | 240 | 241 | ## Contributing 242 | 243 | The project is open for pull requests, issues and feedback. Please read the CODE_OF_CONDUCT.md 244 | -------------------------------------------------------------------------------- /src/Iodev/Whois/Modules/Tld/TldModule.php: -------------------------------------------------------------------------------- 1 | servers; 37 | } 38 | 39 | /** 40 | * @return TldServer[] 41 | */ 42 | public function getLastUsedServers() 43 | { 44 | return $this->lastUsedServers; 45 | } 46 | 47 | /** 48 | * @param TldServer[] $servers 49 | * @return $this 50 | */ 51 | public function addServers($servers) 52 | { 53 | return $this->setServers(array_merge($this->servers, $servers)); 54 | } 55 | 56 | /** 57 | * @param TldServer[] $servers 58 | * @return $this 59 | */ 60 | public function setServers($servers) 61 | { 62 | $weightMap = []; 63 | foreach ($servers as $index => $server) { 64 | $parts = explode('.', $server->getZone()); 65 | $rootZone = array_pop($parts); 66 | $subZone1 = $parts ? array_pop($parts) : ''; 67 | $subZone2 = $parts ? array_pop($parts) : ''; 68 | $weightMap[$server->getId()] = sprintf('%16s.%16s.%32s.%13s', $subZone2, $subZone1, $rootZone, 1000000 - $index); 69 | }; 70 | usort($servers, function(TldServer $a, TldServer $b) use ($weightMap) { 71 | return strcmp($weightMap[$b->getId()], $weightMap[$a->getId()]); 72 | }); 73 | $this->servers = $servers; 74 | return $this; 75 | } 76 | 77 | /** 78 | * @param string $domain 79 | * @param bool $quiet 80 | * @return TldServer[] 81 | * @throws ServerMismatchException 82 | */ 83 | public function matchServers($domain, $quiet = false) 84 | { 85 | $servers = []; 86 | foreach ($this->servers as $server) { 87 | $matchedCount = $server->matchDomainZone($domain); 88 | if ($matchedCount) { 89 | $servers[] = $server; 90 | } 91 | } 92 | if (!$quiet && empty($servers)) { 93 | throw new ServerMismatchException("No servers matched for domain '$domain'"); 94 | } 95 | return $servers; 96 | } 97 | 98 | /** 99 | * @param string $domain 100 | * @return bool 101 | * @throws ServerMismatchException 102 | * @throws ConnectionException 103 | * @throws WhoisException 104 | */ 105 | public function isDomainAvailable($domain) 106 | { 107 | return !$this->loadDomainInfo($domain); 108 | } 109 | 110 | /** 111 | * @param string $domain 112 | * @param TldServer $server 113 | * @return TldResponse 114 | * @throws ServerMismatchException 115 | * @throws ConnectionException 116 | * @throws WhoisException 117 | */ 118 | public function lookupDomain($domain, ?TldServer $server = null) 119 | { 120 | $domain = DomainHelper::toAscii($domain); 121 | $servers = $server ? [$server] : $this->matchServers($domain); 122 | list ($response) = $this->loadDomainData($domain, $servers); 123 | return $response; 124 | } 125 | 126 | /** 127 | * @param string $domain 128 | * @param TldServer $server 129 | * @return TldInfo 130 | * @throws ServerMismatchException 131 | * @throws ConnectionException 132 | * @throws WhoisException 133 | */ 134 | public function loadDomainInfo($domain, ?TldServer $server = null) 135 | { 136 | $domain = DomainHelper::toAscii($domain); 137 | $servers = $server ? [$server] : $this->matchServers($domain); 138 | list (, $info) = $this->loadDomainData($domain, $servers); 139 | return $info; 140 | } 141 | 142 | /** 143 | * @param TldServer $server 144 | * @param string $domain 145 | * @param bool $strict 146 | * @param string $host 147 | * @return TldResponse 148 | * @throws ConnectionException 149 | * @throws WhoisException 150 | */ 151 | public function loadResponse(TldServer $server, $domain, $strict = false, $host = null) 152 | { 153 | $host = $host ?: $server->getHost(); 154 | if ($server->isRdap()) { 155 | $result = $server->getRdapData($domain); 156 | return new TldResponse([ 157 | 'domain' => $domain, 158 | 'host' => $host, 159 | 'text' => $result[1], 160 | 'httpCode' => $result[0], 161 | ]); 162 | } else { 163 | $query = $server->buildDomainQuery($domain, $strict); 164 | return new TldResponse([ 165 | 'domain' => $domain, 166 | 'host' => $host, 167 | 'query' => $query, 168 | 'text' => $this->getLoader()->loadText($host, $query), 169 | ]); 170 | } 171 | } 172 | 173 | /** 174 | * @param string $domain 175 | * @param TldServer[] $servers 176 | * @return array 177 | * @throws ConnectionException 178 | * @throws WhoisException 179 | */ 180 | public function loadDomainData(string $domain, array $servers): array 181 | { 182 | $this->lastUsedServers = []; 183 | $response = null; 184 | $info = null; 185 | $lastError = null; 186 | foreach ($servers as $server) { 187 | $this->lastUsedServers[] = $server; 188 | $this->loadParsedTo($response, $info, $server, $domain, false, null, $lastError); 189 | if ($info) { 190 | break; 191 | } 192 | } 193 | if (!$response && !$info) { 194 | throw $lastError ? $lastError : new WhoisException("No response"); 195 | } 196 | return [$response, $info]; 197 | } 198 | 199 | /** 200 | * @param $outResponse 201 | * @param TldInfo $outInfo 202 | * @param TldServer $server 203 | * @param $domain 204 | * @param $strict 205 | * @param $host 206 | * @param $lastError 207 | * @throws ConnectionException 208 | * @throws WhoisException 209 | */ 210 | protected function loadParsedTo(&$outResponse, &$outInfo, $server, $domain, $strict = false, $host = null, &$lastError = null) 211 | { 212 | try { 213 | $outResponse = $this->loadResponse($server, $domain, $strict, $host); 214 | $outInfo = $server->getParser()->parseResponse($outResponse); 215 | } catch (ConnectionException $e) { 216 | $lastError = $lastError ?: $e; 217 | } 218 | if (!$outInfo && $lastError && $host == $server->getHost() && $strict) { 219 | throw $lastError; 220 | } 221 | if (!$strict && !$outInfo) { 222 | $this->loadParsedTo($tmpResponse, $tmpInfo, $server, $domain, true, $host, $lastError); 223 | $outResponse = $tmpInfo ? $tmpResponse : $outResponse; 224 | $outInfo = $tmpInfo ?: $outInfo; 225 | } 226 | if (!$outInfo || $host == $outInfo->whoisServer) { 227 | return; 228 | } 229 | $host = $outInfo->whoisServer; 230 | if ($host && $host != $server->getHost() && !$server->isCentralized()) { 231 | $this->loadParsedTo($tmpResponse, $tmpInfo, $server, $domain, false, $host, $lastError); 232 | $outResponse = $tmpInfo ? $tmpResponse : $outResponse; 233 | $outInfo = $tmpInfo ?: $outInfo; 234 | } 235 | } 236 | } 237 | -------------------------------------------------------------------------------- /src/Iodev/Whois/Factory.php: -------------------------------------------------------------------------------- 1 | createLoader()); 42 | $whois->setFactory($this); 43 | return $whois; 44 | } 45 | 46 | /** 47 | * @return ILoader 48 | */ 49 | public function createLoader(): ILoader 50 | { 51 | return new SocketLoader(); 52 | } 53 | 54 | /** 55 | * @param Whois $ehois 56 | * @return AsnModule 57 | */ 58 | public function createAsnModule(Whois $ehois): AsnModule 59 | { 60 | $m = new AsnModule($ehois->getLoader()); 61 | $m->setServers($this->createAsnSevers()); 62 | return $m; 63 | } 64 | 65 | /** 66 | * @param Whois $ehois 67 | * @return TldModule 68 | */ 69 | public function createTldModule(Whois $ehois): TldModule 70 | { 71 | $m = new TldModule($ehois->getLoader()); 72 | $m->setServers($this->createTldSevers()); 73 | return $m; 74 | } 75 | 76 | /** 77 | * @param array|null $configs 78 | * @param TldParser|null $defaultParser 79 | * @return TldServer[] 80 | */ 81 | public function createTldSevers($configs = null, ?TldParser $defaultParser = null): array 82 | { 83 | $configs = is_array($configs) ? $configs : Config::load("module.tld.servers"); 84 | $defaultParser = $defaultParser ?: $this->createTldParser(); 85 | $servers = []; 86 | foreach ($configs as $config) { 87 | $servers[] = $this->createTldSever($config, $defaultParser); 88 | } 89 | return $servers; 90 | } 91 | 92 | /** 93 | * @param array $config 94 | * @param TldParser|null $defaultParser 95 | * @return TldServer 96 | */ 97 | public function createTldSever(array $config, ?TldParser $defaultParser = null): TldServer 98 | { 99 | return new TldServer( 100 | $config['zone'] ?? '', 101 | $config['host'] ?? '', 102 | !empty($config['centralized']), 103 | $this->createTldSeverParser($config, $defaultParser), 104 | $config['queryFormat'] ?? null, 105 | $config['rdap'] ?? false 106 | ); 107 | } 108 | 109 | /** 110 | * @param array $config 111 | * @param TldParser|null $defaultParser 112 | * @return TldParser 113 | */ 114 | public function createTldSeverParser(array $config, ?TldParser $defaultParser = null): TldParser 115 | { 116 | $options = $config['parserOptions'] ?? []; 117 | if (isset($config['parserClass'])) { 118 | return $this->createTldParserByClass( 119 | $config['parserClass'], 120 | $config['parserType'] ?? null 121 | )->setOptions($options); 122 | } 123 | if (isset($config['parserType'])) { 124 | return $this->createTldParser($config['parserType'])->setOptions($options); 125 | } 126 | return $defaultParser ?: $this->createTldParser()->setOptions($options); 127 | } 128 | 129 | /** 130 | * @param string $type 131 | * @return TldParser 132 | */ 133 | public function createTldParser($type = null) 134 | { 135 | $type = $type ? $type : TldParser::AUTO; 136 | $d = [ 137 | TldParser::AUTO => AutoParser::class, 138 | TldParser::COMMON => CommonParser::class, 139 | TldParser::COMMON_FLAT => CommonParser::class, 140 | TldParser::BLOCK => BlockParser::class, 141 | TldParser::INDENT => IndentParser::class, 142 | TldParser::INDENT_AUTOFIX => IndentParser::class, 143 | TldParser::RDAP => RdapParser::class, 144 | ]; 145 | return $this->createTldParserByClass($d[$type], $type); 146 | } 147 | 148 | /** 149 | * @param string $className 150 | * @param string $configType 151 | * @return TldParser 152 | */ 153 | public function createTldParserByClass($className, $configType = null) 154 | { 155 | $configType = empty($configType) ? TldParser::AUTO : $configType; 156 | $config = $this->getTldParserConfigByType($configType); 157 | 158 | /* @var $parser TldParser */ 159 | $parser = new $className(); 160 | $parser->setConfig($config); 161 | if ($parser->getType() == TldParser::AUTO) { 162 | $this->setupTldAutoParser($parser, $config); 163 | } 164 | 165 | return $parser; 166 | } 167 | 168 | /** 169 | * @param AutoParser $parser 170 | * @param array $config 171 | */ 172 | protected function setupTldAutoParser(AutoParser $parser, $config = []) 173 | { 174 | /* @var $autoParser AutoParser */ 175 | foreach ($config['parserTypes'] ?? [] as $type) { 176 | $parser->addParser($this->createTldParser($type)); 177 | } 178 | } 179 | 180 | /** 181 | * @param string $type 182 | * @return array 183 | */ 184 | public function getTldParserConfigByType($type) 185 | { 186 | if ($type == TldParser::COMMON_FLAT) { 187 | $type = TldParser::COMMON; 188 | $extra = ['isFlat' => true]; 189 | } 190 | if ($type == TldParser::INDENT_AUTOFIX) { 191 | $type = TldParser::INDENT; 192 | $extra = ['isAutofix' => true]; 193 | } 194 | $config = Config::load("module.tld.parser.$type"); 195 | return empty($extra) ? $config : array_merge($config, $extra); 196 | } 197 | 198 | /** 199 | * @param array $configs|null 200 | * @param AsnParser $defaultParser 201 | * @return AsnServer[] 202 | */ 203 | public function createAsnSevers($configs = null, ?AsnParser $defaultParser = null): array 204 | { 205 | $configs = is_array($configs) ? $configs : Config::load("module.asn.servers"); 206 | $defaultParser = $defaultParser ?: $this->createAsnParser(); 207 | $servers = []; 208 | foreach ($configs as $config) { 209 | $servers[] = $this->createAsnSever($config, $defaultParser); 210 | } 211 | return $servers; 212 | } 213 | 214 | /** 215 | * @param array $config 216 | * @param AsnParser $defaultParser 217 | * @return AsnServer 218 | */ 219 | public function createAsnSever($config, ?AsnParser $defaultParser = null) 220 | { 221 | return new AsnServer( 222 | $config['host'] ?? '', 223 | $this->createAsnSeverParser($config, $defaultParser), 224 | $config['queryFormat'] ?? null 225 | ); 226 | } 227 | 228 | /** 229 | * @param array $config 230 | * @param AsnParser|null $defaultParser 231 | * @return AsnParser 232 | */ 233 | public function createAsnSeverParser(array $config, ?AsnParser $defaultParser = null): AsnParser 234 | { 235 | if (isset($config['parserClass'])) { 236 | return $this->createAsnParserByClass($config['parserClass']); 237 | } 238 | return $defaultParser ?: $this->createAsnParser(); 239 | } 240 | 241 | /** 242 | * @return AsnParser 243 | */ 244 | public function createAsnParser(): AsnParser 245 | { 246 | return new AsnParser(); 247 | } 248 | 249 | /** 250 | * @param string $className 251 | * @return AsnParser 252 | */ 253 | public function createAsnParserByClass($className): AsnParser 254 | { 255 | return new $className(); 256 | } 257 | 258 | } 259 | -------------------------------------------------------------------------------- /src/Iodev/Whois/Helpers/GroupHelper.php: -------------------------------------------------------------------------------- 1 | 1 && $needle[0] === '~') { 21 | return (bool)preg_match($needle, $subject); 22 | } 23 | return $ignoreCase && mb_strtolower($needle) === mb_strtolower($subject); 24 | }; 25 | } 26 | 27 | /** 28 | * @param array $group 29 | * @param bool $keysOnly 30 | * @return array 31 | */ 32 | public static function toLowerCase($group, $keysOnly = false) 33 | { 34 | return $keysOnly 35 | ? self::mapRecursiveKeys($group, 'mb_strtolower') 36 | : self::mapRecursive($group, 'mb_strtolower'); 37 | } 38 | 39 | /** 40 | * @param array $group 41 | * @param callable $callback 42 | * @return array 43 | */ 44 | public static function mapRecursive($group, $callback) { 45 | $out = []; 46 | array_walk($group, function($val, $key) use (&$out, $callback) { 47 | $out[$callback($key)] = is_array($val) ? self::mapRecursive($val, $callback) : $callback($val); 48 | }); 49 | return $out; 50 | } 51 | 52 | /** 53 | * @param array $group 54 | * @param callable $callback 55 | * @return array 56 | */ 57 | public static function mapRecursiveKeys($group, $callback) { 58 | $out = []; 59 | array_walk($group, function($val, $key) use (&$out, $callback) { 60 | $out[$callback($key)] = is_array($val) ? self::mapRecursiveKeys($val, $callback) : $val; 61 | }); 62 | return $out; 63 | } 64 | 65 | /** 66 | * @param array $group 67 | * @param string[] $keys 68 | * @param bool $firstOnly 69 | * @param callable $matcher 70 | * @return string[] 71 | */ 72 | public static function matchKeys($group, $keys, $firstOnly = false, $matcher = null) 73 | { 74 | if (empty($group)) { 75 | return []; 76 | } 77 | $matcher = is_callable($matcher) ? $matcher : self::getMatcher(); 78 | $matches = []; 79 | foreach ($keys as $key) { 80 | if (is_array($key)) { 81 | self::matchSubKeys($group, $key, $matches); 82 | } elseif (isset($group[$key])) { 83 | is_array($group[$key]) ? $matches = array_merge($matches, $group[$key]) : $matches[] = $group[$key]; 84 | } else { 85 | foreach ($group as $groupKey => $groupVal) { 86 | if ($matcher($key, $groupKey)) { 87 | is_array($groupVal) ? $matches = array_merge($matches, $groupVal) : $matches[] = $groupVal; 88 | if ($firstOnly) { 89 | break; 90 | } 91 | } 92 | } 93 | } 94 | if ($firstOnly && count($matches) > 0) { 95 | break; 96 | } 97 | } 98 | return $matches; 99 | } 100 | 101 | /** 102 | * @param array $group 103 | * @param string[] $keys 104 | * @param array $outMatches 105 | * @param callable $matcher 106 | */ 107 | private static function matchSubKeys($group, $keys, &$outMatches = [], $matcher = null) 108 | { 109 | $vals = []; 110 | foreach ($keys as $k) { 111 | $v = self::matchKeys($group, [$k], true, $matcher); 112 | $v = empty($v) ? "" : reset($v); 113 | if (is_array($v)) { 114 | $vals = array_merge($vals, $v); 115 | } elseif (!empty($v)) { 116 | $vals[] = $v; 117 | } 118 | } 119 | if (count($vals) > 1) { 120 | $outMatches[] = $vals; 121 | } elseif (count($vals) == 1) { 122 | $outMatches[] = $vals[0]; 123 | } else { 124 | $outMatches[] = ""; 125 | } 126 | } 127 | 128 | /** 129 | * @param array $subsets 130 | * @param array $params 131 | * @return array 132 | */ 133 | public static function renderSubsets($subsets, $params) 134 | { 135 | array_walk_recursive($subsets, function(&$val) use ($params) { 136 | $origVal = (string)$val; 137 | $val = preg_replace_callback('~\\$[a-z][a-z\d]*\b~ui', function($m) use ($origVal, $params) { 138 | $arg = $m[0]; 139 | $newVal = isset($params[$arg]) ? $params[$arg] : $arg; 140 | if (strlen($origVal) > 1 && $origVal[0] == '~') { 141 | $newVal = preg_quote($newVal); 142 | } 143 | return $newVal; 144 | }, $val); 145 | }); 146 | return $subsets; 147 | } 148 | 149 | /** 150 | * @param array $groups 151 | * @param array $subsets 152 | * @param bool $ignoreCase 153 | * @param bool $stopOnFirst 154 | * @return array 155 | */ 156 | public static function findGroupsHasSubsetOf($groups, $subsets, $ignoreCase = true, $stopOnFirst = false) 157 | { 158 | $keyMatcher = self::getMatcher($ignoreCase); 159 | $valMatcher = self::getMatcher($ignoreCase); 160 | $foundGroups = []; 161 | foreach ($subsets as $subset) { 162 | foreach ($groups as $group) { 163 | if (self::matchGroupSubset($group, $subset, $keyMatcher, $valMatcher)) { 164 | $foundGroups[] = $group; 165 | if ($stopOnFirst) { 166 | break; 167 | } 168 | } 169 | } 170 | } 171 | return $foundGroups; 172 | } 173 | 174 | /** 175 | * @param array $group 176 | * @param array $subset 177 | * @param callable $keyMatcher 178 | * @param callable $valMatcher 179 | * @return bool 180 | */ 181 | public static function matchGroupSubset($group, $subset, $keyMatcher = null, $valMatcher = null) 182 | { 183 | $keyMatcher = is_callable($keyMatcher) ? $keyMatcher : self::getMatcher(); 184 | $valMatcher = is_callable($valMatcher) ? $valMatcher : self::getMatcher(); 185 | foreach ($subset as $subsetKey => $subsetVal) { 186 | $isKeyMatched = false; 187 | $groupVal = null; 188 | if (isset($group[$subsetKey])) { 189 | $isKeyMatched = true; 190 | $groupVal = $group[$subsetKey]; 191 | } else { 192 | foreach ($group as $groupKey => $gv) { 193 | $isKeyMatched = $keyMatcher($subsetKey, $groupKey); 194 | if ($isKeyMatched) { 195 | $groupVal = $gv; 196 | break; 197 | } 198 | } 199 | } 200 | if (!$isKeyMatched) { 201 | return false; 202 | } 203 | if (empty($subsetVal)) { 204 | continue; 205 | } 206 | if (is_array($groupVal)) { 207 | foreach ($groupVal as $groupSubVal) { 208 | if ($valMatcher($subsetVal, $groupSubVal)) { 209 | $found = true; 210 | break; 211 | } 212 | } 213 | } else { 214 | $found = $valMatcher($subsetVal, $groupVal); 215 | } 216 | if (empty($found)) { 217 | return false; 218 | } 219 | } 220 | return true; 221 | } 222 | 223 | /** 224 | * @param array $groups 225 | * @param string $domain 226 | * @param string[] $domainKeys 227 | * @param bool $stopOnFirst 228 | * @return array 229 | */ 230 | public static function findDomainGroups($groups, $domain, $domainKeys, $stopOnFirst = false) 231 | { 232 | $foundGroups = []; 233 | foreach ($groups as $group) { 234 | $foundDomain = null; 235 | foreach (self::matchKeys($group, $domainKeys, true) as $val) { 236 | $foundDomain = DomainHelper::toAscii($val); 237 | if (!empty($foundDomain)) { 238 | break; 239 | } 240 | } 241 | if ($foundDomain && DomainHelper::compareNames($foundDomain, $domain)) { 242 | $foundGroups[] = $group; 243 | if ($stopOnFirst) { 244 | break; 245 | } 246 | } 247 | } 248 | return $foundGroups; 249 | } 250 | } 251 | -------------------------------------------------------------------------------- /src/Iodev/Whois/Helpers/ParserHelper.php: -------------------------------------------------------------------------------- 1 | ltrim($kv[1], ".")]); 38 | continue; 39 | } 40 | if (empty($group[$header]) && count($group) > 0) { 41 | $group[$header] = self::linesToBestHeader($headerLines); 42 | } 43 | if (count($group) > 1) { 44 | $groups[] = array_filter($group); 45 | $group = []; 46 | $headerLines = [$headerLine]; 47 | } 48 | } 49 | return $groups; 50 | } 51 | 52 | /** 53 | * @param string $line 54 | * @param string $trimChars 55 | * @return string[] 56 | */ 57 | public static function lineToKeyVal($line, $trimChars = " \t\n\r\0\x0B") 58 | { 59 | if (preg_match('~^\s*(\.{2,})?\s*(.+?)\s*(\.{2,})?\s*:(?![\\/:])(? 0) { 79 | $empty = 0; 80 | $map[$line] = mb_strlen($line) + count(preg_split('~\s+~ui', $line)); 81 | } 82 | } 83 | $header = ''; 84 | if (!empty($map)) { 85 | asort($map, SORT_NUMERIC); 86 | $header = key($map); 87 | } 88 | return $header; 89 | } 90 | 91 | /** 92 | * @param string[] $lines 93 | * @param callable $validateStoplineFn 94 | * @return array 95 | */ 96 | public static function linesToSpacedBlocks($lines, $validateStoplineFn = null) 97 | { 98 | $lines[] = ''; 99 | $blocks = []; 100 | $block = []; 101 | foreach ($lines as $line) { 102 | $tline = trim($line); 103 | if (!empty($tline) && empty($block) && is_callable($validateStoplineFn) && !$validateStoplineFn($line)) { 104 | break; 105 | } elseif (!empty($tline)) { 106 | $block[] = $line; 107 | } elseif (!empty($block)) { 108 | $blocks[] = $block; 109 | $block = []; 110 | } 111 | } 112 | return $blocks; 113 | } 114 | 115 | /** 116 | * @param array $block 117 | * @param callable $biasIndentFn 118 | * @param int $maxDepth 119 | * @return array 120 | */ 121 | public static function blockToIndentedNodes($block, $biasIndentFn = null, $maxDepth = 10) 122 | { 123 | $nodes = []; 124 | $node = []; 125 | $nodePad = 999999; 126 | foreach ($block as $line) { 127 | $pad = self::calcIndent($line, $biasIndentFn); 128 | if ($pad <= $nodePad) { 129 | $nodePad = $pad; 130 | $nodes[] = [ 131 | 'line' => $line, 132 | 'children' => [], 133 | ]; 134 | $node = &$nodes[count($nodes) - 1]; 135 | } else { 136 | $node['children'][] = $line; 137 | } 138 | } 139 | unset($node); 140 | foreach ($nodes as &$node) { 141 | if (!empty($node['children']) && $maxDepth > 1) { 142 | $node['children'] = self::blockToIndentedNodes($node['children'], $maxDepth - 1); 143 | } 144 | if (empty($node['children'])) { 145 | $node = $node['line']; 146 | } 147 | } 148 | return $nodes; 149 | } 150 | 151 | /** 152 | * @param string $line 153 | * @param callable $biasFn 154 | * @return int 155 | */ 156 | public static function calcIndent($line, $biasFn = null) 157 | { 158 | $pad = strlen($line) - strlen(ltrim($line)); 159 | if (is_callable($biasFn)) { 160 | $pad += $biasFn($line); 161 | } 162 | return $pad; 163 | } 164 | 165 | /** 166 | * @param array $nodes 167 | * @param int $maxKeyLength 168 | * @return array 169 | */ 170 | public static function nodesToDict($nodes, $maxKeyLength = 32) 171 | { 172 | $dict = []; 173 | foreach ($nodes as $node) { 174 | $node = is_array($node) ? $node : ['line' => $node, 'children' => []]; 175 | $k = ''; 176 | $v = ''; 177 | $kv = self::lineToKeyVal($node['line']); 178 | if (count($kv) == 2) { 179 | list ($k, $v) = $kv; 180 | if (empty($v)) { 181 | $v = self::nodesToDict($node['children']); 182 | } elseif (strlen($k) <= $maxKeyLength) { 183 | $v = trim($v) ? [trim($v)] : []; 184 | foreach ($node['children'] as $child) { 185 | if (is_array($child)) { 186 | $childV = self::nodesToDict([$child]); 187 | if (!empty($childV)) { 188 | $dict = array_merge_recursive($dict, $childV); 189 | } 190 | } elseif (is_scalar($child)) { 191 | $childV = trim((string)$child); 192 | if (strlen($childV) > 0) { 193 | $v[] = $childV; 194 | } 195 | } 196 | } 197 | $v = $v ?? ['']; 198 | } else { 199 | $kv = [$node['line']]; 200 | } 201 | } 202 | if (count($kv) == 1) { 203 | $k = trim($kv[0]); 204 | $v = self::nodesToDict($node['children']); 205 | if (empty($v)) { 206 | $v = $k; 207 | $k = ''; 208 | } 209 | } 210 | if (!empty($k)) { 211 | $v = is_array($v) 212 | ? (count($v) > 1 ? $v : reset($v)) 213 | : $v; 214 | $dict = array_merge_recursive($dict, [$k => $v]); 215 | } else { 216 | $dict[] = $v; 217 | } 218 | } 219 | return $dict; 220 | } 221 | 222 | /** 223 | * @param array $dict 224 | * @param string $header 225 | * @return array 226 | */ 227 | public static function dictToGroup($dict, $header = '$header') { 228 | if (empty($dict) || count($dict) > 1) { 229 | return $dict; 230 | } 231 | $k = array_keys($dict)[0]; 232 | $v = array_values($dict)[0]; 233 | if (!is_string($k) || !is_array($v)) { 234 | return $dict; 235 | } 236 | $vk = array_keys($v)[0]; 237 | if (is_string($vk)) { 238 | return array_merge([$header => $k], $v); 239 | } 240 | $dict[$header] = $k; 241 | return $dict; 242 | } 243 | 244 | /** 245 | * @param array $groups 246 | * @return array 247 | */ 248 | public static function joinParentlessGroups($groups) { 249 | $lastGroup = null; 250 | foreach ($groups as &$group) { 251 | if (count($group) == 1 && is_string(key($group)) && reset($group) === false) { 252 | $lastGroup = &$group; 253 | unset($group); 254 | } elseif (isset($lastGroup) && count($group) > 0 && is_string(key($group)) && reset($group)) { 255 | $lastGroup[key($lastGroup)] = $group; 256 | unset($lastGroup); 257 | } 258 | } 259 | unset($lastGroup); 260 | unset($group); 261 | return $groups; 262 | } 263 | 264 | /** 265 | * @param string[]|string $rawstates 266 | * @param bool $removeExtra 267 | * @return string[] 268 | */ 269 | public static function parseStates($rawstates, $removeExtra = true) 270 | { 271 | $states = []; 272 | $rawstates = is_array($rawstates) ? $rawstates : [ strval($rawstates) ]; 273 | foreach ($rawstates as $rawstate) { 274 | if (preg_match('/^\s*((\d{3}\s+)?[a-z]{2,}.*)\s*/ui', $rawstate, $m)) { 275 | $state = mb_strtolower($m[1]); 276 | $state = $removeExtra ? trim((string)preg_replace('~\(.+?\)|((- )?http| $line) { 297 | if ($emptyBefore && preg_match('~^\w+(\s+\w+){0,2}$~', trim(rtrim($line, ':')))) { 298 | $line = trim(rtrim($line, ':')) . ':'; 299 | } 300 | // .jp style 301 | if (preg_match('~([a-z]\.)?\s*\[(.+?)\]\s+(.*)$~', $line, $m)) { 302 | $line = sprintf('%s: %s', $m[2], $m[3]); 303 | } 304 | $isHeader = preg_match('~^\w+(\s+\w+){0,2}:$~', $line); 305 | if ($isHeader) { 306 | $outLines[] = ''; 307 | } 308 | $needIndent = $needIndent || $isHeader; 309 | if (!empty($line) || !$kvBefore) { 310 | if ($needIndent && !$isHeader && !empty($line)) { 311 | $indent = ' '; 312 | $nextLinePad = empty($lines[$i + 1]) || strlen(trim($lines[$i + 1])) == 0 ? 0 : self::calcIndent($lines[$i + 1]); 313 | if ($nextLinePad <= 2 && self::calcIndent($lines[$i]) == 0) { 314 | $indent .= str_repeat(' ', $nextLinePad); 315 | } 316 | $outLines[] = $indent . $line; 317 | } else { 318 | $outLines[] = $line; 319 | } 320 | } 321 | $emptyBefore = empty($line); 322 | $kvBefore = preg_match('~^\w+(\s+\w+){0,2}:\s*\S+~', $line); 323 | } 324 | return $outLines; 325 | } 326 | 327 | /** 328 | * Removes unnecessary empty lines inside block 329 | * @param string[] $lines 330 | * @param callable|null $biasIndentFn 331 | * @return string[] 332 | */ 333 | public static function removeInnerEmpties($lines, $biasIndentFn = null) 334 | { 335 | $prevPad = 0; 336 | $outLines = []; 337 | foreach ($lines as $index => $line) { 338 | if (empty($line)) { 339 | $nextLine = isset($lines[$index + 1]) ? $lines[$index + 1] : ''; 340 | if (!empty($nextLine) && $prevPad > 0 && $prevPad == self::calcIndent($nextLine, $biasIndentFn)) { 341 | continue; 342 | } 343 | } 344 | $prevPad = empty($line) ? 0 : self::calcIndent($line, $biasIndentFn); 345 | $outLines[] = $line; 346 | } 347 | return $outLines; 348 | } 349 | } 350 | -------------------------------------------------------------------------------- /src/Iodev/Whois/Modules/Tld/Parsers/BlockParser.php: -------------------------------------------------------------------------------- 1 | groupsFromText($response->text); 80 | $rootFilter = $this->createGroupFilter() 81 | ->setGroups($groups) 82 | ->useIgnoreCase(true) 83 | ->handleEmpty($this->emptyValuesDict) 84 | ->setHeaderKey($this->headerKey) 85 | ->setDomainKeys($this->domainKeys) 86 | ->setSubsetParams([ 87 | '$domain' => $response->domain, 88 | '$domainUnicode' => DomainHelper::toUnicode($response->domain), 89 | ]); 90 | 91 | $reserved = $rootFilter->cloneMe() 92 | ->filterHasSubsetOf($this->reservedDomainSubsets) 93 | ->toSelector() 94 | ->selectKeys($this->reservedDomainKeys) 95 | ->getFirst(); 96 | 97 | $isReserved = !empty($reserved); 98 | 99 | $domainFilter = $rootFilter->cloneMe() 100 | ->useMatchFirstOnly(true) 101 | ->filterHasSubsetOf($this->domainSubsets); 102 | 103 | $primaryFilter = $rootFilter->cloneMe() 104 | ->useMatchFirstOnly(true) 105 | ->filterHasSubsetOf($this->primarySubsets) 106 | ->useFirstGroupOr($domainFilter->getFirstGroup()); 107 | 108 | $data = [ 109 | "parserType" => $this->getType(), 110 | "domainName" => $this->parseDomain($domainFilter) ?: ($isReserved ? $response->domain : ''), 111 | "states" => $this->parseStates($rootFilter, $primaryFilter), 112 | "nameServers" => $this->parseNameServers($rootFilter, $primaryFilter), 113 | "dnssec" => $this->parseDnssec($rootFilter, $primaryFilter), 114 | "owner" => $this->parseOwner($rootFilter, $primaryFilter) ?: ($isReserved ? $reserved : ''), 115 | "registrar" => $this->parseRegistrar($rootFilter, $primaryFilter), 116 | "creationDate" => $this->parseCreationDate($rootFilter, $primaryFilter), 117 | "expirationDate" => $this->parseExpirationDate($rootFilter, $primaryFilter), 118 | "updatedDate" => $this->parseUpdatedDate($rootFilter, $primaryFilter), 119 | "whoisServer" => $this->parseWhoisServer($rootFilter, $primaryFilter), 120 | ]; 121 | 122 | $info = $this->createDomainInfo($response, $data, [ 123 | 'groups' => $groups, 124 | 'rootFilter' => $rootFilter, 125 | 'domainFilter' => $domainFilter, 126 | 'primaryFilter' => $primaryFilter, 127 | 'reserved' => $reserved, 128 | ]); 129 | return $isReserved || $info->isValuable($this->notRegisteredStatesDict) ? $info : null; 130 | } 131 | 132 | /** 133 | * @param GroupFilter $domainFilter 134 | * @return string 135 | */ 136 | protected function parseDomain(GroupFilter $domainFilter) 137 | { 138 | $sel = $domainFilter 139 | ->toSelector() 140 | ->selectKeys($this->domainKeys) 141 | ->removeEmpty(); 142 | $this->matchedDomain = $sel->getFirst(''); 143 | 144 | $domain = $sel->mapDomain()->removeEmpty()->getFirst(''); 145 | if (!empty($domain)) { 146 | return $domain; 147 | } 148 | 149 | $sel = $domainFilter->cloneMe() 150 | ->filterHasHeader() 151 | ->toSelector() 152 | ->selectKeys([ 'name' ]) 153 | ->removeEmpty(); 154 | $this->matchedDomain = $sel->getFirst(''); 155 | 156 | return $sel->mapDomain()->removeEmpty()->getFirst(''); 157 | } 158 | 159 | /** 160 | * @param GroupFilter $rootFilter 161 | * @param GroupFilter $primaryFilter 162 | * @return array 163 | */ 164 | protected function parseStates(GroupFilter $rootFilter, GroupFilter $primaryFilter) 165 | { 166 | $states = $primaryFilter->toSelector() 167 | ->selectKeys($this->statesKeys) 168 | ->mapStates() 169 | ->removeEmpty() 170 | ->removeDuplicates() 171 | ->getAll(); 172 | 173 | if (!empty($states)) { 174 | return $states; 175 | } 176 | 177 | $extraStates = []; 178 | if ($this->matchedDomain && preg_match('~is\s+(.+)$~', $this->matchedDomain, $m)) { 179 | $extraStates = [ $m[1] ]; 180 | } 181 | return $rootFilter->cloneMe() 182 | ->useMatchFirstOnly(true) 183 | ->filterHasSubsetOf($this->statesSubsets) 184 | ->toSelector() 185 | ->selectItems($extraStates) 186 | ->selectKeys($this->statesKeys) 187 | ->mapStates() 188 | ->removeEmpty() 189 | ->removeDuplicates() 190 | ->getAll(); 191 | } 192 | 193 | /** 194 | * @param GroupFilter $rootFilter 195 | * @param GroupFilter $primaryFilter 196 | * @return array 197 | */ 198 | protected function parseNameServers(GroupFilter $rootFilter, GroupFilter $primaryFilter) 199 | { 200 | $nameServers = $rootFilter->cloneMe() 201 | ->useMatchFirstOnly(true) 202 | ->filterHasSubsetOf($this->nameServersSubsets) 203 | ->useFirstGroup() 204 | ->toSelector() 205 | ->selectKeys($this->nameServersKeys) 206 | ->selectKeyGroups($this->nameServersKeysGroups) 207 | ->mapAsciiServer() 208 | ->removeEmpty() 209 | ->getAll(); 210 | 211 | $nameServers = $rootFilter->cloneMe() 212 | ->filterHasSubsetOf($this->nameServersSparsedSubsets) 213 | ->toSelector() 214 | ->useMatchFirstOnly(true) 215 | ->selectItems($nameServers) 216 | ->selectKeys($this->nameServersKeys) 217 | ->selectKeyGroups($this->nameServersKeysGroups) 218 | ->mapAsciiServer() 219 | ->removeEmpty() 220 | ->removeDuplicates() 221 | ->getAll(); 222 | 223 | if (!empty($nameServers)) { 224 | return $nameServers; 225 | } 226 | return $primaryFilter->toSelector() 227 | ->useMatchFirstOnly(true) 228 | ->selectKeys($this->nameServersKeys) 229 | ->selectKeyGroups($this->nameServersKeysGroups) 230 | ->mapAsciiServer() 231 | ->removeEmpty() 232 | ->removeDuplicates() 233 | ->getAll(); 234 | } 235 | 236 | /** 237 | * @param GroupFilter $rootFilter 238 | * @param GroupFilter $primaryFilter 239 | * @return string 240 | */ 241 | protected function parseDnssec(GroupFilter $rootFilter, GroupFilter $primaryFilter) 242 | { 243 | $dnssec = $rootFilter->cloneMe() 244 | ->useMatchFirstOnly(true) 245 | ->filterHasSubsetOf($this->nameServersSubsets) 246 | ->useFirstGroup() 247 | ->toSelector() 248 | ->selectKeys($this->dnssecKeys) 249 | ->removeEmpty() 250 | ->sort(SORT_ASC) 251 | ->getFirst() 252 | ; 253 | if (empty($dnssec)) { 254 | $dnssec = $primaryFilter->toSelector() 255 | ->selectKeys($this->dnssecKeys) 256 | ->removeEmpty() 257 | ->sort(SORT_ASC) 258 | ->getFirst(''); 259 | } 260 | if (empty($dnssec)) { 261 | $dnssec = $rootFilter->toSelector() 262 | ->selectKeys($this->dnssecKeys) 263 | ->removeEmpty() 264 | ->sort(SORT_ASC) 265 | ->getFirst(''); 266 | } 267 | return $dnssec; 268 | } 269 | 270 | /** 271 | * @param GroupFilter $rootFilter 272 | * @param GroupFilter $primaryFilter 273 | * @return string 274 | */ 275 | protected function parseOwner(GroupFilter $rootFilter, GroupFilter $primaryFilter) 276 | { 277 | $owner = $rootFilter->cloneMe() 278 | ->useMatchFirstOnly(true) 279 | ->filterHasSubsetOf($this->ownerSubsets) 280 | ->toSelector() 281 | ->selectKeys($this->ownerKeys) 282 | ->getFirst(''); 283 | 284 | if (empty($owner)) { 285 | $owner = $primaryFilter->toSelector() 286 | ->selectKeys($this->ownerKeys) 287 | ->getFirst(''); 288 | } 289 | if (!empty($owner)) { 290 | $owner = $rootFilter->cloneMe() 291 | ->setSubsetParams(['$id' => $owner]) 292 | ->useMatchFirstOnly(true) 293 | ->filterHasSubsetOf($this->contactSubsets) 294 | ->toSelector() 295 | ->selectKeys($this->contactOrgKeys) 296 | ->selectItems([ $owner ]) 297 | ->removeEmpty() 298 | ->getFirst(''); 299 | } 300 | return $owner; 301 | } 302 | 303 | /** 304 | * @param GroupFilter $rootFilter 305 | * @param GroupFilter $primaryFilter 306 | * @return string 307 | */ 308 | protected function parseRegistrar(GroupFilter $rootFilter, GroupFilter $primaryFilter) 309 | { 310 | $registrar = $primaryFilter->toSelector() 311 | ->useMatchFirstOnly(true) 312 | ->selectKeys($this->registrarKeys) 313 | ->getFirst(); 314 | 315 | if (empty($registrar)) { 316 | $registrarFilter = $rootFilter->cloneMe() 317 | ->useMatchFirstOnly(true) 318 | ->filterHasSubsetOf($this->registrarSubsets); 319 | 320 | $registrar = $registrarFilter->toSelector() 321 | ->selectKeys($this->registrarGroupKeys) 322 | ->getFirst(); 323 | } 324 | if (empty($registrar) && !empty($registrarFilter)) { 325 | $registrar = $registrarFilter->filterHasHeader() 326 | ->toSelector() 327 | ->selectKeys([ 'name' ]) 328 | ->getFirst(); 329 | } 330 | if (empty($registrar)) { 331 | $registrar = $primaryFilter->toSelector() 332 | ->selectKeys($this->registrarKeys) 333 | ->getFirst(); 334 | } 335 | 336 | $regFilter = $rootFilter->cloneMe() 337 | ->useMatchFirstOnly(true) 338 | ->filterHasSubsetOf($this->registrarReservedSubsets); 339 | 340 | $regId = $regFilter->toSelector() 341 | ->selectKeys($this->registrarReservedKeys) 342 | ->getFirst(); 343 | 344 | if (!empty($regId) && (empty($registrar) || $regFilter->getFirstGroup() != $primaryFilter->getFirstGroup())) { 345 | $registrarOrg = $rootFilter->cloneMe() 346 | ->setSubsetParams(['$id' => $regId]) 347 | ->useMatchFirstOnly(true) 348 | ->filterHasSubsetOf($this->contactSubsets) 349 | ->toSelector() 350 | ->selectKeys($this->contactOrgKeys) 351 | ->getFirst(); 352 | 353 | $owner = $this->parseOwner($rootFilter, $primaryFilter); 354 | $registrar = ($registrarOrg && $registrarOrg != $owner) 355 | ? $registrarOrg 356 | : $registrar; 357 | } 358 | 359 | return $registrar; 360 | } 361 | 362 | protected function parseCreationDate(GroupFilter $rootFilter, GroupFilter $primaryFilter): int 363 | { 364 | return $this->parseDate( 365 | $rootFilter, 366 | $primaryFilter, 367 | $this->creationDateKeys, 368 | '~registered\s+on\b~ui' 369 | ); 370 | } 371 | 372 | protected function parseExpirationDate(GroupFilter $rootFilter, GroupFilter $primaryFilter): int 373 | { 374 | return $this->parseDate( 375 | $rootFilter, 376 | $primaryFilter, 377 | $this->expirationDateKeys, 378 | '~registry\s+fee\s+due\s+on\b~ui' 379 | ); 380 | } 381 | 382 | protected function parseUpdatedDate(GroupFilter $rootFilter, GroupFilter $primaryFilter): int 383 | { 384 | $ts = $this->parseDate($rootFilter, $primaryFilter, $this->updatedDateKeys); 385 | if ($ts) { 386 | return $ts; 387 | } 388 | return $primaryFilter->cloneMe() 389 | ->useMatchFirstOnly(true) 390 | ->filterHasSubsetKeyOf($this->updatedDateExtraKeys) 391 | ->toSelector() 392 | ->selectKeys($this->updatedDateExtraKeys) 393 | ->mapUnixTime($this->getOption('inversedDateMMDD', false)) 394 | ->removeEmpty() 395 | ->getFirst(0) 396 | ; 397 | } 398 | 399 | protected function parseDate( 400 | GroupFilter $rootFilter, 401 | GroupFilter $primaryFilter, 402 | array $keys, 403 | string $fallbackRegex = '' 404 | ): int { 405 | $time = $primaryFilter->toSelector() 406 | ->selectKeys($keys) 407 | ->mapUnixTime($this->getOption('inversedDateMMDD', false)) 408 | ->getFirst(0) 409 | ; 410 | if (!empty($time)) { 411 | return $time; 412 | } 413 | $sel = $rootFilter->cloneMe() 414 | ->useMatchFirstOnly(true) 415 | ->filterHasSubsetKeyOf($keys) 416 | ->toSelector() 417 | ->selectKeys($keys) 418 | ; 419 | $time = $sel->cloneMe() 420 | ->mapUnixTime($this->getOption('inversedDateMMDD', false)) 421 | ->getFirst(0) 422 | ; 423 | if (!empty($time)) { 424 | return $time; 425 | } 426 | if (empty($fallbackRegex)) { 427 | return 0; 428 | } 429 | foreach ($sel->getAll() as $str) { 430 | if ($str && is_string($str) && preg_match($fallbackRegex, $str)) { 431 | $time = DateHelper::parseDateInText($str); 432 | if (!empty($time)) { 433 | return $time; 434 | } 435 | } 436 | } 437 | return 0; 438 | } 439 | 440 | /** 441 | * @param GroupFilter $rootFilter 442 | * @param GroupFilter $primaryFilter 443 | * @return mixed 444 | */ 445 | protected function parseWhoisServer(GroupFilter $rootFilter, GroupFilter $primaryFilter) 446 | { 447 | return $primaryFilter->toSelector() 448 | ->selectKeys($this->whoisServerKeys) 449 | ->mapAsciiServer() 450 | ->getFirst(''); 451 | } 452 | } 453 | --------------------------------------------------------------------------------