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

PHP 實現 Telnet 客戶端:基於 fsockopen 的完整協議處理與命令交互類

     編輯:PHP 相關知識

背景

雖然 SSH 已取代 Telnet 成為主流,但在網路設備管理、舊系統整合、IoT 設備調試等場景,Telnet 仍不可或缺。PHP 內建 fsockopen 即可實現完整 Telnet 客戶端,無需擴展。

核心類:PHPTelnet

<?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)

安全與生產化建議

  1. 絕不在生產環境使用明文 Telnet——密碼、指令、輸出全程明文傳輸。必須在 VPN/專線/防火牆隔離網段內使用。
  2. 儲存憑證時使用加密配置檔或密碼管理器,禁止硬編碼。
  3. 加入命令白名單,禁止執行 reboot、format 等破壞性指令。
  4. 記錄審計日誌:時間、目標 IP、執行命令、操作帳號。
  5. 考慮改用 phpseclib 的 Net\SSH2,同樣純 PHP 實現,支援 SSH 2.0,安全性質變。

總結

PHPTelnet 類展示了 PHP 純 Socket 實現 Telnet 協議的完整流程:TCP 連線 → 選項協商 → 登入驗證 → 命令交互 → 連線關閉。理解其原理,有助於擴展至其他基於文本的協議(SMTP、POP3、IRC、自定義設備協議)。現代項目請優先選擇 SSH,僅在無法替換的舊系統整合時使用本類。

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