• WebSSH远程管理Linux服务器、Web终端窗口自适应(二)


    上一篇:Gin+Xterm.js实现WebSSH远程Kubernetes Pod

     

    • 支持用户名密码认证

    • 支持SSH密钥认证

    • 支持Web终端窗口自适应

    • 支持录屏审计

    #

    Go SSH#

    golang.org/x/crypto/ssh 是 Go 语言的一个库,它提供了 SSH(Secure Shell)协议的实现,可以用来构建 SSH 客户端和服务器。

    • 安装
    go get golang.org/x/crypto/ssh
    • SSH基本示例
    在创建SSH客户端之前,首先需要创建一个ClientConfig对象,其中包含了进行SSH通信所必须的配置信息。
    config := &ssh.ClientConfig{
        User: "username",
        Auth: []ssh.AuthMethod{
            ssh.Password("password"),
        },
        HostKeyCallback: ssh.InsecureIgnoreHostKey(),
    }

    在上述代码中,我们设置了用户名(User)、认证方式(Auth)和主机密钥回调(HostKeyCallback)。请注意,为了安全起见,在生产环境中不应使用InsecureIgnoreHostKey,而应使用更严格的主机密钥检查方式。

    然后可以使用 ssh.Dial 函数来创建一个 SSH 客户端连接:

    client, err := ssh.Dial("tcp", "localhost:22", config)
    if err != nil {
        log.Fatal("Failed to dial: ", err)
    }

    创建了 SSH 客户端连接之后,我们就可以使用它来执行远程命令。例如:

    session, err := client.NewSession()
    if err != nil {
        log.Fatal("Failed to create session: ", err)
    }
    defer session.Close()
    
    out, err := session.CombinedOutput("ls")
    if err != nil {
        log.Fatal("Failed to run command: ", err)
    }
    
    fmt.Println(string(out))

    在这里,我们首先使用 client.NewSession 方法创建了一个新的 SSH 会话。然后,我们使用 session.CombinedOutput 方法来执行远程命令并获取其输出。

    使用Gin、x/crypto/ssh 实现SSH#

    package main
    
    import (
     "encoding/json"
     "fmt"
     "github.com/gin-gonic/gin"
     "github.com/gorilla/websocket"
     "golang.org/x/crypto/ssh"
     "log"
     "net/http"
     "os"
    )
    
    const (
     // 输入消息
     messageTypeInput = "input"
     // 调整窗口大小消息
     messageTypeResize = "resize"
     // 密钥认证方式
     authTypeKey = "key"
     // 密码认证方式
     authTypePwd = "pwd"
    )
    
    // websocket 连接升级
    var upgrader = websocket.Upgrader{
     CheckOrigin: func(r *http.Request) bool {
      return true
     },
    }
    
    // WSClient WebSocket客户端访问对象,包含WebSocket连接对象和SSH会话对象
    type WSClient struct {
     // WebSocket 连接对象
     ws         *websocket.Conn
     sshSession *ssh.Session
    }
    
    // Message 用于解析从websocket接收到的json消息
    type Message struct {
     Type string `json:"type"`
     Cols int    `json:"cols"`
     Rows int    `json:"rows"`
     Text string `json:"text"`
    }
    
    // WSClient 的 Read 方法,实现了 io.Reader 接口,从 websocket 中读取数据。
    func (c *WSClient) Read(p []byte) (n int, err error) {
     // 从 WebSocket 中读取消息
     _, message, err := c.ws.ReadMessage()
     if err != nil {
      return 0, err
     }
     msg := &Message{}
     if err := json.Unmarshal(message, msg); err != nil {
      return 0, err
     }
    
     switch msg.Type {
     case messageTypeInput:
      // 如果是输入消息
      return copy(p, msg.Text), err
     case messageTypeResize:
      // 如果是窗口调整消息、调整窗口大小
      return 0, c.WindowChange(msg.Rows, msg.Cols)
     default:
      return 0, fmt.Errorf("invalid message type")
     }
    }
    
    // WindowChange 改变SSH Session窗口大小
    func (c *WSClient) WindowChange(rows, cols int) error {
     return c.sshSession.WindowChange(rows, cols)
    }
    
    // WSClient 的 Write 方法,实现了 io.Writer 接口,将数据写入 websocket。
    func (c *WSClient) Write(p []byte) (n int, err error) {
     // 将数据作为文本消息写入 WebSocket
     err = c.ws.WriteMessage(websocket.TextMessage, p)
     return len(p), err
    }
    
    // 建立SSH Client
    func sshDial(user, password, ip, authType string, port int) (*ssh.Client, error) {
     var authMethods []ssh.AuthMethod
     // 根据认证类型选择密钥或密码认证
     switch authType {
     case authTypeKey:
      privateKeyByte, err := os.ReadFile("./id_rsa")
      if err != nil {
       return nil, err
      }
      privateKey, err := ssh.ParsePrivateKey(privateKeyByte)
      if err != nil {
       return nil, err
      }
      authMethods = append(authMethods, ssh.PublicKeys(privateKey))
    
     case authTypePwd:
      authMethods = append(authMethods, ssh.Password(password))
     }
     // SSH client配置
     config := &ssh.ClientConfig{
      User:            user,
      Auth:            authMethods,
      HostKeyCallback: ssh.InsecureIgnoreHostKey(),
     }
     // 创建SSH client
     return ssh.Dial("tcp", fmt.Sprintf("%s:%d", ip, port), config)
    }
    
    // SSHHandler 处理SSH会话
    func SSHHandler(wsClient *WSClient, user, password, ip, authType, command string, port int) {
     // 创建SSH client
     sshClient, err := sshDial(user, password, ip, authType, port)
     if err != nil {
      log.Fatal(err)
     }
     defer sshClient.Close()
    
     // 创建SSH session
     session, err := sshClient.NewSession()
     if err != nil {
      log.Fatal(err)
     }
     defer session.Close()
    
     wsClient.sshSession = session
     // 设置终端类型及大小
     terminalModes := ssh.TerminalModes{
      ssh.ECHO:          1,
      ssh.TTY_OP_ISPEED: 14400,
      ssh.TTY_OP_OSPEED: 14400,
     }
     if err := session.RequestPty("xterm", 24, 80, terminalModes); err != nil {
      log.Fatal(err)
     }
     // 关联对应输入、输出流
     session.Stderr = wsClient
     session.Stdout = wsClient
     session.Stdin = wsClient
     // 在远程执行命令
     if err := session.Run(command); err != nil {
      log.Fatal(err)
     }
    
    }
    
    // Query 查询参数
    type Query struct {
     UserName string `form:"username" binding:"required"`
     Password string `form:"password"`
     IP       string `form:"ip" binding:"required"`
     Port     int    `form:"port" binding:"required"`
     AuthType string `form:"auth_type" binding:"required,oneof=key pwd"`
     Command  string `form:"command" binding:"required,oneof=sh bash"`
    }
    
    func main() {
     router := gin.Default()
     router.GET("/ssh", func(ctx *gin.Context) {
      var r Query
      // 绑定并校验请求参数
      if err := ctx.ShouldBindQuery(&r); err != nil {
       ctx.JSON(http.StatusBadRequest, gin.H{
        "err": err.Error(),
       })
       return
      }
      // 将 HTTP 连接升级为 websocket 连接
      ws, err := upgrader.Upgrade(ctx.Writer, ctx.Request, nil)
      if err != nil {
       log.Printf("Failed to upgrade connection: %v", err)
       return
      }
      // 开始处理 SSH 会话
      SSHHandler(&WSClient{
       ws: ws,
      }, r.UserName, r.Password, r.IP, r.AuthType, r.Command, r.Port,
      )
     })
    
     router.Run(":9191")
    }

    后端项目完整代码:https://gitee.com/KubeSec/webssh/tree/master/go-ssh

    使用vue-admin-template和Xterm.js实现Web终端#

    https://github.com/PanJiaChen/vue-admin-template

    https://github.com/xtermjs/xterm.js

    • 下载vue-admin-template项目

    https://github.com/PanJiaChen/vue-admin-template.git

    • 安装xterm.js及插件

    npm install
    npm install xterm
    npm install --save xterm-addon-web-links
    npm install --save xterm-addon-fit
    npm install -S xterm-style
    • 打开vue-admin-template项目,在src/views目录下新建目录ssh,在ssh目录下新建index.vue代码如下

    <template>
      <div class="app-container">
        
        <el-form ref="form" :model="form" :inline="true" label-width="120px">
          <el-form-item label="用户名"> 
            <el-input v-model="form.username" />
          el-form-item>
          <el-form-item label="密码"> 
            <el-input v-model="form.password" />
          el-form-item>
          <el-form-item label="IP"> 
            <el-input v-model="form.ip" />
          el-form-item>
          <el-form-item label="端口"> 
            <el-input v-model="form.port" />
          el-form-item>
          <el-form-item label="认证类型"> 
            <el-select v-model="form.auth_type" placeholder="认证类型">
              <el-option label="密钥" value="key" />
              <el-option label="密码" value="pwd" />
            el-select>
          el-form-item>
          <el-form-item label="Command"> 
            <el-select v-model="form.command" placeholder="bash">
              <el-option label="bash" value="bash" />
              <el-option label="sh" value="sh" />
            el-select>
          el-form-item>
          <el-form-item> 
            <el-button type="primary" @click="onSubmit">SSHel-button>
          el-form-item>
          <div id="terminal" /> 
        el-form>
      div>
    template>
    
    <script>
    import { Terminal } from 'xterm' // 导入 xterm 包,用于创建和操作终端对象
    import { common as xtermTheme } from 'xterm-style' // 导入 xterm 样式主题
    import 'xterm/css/xterm.css' // 导入 xterm CSS 样式
    import { FitAddon } from 'xterm-addon-fit' // 导入 xterm fit 插件,用于调整终端大小
    import { WebLinksAddon } from 'xterm-addon-web-links' // 导入 xterm web-links 插件,可以捕获 URL 并将其转换为可点击链接
    import 'xterm/lib/xterm.js' // 导入 xterm 库
    
    export default {
      data() {
        return {
          form: {
            username: 'root', // 默认命名空间为 "default"
            password: '123', // 默认 shell 命令为 "bash"
            command: 'bash', // 默认 shell 命令为 "bash"
            auth_type: 'pwd', // 默认容器名称为 "nginx"
            ip: '192.168.26.133',
            port: 22
          }
        }
      },
      methods: {
        onSubmit() {
          // 创建一个新的 Terminal 对象
          const xterm = new Terminal({
            theme: xtermTheme,
            rendererType: 'canvas',
            convertEol: true,
            cursorBlink: true
          })
    
          // 创建并加载 FitAddon 和 WebLinksAddon
          const fitAddon = new FitAddon()
          xterm.loadAddon(fitAddon)
          xterm.loadAddon(new WebLinksAddon())
    
          // 打开这个终端,并附加到 HTML 元素上
          xterm.open(document.getElementById('terminal'))
    
          // 调整终端的大小以适应其父元素
          fitAddon.fit()
    
          // 创建一个新的 WebSocket 连接,并通过 URL 参数传递 pod, namespace, container 和 command 信息
          const ws = new WebSocket('ws://127.0.0.1:9191/ssh?username=' + this.form.username + '&password=' + this.form.password + '&auth_type=' + this.form.auth_type + '&ip=' + this.form.ip + '&port=' + this.form.port + '&command=' + this.form.command)
    
          // 当 WebSocket 连接打开时,发送一个 resize 消息给服务器,告诉它终端的尺寸
          ws.onopen = function() {
            ws.send(JSON.stringify({
              type: 'resize',
              rows: xterm.rows,
              cols: xterm.cols
            }))
          }
    
          // 当从服务器收到消息时,写入终端显示
          ws.onmessage = function(evt) {
            xterm.write(evt.data)
          }
    
          // 当发生错误时,也写入终端显示
          ws.onerror = function(evt) {
            xterm.write(evt.data)
          }
    
          // 当窗口尺寸变化时,重新调整终端的尺寸,并发送一个新的 resize 消息给服务器
          window.addEventListener('resize', function() {
            fitAddon.fit()
            ws.send(JSON.stringify({
              type: 'resize',
              rows: xterm.rows,
              cols: xterm.cols
            }))
          })
    
          // 当在终端中键入字符时,发送一个 input 消息给服务器
          xterm.onData((b) => {
            ws.send(JSON.stringify({
              type: 'input',
              text: b
            }))
          })
        }
      }
    }
    script>
    
    <style scoped>
    .line{
      text-align: center;
    }
    style>
    • 在src/router/index.js文件中增加路由

    {
        path: '/ssh',
        component: Layout,
        children: [
          {
            path: 'ssh',
            name: 'SSH',
            component: () => import('@/views/ssh/index'),
            meta: { title: 'SSH', icon: 'form' }
          }
        ]
      },
    • 启动项目

    npm install
    npm run dev
    • 前端全部代码

    https://gitee.com/KubeSec/webssh/tree/master/webssh

    测试#

    • 生成SSH密码

    ssh-keygen -t rsa
    cd /root/.ssh/
    cp id_rsa.pub authorized_keys

    访问http://localhost:9528/#/ssh/ssh

    • 选择密钥连接

    • 用户名密码连接

     

  • 相关阅读:
    c++多态
    Python进阶篇:百度指数解密【抓包|JS逆向|数据区分】
    Odoo 15开发手册第八章 业务逻辑 - 业务流程的支持
    Springboot考研教材查询系统wi7pz计算机毕业设计-课程设计-期末作业-毕设程序代做
    Web前端开发技术课程大作业,期末考试HTML+CSS+JavaScript电竞游戏介绍网站
    分布式中的常见问题
    docker理论+部署(一)
    c++文件的打开、读写和关闭。缓冲区的使用和控制。
    计算机网络重点概念整理-第六章 应用层【期末复习|考研复习】
    Excel冻结窗格
  • 原文地址:https://www.cnblogs.com/0x00000/p/17557788.html