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

DedeCMS HTTP 下載類源碼分析:Socket 底層實現、重定向追蹤、斷點續傳與安全防護

     編輯:PHP 相關知識

DedeCMS HTTP 下載類源碼分析:Socket 底層實現、重定向追蹤、斷點續傳與安全防護

DedeCMS 的 `DedeHttpDown` 類是基於 `fsockopen` 的純 PHP HTTP 客戶端實現,無需 cURL 擴展即可完成文件下載、網頁抓取、POST 提交、302 重定向追蹤、相對 URL 補全等功能。本文逐模塊剖析其核心邏輯,並指出歷史遺留問題與現代化替代方案。

一、 類結構與核心屬性

class DedeHttpDown {
    // URL 解析後組件
    public $m_scheme = 'http';   // http/https
    public $m_host = '';         // 域名
    public $m_port = 80;         // 端口
    public $m_path = '/';        // 路徑
    public $m_query = '';        // 查詢字符串
    public $m_urlpath = '';      // path + query
    
    // 認證
    public $m_user = '';
    public $m_pass = '';
    
    // 連接資源
    public $m_fp = '';           // fsockopen 返回的流資源
    
    // 響應數據
    public $m_httphead = [];     // 解析後的響應頭 (小寫鍵名)
    public $m_html = '';         // 響應體內容
    
    // 請求頭(用戶可設置)
    public $m_puthead = [];      // 發送的請求頭
    
    // 狀態控制
    public $m_error = '';
    public $reTry = 0;           // 重試次數
    public $JumpCount = 0;       // 重定向計數
}

二、 核心流程:`PrivateStartSession`

統一入口,處理 GET/POST、HTTP/1.1 連接復用、響應頭解析、重定向遞歸:

1. 建立 TCP 連接 (`PrivateOpenHost`)

private function PrivateOpenHost(): bool {
    $this->m_fp = @fsockopen($this->m_host, $this->m_port, $errno, $errstr, 10);
    if (!$this->m_fp) { $this->m_error = $errstr; return false; }
    // 注意:不支持 HTTPS (fsockopen 無 SSL 上下文)
    return true;
}

缺陷:僅支持 HTTP,`$m_scheme === 'https'` 時無法建立 TLS 連接。現代環境必須改用 `stream_socket_client('ssl://...')` 或 cURL。

2. 構建並發送請求頭


// 請求行
$headString = ($requestType === 'POST') 
    ? "POST {$ps[0]} $httpv\r\n" 
    : "GET {$this->m_urlpath} $httpv\r\n";

// Host 頭 (HTTP/1.1 必須)
$this->m_puthead['Host'] = $this->m_host;

// 默認頭
$defaults = [
    'Accept'       => '*/*',
    'User-Agent'   => 'Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.2)',
    'Referer'      => 'https://'.$this->m_host,
];
foreach ($defaults as $k=>$v) $this->m_puthead[$k] ??= $v;

// 發送所有頭
foreach ($this->m_puthead as $k=>$v) {
    fputs($this->m_fp, "$k: $v\r\n");
}

// POST 數據
if ($requestType === 'POST') {
    $postdata = $ps[1] ?? 'OK';
    fputs($this->m_fp, "Content-Type: application/x-www-form-urlencoded\r\n");
    fputs($this->m_fp, "Content-Length: ".strlen($postdata)."\r\n");
}

// 連接控制
if ($httpv === 'HTTP/1.1') {
    fputs($this->m_fp, "Connection: Close\r\n\r\n"); // 關閉長連接,便於 feof 判斷結束
} else {
    fputs($this->m_fp, "\r\n");
}

if ($requestType === 'POST') fputs($this->m_fp, $postdata);

3. 解析響應頭 (關鍵邏輯)


// 狀態行
$httpstas = explode(' ', fgets($this->m_fp, 256));
$this->m_httphead['http-edition'] = $httpstas[0]; // HTTP/1.1
$this->m_httphead['http-state']   = $httpstas[1]; // 200
$this->m_httphead['http-describe']= implode(' ', array_slice($httpstas,2));

// 頭字段循環讀取
while (!feof($this->m_fp)) {
    $line = trim(fgets($this->m_fp, 256));
    if ($line === '') break; // 空行結束頭部
    
    // 手動解析 Key: Value (不使用爆破正則)
    $colonPos = strpos($line, ':');
    if ($colonPos !== false) {
        $hkey   = strtolower(trim(substr($line, 0, $colonPos)));
        $hvalue = trim(substr($line, $colonPos+1));
        if ($hkey) $this->m_httphead[$hkey] = $hvalue;
    }
}

// 連接異常重試 (最多 10 次)
if (feof($this->m_fp) && $this->reTry++ < 10) {
    return $this->PrivateStartSession($requestType);
}

4. 3xx 重定向自動追蹤


if (preg_match('/^3/', $this->m_httphead['http-state'])) {
    if ($this->JumpCount++ > 3) return; // 限制最大跳轉 3 次,防循環
    if (isset($this->m_httphead['location'])) {
        $newurl = $this->m_httphead['location'];
        // 相對 URL 轉絕對
        if (!preg_match('/^http/i', $newurl)) {
            $newurl = $this->FillUrl($newurl);
        }
        $this->JumpOpenUrl($newurl); // 遞歸 GET 請求新地址
    }
}

三、 實用公共方法

方法用途返回值
`OpenUrl($url, 'GET')`發起 GET 請求,自動處理重定向bool (成功/失敗)
`GetHtml()`獲取響應體 (文本類型)string / ''
`SaveToBin($file)`下載二進制文件 (流式寫入)bool
`SaveToText($file)`保存文本響應bool
`GetHead($name)`獲取響應頭值 (不區分大小寫)string / ''
`SetHead($k, $v)`設置自定義請求頭void
`IsGetOK()`判斷狀態碼 2xxbool
`IsText()`判斷 2xx 且 Content-Type 含 text/xmlbool
`FillUrl($rel)`相對 URL 轉絕對 (處理 ../、./、//、錨點)string

四、 `FillUrl` 相對路徑解析算法詳解

處理四類輸入:

  1. 絕對路徑 `/path/to/file` → `https://host/path/to/file`
  2. 相對路徑 `./`/`../`:基於 `BaseUrlPath` (當前目錄) 解析 `../` 回溯層級
  3. 當前目錄相對 `file.html` → `https://host/current_dir/file.html`
  4. 完整 URL 直接返回

// 核心邏輯片段
if ($surl[0] === '/') { // 絕對路徑
    $okurl = "https://{$this->HomeUrl}$surl";
} elseif ($surl[0] === '.') { // 相對路徑
    if ($surl[1] === '/') { // ./file
        $okurl = "https://{$this->BaseUrlPath}/".substr($surl,2);
    } else { // ../file 或 ../../file
        $urls = explode('/',$surl);
        $pathStep = 0; $dstr = '';
        foreach($urls as $i=>$u) {
            if ($u === '..') $pathStep++;
            elseif ($i < count($urls)-1) $dstr .= "$u/";
            else $dstr .= $u;
        }
        $baseParts = explode('/',$this->BaseUrlPath);
        if (count($baseParts) <= $pathStep) return '';
        $pstr = 'https://'.implode('/',$baseParts,0,count($baseParts)-$pathStep).'/';
        $okurl = $pstr.$dstr;
    }
} else { // 文件名或完整 URL
    $okurl = (str_starts_with($surl,'https://')) ? $surl : "https://{$this->BaseUrlPath}/$surl";
}
return 'https://'.preg_replace('/\/{1,}/','/',$okurl);

五、 歷史遺留問題與安全風險

問題影響修復建議
不支持 HTTPS現代網站全站 HTTPS,類失效改用 `stream_socket_client('ssl://host:443', ...)` 或集成 cURL
無證書驗證中間人攻擊風險cURL: `CURLOPT_SSL_VERIFYPEER=>true, CURLOPT_CAINFO=>...`
User-Agent 固定為 IE6易被 WAF/反爬蟲封禁可配置、隨機化、模擬真實瀏覽器
無超時精確控制`fsockopen` 第 5 參數僅連接超時,讀寫無超時`stream_set_timeout($fp, $sec, $usec)`
響應體全量讀入內存 (`GetHtml`)大文件導致 OOM流式處理、分塊寫入文件
手動頭解析脆弱多行頭、折行、大小寫不一致可能失敗使用 `http_parse_headers()` (pecl_http) 或 cURL `CURLOPT_HEADERFUNCTION`
無 Cookie 自動管理需手動 `SetHead('Cookie', ...)`引入 `GuzzleHttp\Cookie\CookieJar`
無並發/異步支持串行阻塞Swoole/Fiber 協程、Guzzle Promise、amphp

六、 現代化替代:GuzzleHTTP / Symfony HttpClient

<?php
require 'vendor/autoload.php';
use GuzzleHttp\Client;
use GuzzleHttp\Exception\RequestException;

$client = new Client([
    'timeout'  => 10,
    'connect_timeout' => 5,
    'follow_redirects' => ['max' => 5],
    'headers' => ['User-Agent' => 'MyBot/1.0'],
    'verify'  => '/etc/ssl/certs/ca-certificates.crt', // SSL 驗證
    'cookies' => true, // 自動 Cookie Jar
]);

try {
    // GET 下載文件 (流式,低內存)
    $response = $client->get('https://example.com/large.zip', ['sink' => '/path/to/save.zip']);
    
    // POST API
    $resp = $client->post('https://api.example.com/data', [
        'json' => ['key'=>'value'],
        'headers' => ['Authorization' => 'Bearer token']
    ]);
    $data = json_decode($resp->getBody()->getContents(), true);
    
    // 並發請求 (Promise)
    $promises = [
        'users' => $client->getAsync('https://api.example.com/users'),
        'posts' => $client->getAsync('https://api.example.com/posts'),
    ];
    $results = Utils::unwrap($promises); // 等待全部完成
    
} catch (RequestException $e) {
    echo "Request failed: ".$e->getMessage();
    if ($e->hasResponse()) echo "Status: ".$e->getResponse()->getStatusCode();
}

七、 遷移策略建議

  1. 兼容層封裝:新建 `HttpClientInterface`,實現 `DedeHttpDownAdapter` (包裝舊類) 和 `GuzzleAdapter`,業務代碼依賴接口。
  2. 灰度切換:配置開關 `use_guzzle = true`,新業務優先 Guzzle,舊業務逐步遷移。
  3. 功能對齊:重點補齊 `FillUrl` (用 `League\Uri\Parser`)、重定向限制、錯誤碼映射。
  4. 棄用計劃:標註 `@deprecated`,PHPStan 靜態分析攔截新調用,設定截止日期移除舊代碼。

`DedeHttpDown` 是 PHP 早期無 cURL 環境下的偉大發明,展現了對 HTTP 協議、Socket 編程、URL 解析的深刻理解。但在 HTTPS 無處不在、並發性能、安全合規要求提高的今天,它已完成歷史使命。拥抱 PSR-18 (HTTP Client)、PSR-17 (Factory)、PSR-7 (Message) 標準生態,才是可持續發展之道。

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