標準 WHOIS 協議(RFC 3912)基於 TCP 43 端口,查詢域名註冊信息。原版類僅查詢 whois.internic.net,對於非 .com/.net 域名(如 .org、.cn、.io 等)返回信息極少。本增強版實現兩大改進:
Whois Server: 字段,自動向該權威服務器發起二次查詢,獲取完整註冊詳情(如 Yahoo 在 whois.networksolutions.com 的詳細記錄)。class whois {
// 配置屬性
public $use_cache = 1;
public $FROM_CACHE = 0;
public $cache_dir = "./";
public $port = 43;
public $MAXLEN = 100;
public $MAX_RETRIES = 0; // 連接失敗重試次數
public $SLEEP_VAL = 1; // 重試間隔秒數
public $RETRY = 0;
public $FOUND = 0; // 查詢結果標記
public $ERROR = 0; // 錯誤計數
public $DATA_MIN = 8; // 最小有效數據字節數
public $DATA_COUNT = 0;
// 運行時狀態
public $WHOIS_SERVER; // 當前查詢服務器
public $NEW_WHOIS; // 解析出的權威服務器
public $FURTHER_INFO = 0; // 是否發現權威服務器
// 核心方法:connect()、rawlookup()、cnrawlookup()
}
function connect($server) {
$this->RETRY = 0;
while ($this->RETRY <= $this->MAX_RETRIES) {
$ptr = fsockopen($server, $this->port);
if ($ptr > 0) {
$this->ERROR = 0;
return $ptr;
} else {
$this->ERROR++;
$this->RETRY++;
sleep($this->SLEEP_VAL);
}
}
return false; // 隱式返回
}
簡單的阻塞式連接重試,生產環境建議設置 MAX_RETRIES=3、SLEEP_VAL=2,並加入連接超時 stream_set_timeout($ptr, 10)。
function rawlookup($query, $server) {
if (!$query) return "";
$ptr = $this->connect($server);
if (!$ptr) { $this->ERROR++; return false; }
if (!ereg($query, "\n$")) $query .= "\n"; // 確保以換行結尾
fputs($ptr, $query);
$i = 0; $this->FOUND = 1;
while (!feof($ptr)) {
$array[$i] = fgets($ptr, $this->MAXLEN);
$this->DATA_COUNT += strlen(chop($array[$i]));
// 判斷查詢失敗標記
if (eregi("No match for", $array[$i]) || eregi("No entries found", $array[$i])) {
$this->FOUND = 0;
} elseif (eregi("WHOIS database is down", $array[$i]) || eregi("Please wait a while", $array[$i])) {
$this->ERROR++; $this->FOUND = 0; break;
}
// 關鍵:解析權威 WHOIS 服務器
if (eregi("Whois Server:", $array[$i])) {
// 取行尾 13 字符(假設格式固定),實際建議用正則提取主機名
$this->NEW_WHOIS = trim(substr(trim($array[$i]), (strlen(trim($array[$i])) - 13) * (-1)));
$this->FURTHER_INFO = 1;
}
$i++;
}
fclose($ptr);
if ($this->DATA_COUNT > $this->DATA_MIN) return $array;
else { $this->ERROR++; return false; }
}
關鍵點:eregi("Whois Server:") 捕獲類似 Whois Server: whois.networksolutions.com 的行,提取主機名用於二次查詢。
function cnrawlookup($query, $server) {
// 邏輯與 rawlookup 相同,但無權威服務器解析分支
// 直接查詢 whois.cnnic.net.cn
}
$myWHOIS = new whois();
$thisname = $servername . $domainname; // 如 "yahoo.com"
// 根據後綴選擇初始服務器
if (ereg("\.cn$", $thisname)) {
$myWHOIS->WHOIS_SERVER = "whois.cnnic.net.cn";
$array = $myWHOIS->cnrawlookup($thisname, $myWHOIS->WHOIS_SERVER);
} else {
$myWHOIS->WHOIS_SERVER = "whois.internic.net";
$array = $myWHOIS->rawlookup($thisname, $myWHOIS->WHOIS_SERVER);
}
// 輸出首輪結果
echo $thisname . "\n";
for ($x = 0; $x < count($array); $x++) {
echo "$x {$array[$x]}";
}
// 國際域名且發現權威服務器 → 發起二次查詢
if (!ereg("\.cn$", $thisname) && $myWHOIS->FURTHER_INFO) {
echo "\nFurther information\n";
$array_further = $myWHOIS->rawlookup($thisname, $myWHOIS->NEW_WHOIS);
for ($x = 0; $x < count($array_further); $x++) {
echo "$x {$array_further[$x]}";
}
}
| 問題 | 影響 | 現代替代 |
|---|---|---|
ereg/eregi 已棄用 (PHP 5.3+) / 移除 (PHP 7+) | 代碼無法運行 | preg_match (PCRE),大小寫不敏感用 /i 修飾符 |
| 無連接超時控制 | 掛起風險 | stream_set_timeout($ptr, 10) 或 stream_context_create(['socket'=>['bindto'=>'', 'timeout'=>10]]) |
| 緩存機制未實現 | $use_cache 僅佔位 | 實現文件/Redis 緩存,Key 為域名,TTL 1 天 |
| 解析 Whois Server 位置硬編碼 | 格式變動即失效 | preg_match('/Whois Server:\s*(\S+)/i', $line, $m) 提取 |
| 同步阻塞查詢 | 高併發下性能差 | Swoole/Workerman 協程 + 連接池,或異步隊列 |
| 無 IPv6 支持 | 部分新服務器不可達 | fsockopen 自動支持雙棧,或顯式 tcp://[::1]:43 |
<?php
declare(strict_types=1);
namespace App\Whois;
use RuntimeException;
class WhoisClient
{
private int $port = 43;
private int $timeout = 10;
private int $maxRetries = 3;
private int $retryDelay = 1;
private ?string $cacheDir = null;
private int $cacheTtl = 86400; // 1 day
public function __construct(?string $cacheDir = null) {
$this->cacheDir = $cacheDir;
}
public function query(string $domain): WhoisResult {
$domain = strtolower(trim($domain));
// 緩存檢查
if ($this->cacheDir) {
$cacheFile = $this->cacheDir . '/' . md5($domain) . '.json';
if (file_exists($cacheFile) && (time() - filemtime($cacheFile)) < $this->cacheTtl) {
return WhoisResult::fromJson(file_get_contents($cacheFile));
}
}
$server = $this->selectServer($domain);
$rawData = $this->fetchWithRecursion($domain, $server);
$result = new WhoisResult($domain, $rawData);
// 寫入緩存
if ($this->cacheDir) {
file_put_contents($cacheFile, $result->toJson());
}
return $result;
}
private function selectServer(string $domain): string {
// 簡化:實際可維護 TLD->Server 映射表或查詢 IANA
return str_ends_with($domain, '.cn') ? 'whois.cnnic.cn' : 'whois.iana.org';
}
private function fetchWithRecursion(string $domain, string $server, int $depth = 0): array {
if ($depth > 3) return []; // 防無限遞歸
$data = $this->fetchRaw($domain, $server);
// 解析權威服務器
foreach ($data as $line) {
if (preg_match('/^(?:Whois Server|Registrar WHOIS Server|ReferralServer):\s*(\S+)/i', $line, $m)) {
$authServer = trim($m[1]);
if ($authServer !== $server) {
$data = array_merge($data, $this->fetchWithRecursion($domain, $authServer, $depth + 1));
}
break;
}
}
return $data;
}
private function fetchRaw(string $query, string $server): array {
$lastErr = null;
for ($attempt = 0; $attempt <= $this->maxRetries; $attempt++) {
$ctx = stream_context_create(['socket' => ['timeout' => $this->timeout]]);
$fp = @stream_socket_client("tcp://{$server}:{$this->port}", $errno, $errstr, $this->timeout, STREAM_CLIENT_CONNECT, $ctx);
if ($fp) {
stream_set_timeout($fp, $this->timeout);
fwrite($fp, $query . "\r\n");
$lines = [];
while (!feof($fp)) {
$lines[] = fgets($fp, 4096);
}
fclose($fp);
return $lines;
}
$lastErr = "{$errstr} ({$errno})";
if ($attempt < $this->maxRetries) sleep($this->retryDelay);
}
throw new RuntimeException("WHOIS query failed for {$server}: {$lastErr}");
}
}
class WhoisResult {
public string $domain;
public array $rawLines;
public string $rawText;
public array $parsed = [];
public function __construct(string $domain, array $lines) {
$this->domain = $domain;
$this->rawLines = $lines;
$this->rawText = implode('', $lines);
$this->parseKeyFields();
}
private function parseKeyFields(): void {
$patterns = [
'registrar' => '/Registrar:\s*(.+)/i',
'creation_date' => '/Creation Date:\s*(.+)/i',
'expiry_date' => '/(?:Registry Expiry Date|Expiration Date):\s*(.+)/i',
'name_servers' => '/Name Server:\s*(.+)/i',
'status' => '/Status:\s*(.+)/i',
];
foreach ($patterns as $key => $pattern) {
if (preg_match_all($pattern, $this->rawText, $m)) {
$this->parsed[$key] = $m[1];
}
}
}
public function toJson(): string {
return json_encode([
'domain' => $this->domain,
'raw' => $this->rawText,
'parsed' => $this->parsed,
'queried_at' => time(),
], JSON_UNESCAPED_UNICODE);
}
public static function fromJson(string $json): self {
$data = json_decode($json, true);
$obj = new self($data['domain'], explode("\n", $data['raw']));
$obj->parsed = $data['parsed'];
return $obj;
}
}
$client = new WhoisClient(sys_get_temp_dir() . '/whois_cache');
try {
$result = $client->query('google.com');
echo "Domain: {$result->domain}\n";
echo "Registrar: " . ($result->parsed['registrar'][0] ?? 'N/A') . "\n";
echo "Created: " . ($result->parsed['creation_date'][0] ?? 'N/A') . "\n";
echo "Expires: " . ($result->parsed['expiry_date'][0] ?? 'N/A') . "\n";
echo "Name Servers:\n";
foreach ($result->parsed['name_servers'] ?? [] as $ns) echo " - $ns\n";
} catch (Exception $e) {
echo "Error: {$e->getMessage()}\n";
}
原版 WHOIS 類雖然實現了核心遞歸查詢思路,但代碼已嚴重過時(ereg、無超時、無緩存實現、同步阻塞)。現代重構應採用:PCRE 正則、流上下文超時控制、文件/Redis 緩存、結構化結果解析、異步就緒架構。本文提供的 WhoisClient 可直接用於生產環境,並易於擴展為批量查詢、API 服務或域名監控系統的核心組件。