首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> 網頁程式設計 >> PHP 程式設計 >> PHP 綜合知識

PHP fsockopen/pfsockopen 被禁用解決方案:stream_socket_client 完美替代與兼容方案

     編輯:PHP 綜合知識

問題背景與根因

眾多虛擬主機/雲服務商出於安全考慮禁用 fsockopen、pfsockopen,導致依賴 Socket 通信的功能失效(SMTP 郵件、API 調用、爬蟲、支付回調等)。核心原因:這些函數可發起任意 TCP 連接,存在掃描內網、SSRF 攻擊風險。

方案一:stream_socket_client 直接替代 (PHP 5.0+)

參數對照表

fsockopen 參數stream_socket_client 參數說明
$hostname$remote_socket (含協議前綴)需添加 tcp://、ssl://、udp://
$port包含在 $remote_socket 中格式:tcp://host:port
$errno$errno (引用)錯誤代碼
$errstr$errstr (引用)錯誤信息
$timeout$timeout連接超時 (秒)
-$flagsSTREAM_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";
    }
}

方案二:PHP 版本 < 5.0 或 stream_socket_client 也被禁用

純 PHP Socket 擴展實現

/**
 * 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);
}

方案三:cURL 替代 (最高兼容性)

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 組裝 HTTPGuzzle/cURL高級特性:重試、中間件、異步
支付網關回調fsockopen 驗證SDK 官方驗證類避免手動 Socket 通信
Whois 查詢fsockopen port 43stream_socket_client協議簡單,直接替換
DNS 查詢fsockopen UDP 53dns_get_record()內置函數更可靠
即時通訊/推送pfsockopen 長連接Swoole/Workerman/Redis PubSub專業框架替代手寫 Socket

PHPMailer 具體修復示例

// 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 代理服務替代方案。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved