1 Star 3 Fork 3

某君 / MServer-terminal

加入 Gitee
与超过 1200万 开发者一起发现、参与优秀开源项目,私有仓库也完全免费 :)
免费加入
克隆/下载
贡献代码
同步代码
取消
提示: 由于 Git 不支持空文件夾,创建文件夹后会生成空的 .keep 文件
Loading...
README
MIT

从零开发一个webssh的后端

要开发一个webssh,需要对接两个对象,一个是用户,一个ssh服务器,我们需要做的是接收用户传入的输入,转发到ssh服务器上,并且将ssh的输出发送到前端页面进行展示

功能:

  1. ssh连接 √
  2. 使用rz sz命令进行上传下载 √
  3. 回放操作 √
  4. 实时查看操作
  5. 切断会话

web:

  1. 用户
    • 新增
    • 修改
    • 删除
    • 查询
  2. 资产
    • 新增
    • 修改
    • 删除
    • 查询
  3. 账号
    • 新增
    • 修改
    • 删除
    • 查询

前端技术栈:

  • Vue3
  • Vite2
  • NaiveUI
  • Xtermjs
  • Zmodemjs

后端技术栈:

  • golang1.17
  • gin

1. 对ssh的认识

SSH是一种网络协议,用于计算机之间的加密登录。如果一个用户从本地计算机,使用SSH协议登录另一台远程计算机,我们就可以认为,这种登录是安全的,即使被中途截获,密码也不会泄露。

最早的时候,互联网通信都是明文通信,一旦被截获,内容就暴露无疑。1995年,芬兰学者Tatu Ylonen设计了SSH协议,将登录信息全部加密,成为互联网安全的一个基本解决方案,迅速在全世界获得推广,目前已经成为Linux系统的标准配置。以上摘自廖雪峰博客

#### 1.1 go代码里的连接方式

golang 创建一个sshClient,并且通过client创建一个会话,并将会话跟远程pty进行绑定,会话有三个通道

  • stdin 向ssh发送数据的通道
  • stderr ssh报错输出的通道
  • stdout ssh正常输出的通道(包括命令执行错误 如不存在的命令其实是正常的输出)

正常情况下,我们只需要建立连接以后使用 stdin 向ssh会话发送数据,使用stdout、stderr 读取数据即可

首先我们需要使用Gin开启一个服务来监听请求:

package main

import (
	"fmt"
	"more_ssh/myssh01"
	"more_ssh/ssh"
	"more_ssh/util"

	"github.com/gin-gonic/gin"
)

func main() {
	r := gin.Default()
	r.Use(util.Cors()) //解决跨域问题
	r.GET("/myssh", myssh01.RunWebSSH)
	r.Run() //默认8080端口

}

新建一个文件夹 myssh01,在里面创建一个myssh.go文件

package myssh01

import (
	"bytes"
	"fmt"
	"io"
	"net/http"
	"sync"
	"time"

	"github.com/gin-gonic/gin"
	"github.com/gorilla/websocket"
	"golang.org/x/crypto/ssh"
)

// 定义一个结构体 方便保存各种连接信息
type MySSH struct {
	Websocket *websocket.Conn
	Stdin     io.WriteCloser
	Stdout    *wsBufferWriter
	Session   *ssh.Session
}

// 定义一个wsBufferWriter 并且写入时候加锁 防止stdout跟stderr同时写入
type wsBufferWriter struct {
	buffer bytes.Buffer
	mu     sync.Mutex
}

//定义write方法, 防止stdout跟stderr同时写入
func (w *wsBufferWriter) Write(p []byte) (int, error) {
	w.mu.Lock()
	defer w.mu.Unlock()
	return w.buffer.Write(p)
}

// 程序入口
func RunWebSSH(c *gin.Context) {
	mySSH := &MySSH{}

	// 1. 升级请求websocket
	upGrader := websocket.Upgrader{
		ReadBufferSize:  1024,
		WriteBufferSize: 1024 * 1024 * 10,
		CheckOrigin: func(r *http.Request) bool {
			return true
		},
		Subprotocols: []string{"webssh"},
	}

	webcon, err := upGrader.Upgrade(c.Writer, c.Request, nil)
	if err != nil {
		fmt.Println("升级http 为websoket失败:", err)
	}
	mySSH.Websocket = webcon // 将websocket连接保存到对象中

	// 创建一个ssh的配置
	config := &ssh.ClientConfig{
		Timeout:         time.Second * 10, //ssh 连接time out 时间一秒钟, 如果ssh验证错误 会在一秒内返回
		User:            "root",
		HostKeyCallback: ssh.InsecureIgnoreHostKey(), //这个可以, 但是不够安全
		//HostKeyCallback: hostKeyCallBackFunc(h.Host),
		Auth: []ssh.AuthMethod{ssh.Password("more@123")},
	}

	// 创建一个客户端
	sshClient, err := ssh.Dial("tcp", "162.14.109.53:22", config)
	if err != nil {
		fmt.Println(err)
		return
	}

	session, err := sshClient.NewSession()
	if err != nil {
		fmt.Println(err)
		return
	}
	mySSH.Session = session

	// 保存输入流
	mySSH.Stdin, err = session.StdinPipe()
	if err != nil {
		fmt.Println(err)
		return
	}

	//保存ssh输出流
	sshOut := new(wsBufferWriter)
	session.Stdout = sshOut
	session.Stderr = sshOut
	mySSH.Stdout = sshOut

	modes := ssh.TerminalModes{
		ssh.ECHO:          1,     // disable echo
		ssh.TTY_OP_ISPEED: 14400, // input speed = 14.4kbaud
		ssh.TTY_OP_OSPEED: 14400, // output speed = 14.4kbaud
	}
	// Request pseudo terminal
	if err := session.RequestPty("xterm", 30, 120, modes); err != nil {
		fmt.Println("绑定pty失败:", err)
		return
	}

	session.Shell()

	//执行远程命令
	go Send2SSH(mySSH)
	go Send2Web(mySSH)

}

// 读取websocket数据,发送到ssh输入流中
func Send2SSH(mySSh *MySSH) {
	for {
		//read websocket msg  需要通过msgType 判断是传输类型
		_, wsData, err := mySSh.Websocket.ReadMessage()
		if err != nil {
			fmt.Println("读取websocket数据失败:", err)
			return
		}
		_, err = mySSh.Stdin.Write(wsData)
		if err != nil {
			fmt.Println("ssh发送数据失败:", err)
		}
		// fmt.Println("ssh发送数据:", string(wsData))
	}

}

// 读取ssh输出,发送到websocket中
func Send2Web(mySSh *MySSH) {
	for {
		if mySSh.Stdout.buffer.Len() > 0 {
			err := mySSh.Websocket.WriteMessage(websocket.TextMessage, mySSh.Stdout.buffer.Bytes())
			fmt.Printf(string(mySSh.Stdout.buffer.Bytes()))
			if err != nil {
				fmt.Println("websocket发送数据失败:", err)
			}
			mySSh.Stdout.buffer.Reset() //读完清空
		}

	}
}

到此,一个简单的ssh后端就实现了

1.2 前端的实现

<template>
    <div>
        <div ref="terminalBox" style="height: 100vh;"></div>
    </div>
</template>

<script setup>
import 'xterm/css/xterm.css'
import { Terminal } from 'xterm'
import { FitAddon } from 'xterm-addon-fit'
import { ref, onMounted } from 'vue'
import { useMessage } from "naive-ui";

const message = useMessage();
let terminalBox = ref(null)
let term
let socket

onMounted(() => {
    //创建一个客户端
    term = new Terminal({
        rendererType: 'canvas', //使用这个能解决vim不显示或乱码
        cursorBlink: true,
        cursorStyle: "bar",
    })
    // term.write
    // 将客户端挂载到dom上
    const fitAddon = new FitAddon()
    term.loadAddon(fitAddon)
    term.open(terminalBox.value)
    fitAddon.fit()

    // 创建socket连接
    term.write('正在连接...\r\n');
    socket = new WebSocket('ws://127.0.0.1:8080/myssh')

    socket.binaryType = "arraybuffer";

    // 打开socket监听事件的方法
    socket.onopen = function () {
        fitAddon.fit()
        term.onData(function (data) {
            // socket.send(JSON.stringify({ type: "stdin", data: data }))
            socket.send(data)
            console.log(data)
        });
        message.success("会话成功连接!")
    }
    socket.onclose = function () {
        term.writeln('连接关闭');
    }
    socket.onerror = function (err) {
        // console.log(err)
        term.writeln('读取数据异常:', err);
    }
    // 接收数据
    socket.onmessage = function (recv) {
        try {
            term.write(recv.data)
        } catch (e) {
            console.log('unsupport data', recv.data)
        }
    }

    window.addEventListener("resize", () => {
        fitAddon.fit()
    }, false)

})


</script>

<style scoped>
.upload {
    min-height: 100px;
}
</style>

2.支持Zmodem 文件传输

golang逻辑分析:

  1. 下载逻辑分析:
    • 拿到输出的数据,判断当前是否是下载状态,是直接发给web
    • 否 查看数据包中是否含有下载的参数 是修改当前下载状态,直接发给web
    • 否 编辑成JSON代码发给前端
MIT License Copyright (c) 2022 某君 Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

简介

golang+Vue3+Vite2实现的webssh项目,支持rz、sz命令,支持审计回放 展开 收起
JavaScript 等 4 种语言
MIT
取消

发行版

暂无发行版

贡献者

全部

近期动态

加载更多
不能加载更多了
1
https://gitee.com/moujun/mserver-terminal.git
git@gitee.com:moujun/mserver-terminal.git
moujun
mserver-terminal
MServer-terminal
master

搜索帮助

53164aa7 5694891 3bd8fe86 5694891