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; // 重定向計數
}
統一入口,處理 GET/POST、HTTP/1.1 連接復用、響應頭解析、重定向遞歸:
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。
// 請求行
$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);
// 狀態行
$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);
}
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()` | 判斷狀態碼 2xx | bool |
| `IsText()` | 判斷 2xx 且 Content-Type 含 text/xml | bool |
| `FillUrl($rel)` | 相對 URL 轉絕對 (處理 ../、./、//、錨點) | string |
處理四類輸入:
// 核心邏輯片段
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 |
<?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();
}
`DedeHttpDown` 是 PHP 早期無 cURL 環境下的偉大發明,展現了對 HTTP 協議、Socket 編程、URL 解析的深刻理解。但在 HTTPS 無處不在、並發性能、安全合規要求提高的今天,它已完成歷史使命。拥抱 PSR-18 (HTTP Client)、PSR-17 (Factory)、PSR-7 (Message) 標準生態,才是可持續發展之道。