眾多虛擬主機/雲服務商出於安全考慮禁用 fsockopen、pfsockopen,導致依賴 Socket 通信的功能失效(SMTP 郵件、API 調用、爬蟲、支付回調等)。核心原因:這些函數可發起任意 TCP 連接,存在掃描內網、SSRF 攻擊風險。
| fsockopen 參數 | stream_socket_client 參數 | 說明 |
|---|---|---|
| $hostname | $remote_socket (含協議前綴) | 需添加 tcp://、ssl://、udp:// |
| $port | 包含在 $remote_socket 中 | 格式:tcp://host:port |
| $errno | $errno (引用) | 錯誤代碼 |
| $errstr | $errstr (引用) | 錯誤信息 |
| $timeout | $timeout | 連接超時 (秒) |
| - | $flags | STREAM_CLIENT_CONNECT | STREAM_CLIENT_ASYNC_CONNECT |
| - | $context | 流上下文 (SSL 驗證、代理、綁定 IP 等) |
// 原代碼
$fp = fsockopen($host, $port, $errno, $errstr, $timeout);
// 替換為
$fp = stream_socket_client(
"tcp://{$host}:{$port}", // 關鍵:協議前綴 + 主機:端口
$errno,
$errstr,
$timeout,
STREAM_CLIENT_CONNECT | STREAM_CLIENT_PERSISTENT // 持久連接對應 pfsockopen
);
// 帶 SSL/TLS
$fp = stream_socket_client(
"ssl://{$host}:{$port}",
$errno,
$errstr,
$timeout,
STREAM_CLIENT_CONNECT,
stream_context_create([
'ssl' => [
'verify_peer' => false, // 生產環境建議 true
'verify_peer_name' => false, // 生產環境建議 true
'allow_self_signed' => true,
]
])
);
'stream_socket_client("tcp://${1}:${2}", ${3}, ${4}, ${5})',
// fsockopen("host", 80, $errno, $errstr, 30)
'/fsockopen\s*\(\s*["\']([^"\']+)["\']\s*,\s*(\d+)\s*,\s*\&?\$([a-zA-Z_][a-zA-Z0-9_]*)\s*,\s*\&?\$([a-zA-Z_][a-zA-Z0-9_]*)\s*,\s*(\d+)\s*\)/'
=> 'stream_socket_client("tcp://${1}:${2}", ${3}, ${4}, ${5})',
];
$files = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator($dir, FilesystemIterator::SKIP_DOTS)
);
foreach ($files as $file) {
if (!$file->isFile()) continue;
$ext = strtolower($file->getExtension());
if (!in_array($ext, $extensions)) continue;
$content = file_get_contents($file->getPathname());
$original = $content;
foreach ($patterns as $pattern => $replacement) {
$content = preg_replace($pattern, $replacement, $content);
}
if ($content !== $original) {
file_put_contents($file->getPathname(), $content);
echo "Updated: {$file->getPathname()}\n";
}
}
/**
* fsockopen 兼容實現 (基於 sockets 擴展)
* 需啟用 php_sockets.dll / --enable-sockets
*/
function compat_fsockopen($hostname, $port, &$errno, &$errstr, $timeout = 30) {
// 解析主機名
$ip = gethostbyname($hostname);
if ($ip === $hostname) { // 解析失敗
$errno = EAI_NONAME ?? 11001;
$errstr = "php_network_getaddresses: getaddrinfo failed: Name or service not known";
return false;
}
// 創建 Socket
$socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
if ($socket === false) {
$errno = socket_last_error();
$errstr = socket_strerror($errno);
return false;
}
// 設置非阻塞 (模擬 fsockopen 行為)
socket_set_nonblock($socket);
// 連接
$result = @socket_connect($socket, $ip, $port);
if ($result === false) {
$error = socket_last_error($socket);
if ($error !== EINPROGRESS && $error !== EWOULDBLOCK) {
$errno = $error;
$errstr = socket_strerror($error);
socket_close($socket);
return false;
}
}
// 等待連接完成 (使用 select)
$write = [$socket];
$except = [$socket];
$tv_sec = (int)$timeout;
$tv_usec = ($timeout - $tv_sec) * 1000000;
$select = @socket_select($read = null, $write, $except, $tv_sec, $tv_usec);
if ($select === false || $select === 0) {
// 超時或錯誤
$errno = $select === false ? socket_last_error($socket) : ETIMEDOUT ?? 110;
$errstr = $select === false ? socket_strerror($errno) : "Connection timed out";
socket_close($socket);
return false;
}
// 檢查異常
if (!empty($except)) {
$errno = socket_last_error($socket);
$errstr = socket_strerror($errno);
socket_close($socket);
return false;
}
// 連接成功,恢復阻塞模式
socket_set_block($socket);
// 返回流資源 (關鍵:將 socket 資源轉為流)
// PHP 內部無直接轉換,需使用 stream_socket_accept 包裝
// 此處返回 socket 資源,調用方需配合 socket_read/write/close
return $socket;
}
// 配套流操作函數
function compat_fread($socket, $length) {
return socket_read($socket, $length, PHP_BINARY_READ);
}
function compat_fwrite($socket, $string) {
return socket_write($socket, $string, strlen($string));
}
function compat_fclose($socket) {
return socket_close($socket);
}
function compat_feof($socket) {
// socket 無直接 feof,嘗試讀取 1 字節判斷
$peek = @socket_read($socket, 1, PHP_BINARY_READ);
return $peek === false || $peek === '';
}
// 原代碼
$fp = fsockopen($host, 80, $errno, $errstr, 30);
fwrite($fp, $request);
$response = '';
while (!feof($fp)) {
$response .= fread($fp, 1024);
}
fclose($fp);
// 適配後 (使用 compat 系列)
$fp = compat_fsockopen($host, 80, $errno, $errstr, 30);
if ($fp) {
compat_fwrite($fp, $request);
$response = '';
while (!compat_feof($fp)) {
$response .= compat_fread($fp, 1024);
}
compat_fclose($fp);
}
function curl_socket_request($url, $postData = null, $timeout = 30, $headers = []) {
$ch = curl_init();
curl_setopt_array($ch, [
CURLOPT_URL => $url,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => $timeout,
CURLOPT_CONNECTTIMEOUT => min(10, $timeout),
CURLOPT_SSL_VERIFYPEER => false, // 生產環境請設置 true 並配置 CA
CURLOPT_SSL_VERIFYHOST => 2,
CURLOPT_FOLLOWLOCATION => true,
CURLOPT_MAXREDIRS => 5,
CURLOPT_USERAGENT => 'PHP-Socket-Compat/1.0',
]);
if ($postData !== null) {
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, $postData);
}
if ($headers) {
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
}
// 原始 Socket 協議 (非 HTTP) 無法直接用 cURL
// 僅適用於 HTTP/HTTPS 協議替代
$response = curl_exec($ch);
$errno = curl_errno($ch);
$error = curl_error($ch);
$info = curl_getinfo($ch);
curl_close($ch);
return [
'response' => $response,
'errno' => $errno,
'error' => $error,
'info' => $info,
];
}
| 場景 | 原函數 | 推薦替代 | 關鍵調整 |
|---|---|---|---|
| SMTP 發郵 | fsockopen + 手動協議 | PHPMailer/Symfony Mailer | 內置 stream_socket_client 支持 |
| HTTP API 調用 | fsockopen 組裝 HTTP | Guzzle/cURL | 高級特性:重試、中間件、異步 |
| 支付網關回調 | fsockopen 驗證 | SDK 官方驗證類 | 避免手動 Socket 通信 |
| Whois 查詢 | fsockopen port 43 | stream_socket_client | 協議簡單,直接替換 |
| DNS 查詢 | fsockopen UDP 53 | dns_get_record() | 內置函數更可靠 |
| 即時通訊/推送 | pfsockopen 長連接 | Swoole/Workerman/Redis PubSub | 專業框架替代手寫 Socket |
// vendor/phpmailer/phpmailer/src/SMTP.php
// 原代碼 (約第 200 行)
$this->smtp_conn = @fsockopen(
$host,
$port,
$errno,
$errstr,
$timeout
);
// 修改為
$context = stream_context_create([
'ssl' => [
'verify_peer' => false,
'verify_peer_name' => false,
]
]);
$this->smtp_conn = @stream_socket_client(
($this->SMTPSecure ? 'ssl://' : 'tcp://') . $host . ':' . $port,
$errno,
$errstr,
$timeout,
STREAM_CLIENT_CONNECT,
$context
);
// 如果需要 STARTTLS (先明文後升級)
if ($this->SMTPSecure === 'tls') {
// 連接後發送 STARTTLS 命令
stream_socket_enable_crypto($this->smtp_conn, true, STREAM_CRYPTO_METHOD_TLS_CLIENT);
}
主機商溝通話術
工單模板:
我們的應用需要發送郵件/調用外部 API/處理支付回調,依賴 PHP 的
stream_socket_client函數(標準擴展,非危險函數)。該函數僅用於合法業務通信,不涉及端口掃描或內網探測。請確認是否已禁用stream_socket_client,或提供白名單 IP 機制讓我們的出站連接正常工作。如確需禁用,請推薦貴司提供的郵件發送 API / HTTP 代理服務替代方案。