Xterm+Golang+Websocket+Vue实现的webshell功能,包含操作审计

概述

使用xterm.js+socket技术实现一个网页端的webShell功能,完成shell连接操作,并记录用户的操作日志。

插件版本

这里使用的xtrem及相关插件版本如下:
"xterm": "^5.1.0", "xterm-addon-fit": "^0.7.0"
后端使用了https://github.com/mojocn/felix.git中封装好的socket操作请求
go get github.com/libragen/felix

前端采用的是Vue+Xtrem.js
后端使用golang(基于gin框架)

前端代码

<template>
  <div id="xterm" class="terminal" />
</template>

<script>
import 'xterm/css/xterm.css'
import { Terminal } from 'xterm'
import { FitAddon } from 'xterm-addon-fit'
import { Base64 } from 'js-base64'
export default{
  name: 'Shell',
  data() {
    return {
      socket: null,
      term: null,
      hostInfo: {
        ip: '',
        port: '',
        pass: ''
      }
    }
  },
  mounted(){
    // 这里执行获取要ssh连接的服务器信息的逻辑
    this.hostInfo.ip = '192.168.1.1'
    this.hostInfo.port = '22'
    this.hostInfo.pass = '123456'
    this.initTerm()
  },
  beforeDestroy(){
    if (this.socket !== null) {
      this.socket.close()
    }
    if (this.term !== null) {
      this.term.dispose()
    }
  },
  methods: {
    initTerm() {
      const term = new Terminal({
        frontSize: 14,
        lineHeight: 1.2,
        cursorBlink: false,
        cursorStyle: 'block', // 光标样式  null | 'block' | 'underline' | 'bar'
        scrollback: 800, // 回滚
        tabStopWidth: 8, // 制表宽度
        screenKeys: true
      })
      const fitAddon = new FitAddon()
      term.loadAddon(fitAddon)
      term.open(document.getElementById('xterm'))
      fitAddon.fit()
      this.cols = term.
      term.focus()
      this.term.onData(function(data) {
        g.socket.send(
          JSON.stringify({
            type: 'cmd',
            cmd: Base64.encode(data)
          })
        )
      })
    },
    initSocket() {
      this.socket = new WebSocket('ws://localhost:8888/shell/' + this.hostInfo.ip + '/' + this.hostInfo.port + '/' + this.hostInfo.pass + '/' + this.term.cols + '/' + this.term.rows)
      this.socket.onopen = () => {
        //这边要用\r而不是\n,\n会导致下一行有空格 
        this.term.write('连接已建立\r')
      }
      this.socket.onclose = () => {
        this.term.write('连接已断开\n')
      }
      this.socket.onmessage = (ev) => {
        this.term.write(ev.data)
      }
    }
  }
}
</script>
<style scoped>
.terminal {
  width: 100%;
  height: 100%;
  font-family: 'Courier New', Courier, monospace;
  overflow: hidden;
}
</style>

后端代码

接收请求

func Routers(r *gin.RouterGroup) {
  // socket请求到后端默认是GET类型的,这边需要使用GET来接收
  r.GET("/shell/:ip/:port/:pass/:cols/:rows", Shell)
}

处理连接的逻辑(shell.go)

import (
  "github.com/libragen/felix/util"
  "strconv"
)
func Shell(c *gin.Context){
    var (
      row int
      col int
      err errors
    )
    ip := c.Param("ip")
    port := c.Param("port")
    pass := c.Param("pass")
    rowStr := c.Param("rows")
    colStr := c.Param("cols")
    rows, err = strconv.Atoi(rowStr)
    cols, err = strconv.Atoi(colStr)
    startTime := time.Now()
    // 创建ssh连接客户端
    conn := ssh.NewSSHClientByPass(user, pass, ip, port)
    // 建立ssh连接
    err = conn.ConnectByPass()
    ssConn, err := util.NewSshConn(cols, rows, conn.GetClient())
    defer ssConn.Close()
    wsConn, err := upgrader.Upgrade(c.Writer, c.Request, nil)
    defer wsConn.Close()
    quitChan := make(chan bool, 3)

    var logBuff = new(bytes.Buffer)
    go ssConn.ReceiveWsMsg(wsConn, logBuff, quitChan)
    go ssConn.SendComboOutput(wsConn, quitChan)
    go ssConn.SessionWait(quitChan)
    <-quitChan

    //写入操作日志(这里就是基于gorm的写数据库操作了,具体的字段就不列出来了,可以根据自己的需要进行修改)
    var shellLog ShellLog
    jsons := make(map[string]interface{}, 1)
    shellLog.ShellType = "主机连接(密码模式)"
    shellLog.ConnIp = ip
    shellLog.ConnUser = user
    shellLog.ConnEndTime = time.Now()
    shellLog.ConnStartTime = startTime
    jsons["command"] = logBuff.Bytes()
    newJson, _ := json.Marshal(jsons)
    shellLog.Cmd = newJson
    if logBuff.Bytes() != nil {
        
        _, errs := shellLog.Insert()
        if errs != nil {
			log.Errorf("写入shell日志时发生异常:%v", errs)
		}
    }
}

客户端连接的代码(ssh.go)

import (
    "github.com/pkg/sftp"
	"github.com/spf13/viper"
	"golang.org/x/crypto/ssh"
)
type Client struct {
	user           string
	pwd            string
	ip             string
	port           string
	sshClient      *ssh.Client
	sftpClient     *sftp.Client
}

func NewSSHClientByPass(user, pwd, ip, port string) Client {
    return Client{
      user:    user,
      pwd:     pwd,
      ip:      ip,
      port:    port,
    }
}

func (c *Client) ConnectByPass error {
    config := c.getConfigByPass()
    client, err := ssh.Dial("tcp", c.ip+":"+c.port, config)
    if err != nil {
      return fmt.Errorf("创建ssh连接失败: %w", err)
    }
    sftp, err := sftp.NewClient(client)
    if err != nil {
      return fmt.Errorf("创建sftp连接失败: %w", err)
    }
    c.sshClient = client
    c.sftpClient = sftp
    return nil
}

func (c *Client) getConfigByPass() *ssh.ClientConfig {
	config := &ssh.ClientConfig{
		User: c.user,
		Auth: []ssh.AuthMethod{
			ssh.Password(c.pwd),
		},
		Timeout:         30 * time.Second,
		HostKeyCallback: ssh.InsecureIgnoreHostKey(),
	}

	return config
}

后端ssh与xterm的交互具体的逻辑操作可以参考这篇文章,里面有详细的解读
https://mojotv.cn/2019/05/27/xtermjs-go

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值