雖然 SSH 已取代 Telnet 成為主流,但在網路設備管理、舊系統整合、IoT 設備調試等場景,Telnet 仍不可或缺。PHP 內建 fsockopen 即可實現完整 Telnet 客戶端,無需擴展。
<?php
class PHPTelnet {
public int $showConnectError = 1;
public int $useUsleep = 0; // 1 = usleep(快),Windows 需 PHP 5+
public int $sleepTime = 125000; // 微秒
public int $loginSleepTime = 1000000;
private ?resource $fp = null;
private string $loginPrompt = '';
private string $conn1 = '';
private string $conn2 = '';
/** 返回碼:0=成功 1=連線失敗 2=未知主機 3=登入失敗 4=PHP版本過低 */
public function connect(string $server, string $user, string $pass): int {
// PHP 版本檢查(需 4.3.0+)
if (version_compare(PHP_VERSION, '4.3.0', '<')) {
return $this->connectError(4);
}
$this->disconnect();
// 解析主機
$ip = $server;
if (strlen($server) && preg_match('/[^0-9.]/', $server)) {
$ip = gethostbyname($server);
if ($ip === $server) return $this->connectError(2);
} elseif (!strlen($server)) {
$ip = '127.0.0.1';
}
// 建立 TCP 連線(Port 23)
if (!($this->fp = @fsockopen($ip, 23, $errno, $errstr, 10))) {
return $this->connectError(1);
}
// Telnet 選項協商(DO/DONT/WILL/WONT)
fwrite($this->fp, $this->conn1);
$this->sleep();
fwrite($this->fp, $this->conn2);
$this->sleep();
// 讀取登入提示
$this->getResponse($response);
$lines = explode("\n", $response);
$this->loginPrompt = $lines[count($lines) - 1] ?? '';
// 發送帳號密碼
fwrite($this->fp, "{$user}\n");
$this->sleep();
fwrite($this->fp, "{$pass}\n");
$this->useUsleep ? usleep($this->loginSleepTime) : sleep(1);
// 驗證登入結果
$this->getResponse($response);
$lines = explode("\n", $response);
$lastLine = $lines[count($lines) - 1] ?? '';
if ($lastLine === '' || $lastLine === $this->loginPrompt) {
$this->disconnect();
return $this->connectError(3);
}
return 0;
}
public function disconnect(bool $exit = true): void {
if ($this->fp) {
if ($exit) $this->doCommand('exit', $junk);
fclose($this->fp);
$this->fp = null;
}
}
/** 發送命令並獲取輸出(已去除首尾提示行) */
public function doCommand(string $cmd, string &$output): bool {
if (!$this->fp) return false;
fwrite($this->fp, "{$cmd}\n");
$this->sleep();
$this->getResponse($output);
// 移除首行(命令回顯)與尾行(新提示符)
$output = preg_replace('/^.*?\n(.*)\n[^\n]*$/', '$1', $output);
return true;
}
private function getResponse(string &$response): void {
$response = '';
do {
$response .= fread($this->fp, 1024);
$meta = stream_get_meta_data($this->fp);
} while ($meta['unread_bytes'] ?? false);
}
private function sleep(): void {
$this->useUsleep ? usleep($this->sleepTime) : sleep(1);
}
public function __construct() {
// Telnet 選項協商序列(RFC 854/855)
// conn1: 客戶端發起的選項協商
$this->conn1 = chr(0xFF).chr(0xFB).chr(0x1F) // WILL NAWS
. chr(0xFF).chr(0xFB).chr(0x20) // WILL TSPEED
. chr(0xFF).chr(0xFB).chr(0x18) // WILL TTYPE
. chr(0xFF).chr(0xFB).chr(0x27) // WILL NEW-ENVIRON
. chr(0xFF).chr(0xFD).chr(0x01) // DO ECHO
. chr(0xFF).chr(0xFB).chr(0x03) // WILL SUPPRESS-GO-AHEAD
. chr(0xFF).chr(0xFD).chr(0x03) // DO SUPPRESS-GO-AHEAD
. chr(0xFF).chr(0xFC).chr(0x23) // WONT LINEMODE
. chr(0xFF).chr(0xFC).chr(0x24) // WONT ENVIRON
. chr(0xFF).chr(0xFA).chr(0x1F).chr(0x00).chr(0x50).chr(0x00).chr(0x18).chr(0xFF).chr(0xF0) // SB NAWS 80x24
. chr(0xFF).chr(0xFA).chr(0x20).chr(0x00).chr(0x33).chr(0x38).chr(0x34).chr(0x30).chr(0x2C).chr(0x33).chr(0x38).chr(0x34).chr(0x30).chr(0xFF).chr(0xF0) // SB TSPEED
. chr(0xFF).chr(0xFA).chr(0x27).chr(0x00).chr(0xFF).chr(0xF0) // SB NEW-ENVIRON
. chr(0xFF).chr(0xFA).chr(0x18).chr(0x00).chr(0x58).chr(0x54).chr(0x45).chr(0x52).chr(0x4D).chr(0xFF).chr(0xF0); // SB TTYPE XTERM
// conn2: 伺服器回應確認
$this->conn2 = chr(0xFF).chr(0xFC).chr(0x01) // WONT ECHO
. chr(0xFF).chr(0xFC).chr(0x22) // WONT LINEMODE
. chr(0xFF).chr(0xFE).chr(0x05) // DONT STATUS
. chr(0xFF).chr(0xFC).chr(0x21); // WONT ENVIRON
}
private function connectError(int $num): int {
if ($this->showConnectError) {
$msg = match($num) {
1 => 'Unable to open network connection',
2 => 'Unknown host',
3 => 'Login failed',
4 => 'PHP version too low (need 4.3.0+)',
default => 'Unknown error',
};
echo "
[PHPTelnet] {$msg}
";
}
return $num;
}
}
<?php
require 'PHPTelnet.php';
$telnet = new PHPTelnet();
// 開啟 usleep 加速(Linux/Unix 建議)
$telnet->useUsleep = 1;
$telnet->sleepTime = 50000; // 50ms
$rv = $telnet->connect('192.168.1.1', 'admin', 'password');
if ($rv !== 0) {
exit("Connection failed with code: {$rv}");
}
$telnet->doCommand('show version', $output);
echo "=== show version ===\n{$output}\n";
$telnet->doCommand('show interfaces brief', $output);
echo "=== interfaces ===\n{$output}\n";
$telnet->disconnect();
| 項目 | 說明 |
|---|---|
| 協議協商 | 建構子預置 $conn1/$conn2 完成 NAWS、TSPEED、TTYPE、ECHO、SGA 等選項協商,兼容大多數 Cisco/Linux/BusyBox Telnetd |
| 讀取策略 | stream_get_meta_data() 檢查 unread_bytes,循環讀取直到緩衝區清空,避免截斷 |
| 命令輸出清洗 | 正則移除首行(命令回顯)與尾行(新 Prompt),保留純輸出內容 |
| 登入驗證 | 比對登入前後 Prompt,相同視為失敗(密碼錯誤或未獲 Shell) |
| 超時控制 | fsockopen 第 5 參數設定連線超時;讀取超時可用 stream_set_timeout($fp, $sec) |
reboot、format 等破壞性指令。phpseclib 的 Net\SSH2,同樣純 PHP 實現,支援 SSH 2.0,安全性質變。PHPTelnet 類展示了 PHP 純 Socket 實現 Telnet 協議的完整流程:TCP 連線 → 選項協商 → 登入驗證 → 命令交互 → 連線關閉。理解其原理,有助於擴展至其他基於文本的協議(SMTP、POP3、IRC、自定義設備協議)。現代項目請優先選擇 SSH,僅在無法替換的舊系統整合時使用本類。