Stories

Detail Return Return

WebSSH的簡單實現 - Stories Detail

今天我們來看WebSSH的簡單實現。

因為web的便利性,很多傳統功能都有了web端的實現,WebSSH就是其中之一,我是第一次接觸,所以來記錄一下使用。

WebSSH支持終端交互,主要可以分為兩部分,第一是頁面輸入命令行並傳遞給遠程終端,第二是展示命令執行結果,這兩部分現在都已經有具體實現的庫了,所以我們只需要把它們組合起來。

在具體實現之前,需要先準備一個遠程終端,我這裏用的是VMware創建的虛擬機

可以在Mac的終端直接登錄

接下來我們就來看代碼的實現,前端頁面使用三方庫xtermjs實現終端界面,遠程連接使用nodejs的ssh2模塊。

前端實現

我們先來看web端的實現。

前端代碼主要做三件事,第一初始化終端對象terminal,第二增加監聽事件監聽用户的輸入,第三建立web socket連接實現實時交互。我這裏用react項目做簡單的演示。

先在頁面上準備一個div,模擬終端背景。

import React, {useEffect, useRef, useState} from 'react';
import { Terminal } from 'xterm';

import 'xterm/css/xterm.css';

const FontSize = 14;
const Col = 80;

const WebTerminal = () => {
  const terminalRef = useRef(null);
  const webTerminal = useRef(null);
  const ws = useRef(null);

  useEffect(() => {
    const ele = terminalRef.current;
    if (ele) {

    }

  }, [terminalRef.current]);

  return <div ref={terminalRef}  style={{ backgroundColor: '#000', width: '100vw', height: '100vh' }}/>;
};

export default WebTerminal;

然後我們對終端進行初始化。

import React, {useEffect, useRef, useState} from 'react';
import { Terminal } from 'xterm';

import 'xterm/css/xterm.css';

const FontSize = 14;
const Col = 80;

const WebTerminal = () => {
  const terminalRef = useRef(null);
  const webTerminal = useRef(null);
  const ws = useRef(null);

  useEffect(() => {
    const ele = terminalRef.current;
    if (ele && !webTerminal.current) {
      const height = ele.clientHeight; // +
      const terminal = new Terminal({ // +
        cursorBlink: true, // +
        cols: Col, // +
        rows: Math.ceil(height / FontSize), // +
      }); // +
             // +
      terminal.open(ele); // +
      // +
      webTerminal.current = terminal; // +
      
    }

  }, [terminalRef.current]);

  return <div ref={terminalRef}  style={{ backgroundColor: '#000', width: '100vw', height: '100vh' }}/>;
};

export default WebTerminal;

這個時候可以看到頁面上出現了一個閃爍的光標,就像input輸入框被聚焦時候的狀態,cols屬性指定了一行可以輸入的字符數,rows指定了展示的行數,這裏做了一個簡單的取整的計算。這個時候還不能輸入內容,因為還沒加上事件監聽,那麼現在我們給它加上。

import React, {useEffect, useRef, useState} from 'react';
import { Terminal } from 'xterm';

import 'xterm/css/xterm.css';

const FontSize = 14;
const Col = 80;

const WebTerminal = () => {
  const terminalRef = useRef(null);
  const webTerminal = useRef(null);
  const ws = useRef(null);

  useEffect(() => {
    const ele = terminalRef.current;
    if (ele && !webTerminal.current) {
      const height = ele.clientHeight;
      const terminal = new Terminal({
        cursorBlink: true,
        cols: Col,
        rows: Math.ceil(height / FontSize),
      });

      terminal.open(ele);
      
      webTerminal.current = terminal;

      terminal.onData((val) => { // 鍵盤輸入 // +
        if (val === '\x03') { // +
          // nothig todo // +
        } else { // +
          terminal.write(val); // +
        } // +
      }); // +
    }

  }, [terminalRef.current]);

  return <div ref={terminalRef}  style={{ backgroundColor: '#000', width: '100vw', height: '100vh' }}/>;
};

export default WebTerminal;

這裏我們利用terminal對象的onData方法對用户輸入進行監聽,然後調用terminal的write方法將內容輸出到頁面上,這裏因為x03代表了Ctrl+C的組合鍵,所以把它做了過濾。這個時候你可能會發現一個問題,就是我們點擊回退鍵刪除內容的時候,控制枱會出現報錯,這是因為編碼的問題,onData返回的是utf16/ucs2編碼的內容,需要轉換為utf8編碼,這個後面我們交給node端去處理。

最後我們來實現web socket的實時交互,這塊因為需要建立web socket鏈接,需要web和node一起配合實現。

Webscoket實現

我們先寫node端的代碼。

const express = require('express');
const app = express();
const expressWs = require('express-ws')(app);

import { createNewServer } from './utils/createNewServer';


app.get('/', function (req: any, res: any, next: any) {
  res.end();
});

app.ws('/', function (ws: any, req: any) {
  createNewServer({
    host: '172.16.215.129',
    username: 'root',
    password: '123456'
  }, ws);
});

app.listen(3001)

這裏我們使用express-ws模塊來實現socket通信,監聽3001端口,接下來我們主要看createNewServer方法的實現。

首先引入ssh2模塊,構造一個ssh客户端,並與遠程主機、也就是我前面創建的虛擬機建立連接。

const SSHClient = require('ssh2').Client;
const utf8 = require('utf8');

const termCols = 80;
const termRows = 30;

export const createNewServer = (machineConfig: any, socket: any) => {
  const ssh = new SSHClient(); // +
  const { host, username, password } = machineConfig; // +
   // +
  ssh.connect({ // +
    port: 22, // +
    host, // +
    username, // +
    password, // +
  }).on('ready', function() { // +
    console.log('ssh連接已建立'); // +
  }) // +
}

這裏端口22是SSH提供遠程連接服務的默認端口,當ssh連接建立成功後,就會打印出”ssh連接已建立“。現在我們到web端去創建web socket連接來查看效果。

import React, {useEffect, useRef, useState} from 'react';
import { Terminal } from 'xterm';

import 'xterm/css/xterm.css';

const FontSize = 14;
const Col = 80;

const WebTerminal = () => {
  const terminalRef = useRef(null);
  const webTerminal = useRef(null);
  const ws = useRef(null);

  useEffect(() => {
    const ele = terminalRef.current;
    if (ele && !webTerminal.current) {
      const height = ele.clientHeight;
      const terminal = new Terminal({
        cursorBlink: true,
        cols: Col,
        rows: Math.ceil(height / FontSize),
      });

      terminal.open(ele);
      
      webTerminal.current = terminal;

      terminal.onData((val) => { // 鍵盤輸入
        if (val === '\x03') {
          // nothig todo
        } else {
          terminal.write(val);
        }
      });

      const socket = new WebSocket(`ws://127.0.0.1:3001`); // +
      socket.onopen = () => { // +
        socket.send('connect success'); // +
      }; // +
      ws.current = socket; // +
    }

  }, [terminalRef.current]);

  return <div ref={terminalRef}  style={{ backgroundColor: '#000', width: '100vw', height: '100vh' }}/>;
};

export default WebTerminal;

這個時候我們去刷新頁面,就可以看到nodejs的控制枱打印出了”ssh連接已建立“這句話。

與遠程主機建立連接後,我們就可以使用ssh2客户端的shell方法,與主機終端開啓交互。

const SSHClient = require('ssh2').Client;
const utf8 = require('utf8');

const termCols = 80;
const termRows = 30;

export const createNewServer = (machineConfig: any, socket: any) => {
  const ssh = new SSHClient();
  const { host, username, password } = machineConfig;

  ssh.connect({
    port: 22,
    host,
    username,
    password,
  }).on('ready', function() {
    console.log('ssh連接已建立');

    ssh.shell({ // +
      cols: termCols, // +
      rows: termRows, // +
    }, function(err: any, stream: any) { // +
      if (err) { // +
        return socket.send('\r\n*** SSH SHELL ERROR: ' + err.message + ' ***\r\n');
      } // +
      console.log('開啓交互'); // +
    }); // +
  })
}

此時nodejs的控制枱就打印出了”開啓交互“這句話,stream用於控制終端的輸入輸出。現在我們需要在web和nodejs兩端都加上對消息的監聽和發送,這樣才能開始真正的交互,我們就接着先寫node端的監聽和發送。

在node端接收到socket消息後,用on-message對前端傳遞過來的內容進行編碼轉換處理,並轉換為原始字節流寫入終端。

const SSHClient = require('ssh2').Client;
const utf8 = require('utf8');

const termCols = 80;
const termRows = 30;

export const createNewServer = (machineConfig: any, socket: any) => {
  const ssh = new SSHClient();
  const { host, username, password } = machineConfig;

  ssh.connect({
    port: 22,
    host,
    username,
    password,
  }).on('ready', function() {
    console.log('ssh連接已建立');

    ssh.shell({
      cols: termCols,
      rows: termRows,
    }, function(err: any, stream: any) {
      if (err) {
        return socket.send('\r\n*** SSH SHELL ERROR: ' + err.message + ' ***\r\n');
      }
      console.log('開啓交互');
      socket.on('message', function (data: any) { // +
        stream.write(Buffer.from(data, 'utf8')); // +
      }); // +
      // +
      stream.on('data', function (d: Buffer) { // +
        socket.send(d.toString('binary')); // +
      }); // +
    });
  })
}

同時監聽終端的輸出,對前端輸入的內容進行處理,並通過socket連接返回給前端。toString binary表示保持原始字節,避免utf8解碼異常。

接着我們來完成web端對socket的處理,首先把原來的用户輸入顯示到屏幕上改為發送socket消息。

terminal.onData((val) => { // 鍵盤輸入
  if (val === '\x03') {
    // nothig todo
  } else {
    socket.send(val); // M
  }
});

並增加對socket消息的監聽。

socket.onmessage = e => {
  terminal.write(e.data);
};

將socket返回的消息輸出到頁面模擬的終端容器上。

這個時候我們刷新頁面看到如下圖所示,就表示我們實現了最基本的交互功能。

這樣就可以愉快地和遠程終端開始交互了。

到這裏我們還可以簡單優化一下,就是socket連接斷開後,如果不退出終端,會在遠程主機保留很多進程,我們可以用這個命令ps -aux | grep ssh看到。

為了避免佔用內存,我們可以對socket的關閉進行監聽,在socket連接關閉的時候調用end方法來結束ssh連接服務。

socket.onclose = (event: any) => {
  ssh.end();
}

這樣我們再去查看的時候,就會看到只剩下一個正在運行的ssh服務。

到這裏我們就實現了一個簡單的WebSSH的交互了。當然這個例子比較簡單,類似瀏覽器窗口尺寸變化對輸出顯示的影響這裏也沒有處理,以及node端ssh2模塊的其他功能方法也沒有涉及到,感興趣的同學們可以去查閲文檔自己嘗試一下。

user avatar linx Avatar assassin Avatar sy_records Avatar minnanitkong Avatar _58d8892485f34 Avatar nanchengfe Avatar chenchaoyang666 Avatar willemwei Avatar shawnphang Avatar o2team Avatar ning_643b67be37ac3 Avatar chen_6016206b422ca Avatar
Favorites 13 users favorite the story!
Favorites

Add a new Comments

Some HTML is okay.