# MServer-terminal
**Repository Path**: moujun/mserver-terminal
## Basic Information
- **Project Name**: MServer-terminal
- **Description**: golang+Vue3+Vite2实现的webssh项目,支持rz、sz命令,支持审计回放
- **Primary Language**: Unknown
- **License**: MIT
- **Default Branch**: master
- **Homepage**: None
- **GVP Project**: No
## Statistics
- **Stars**: 5
- **Forks**: 3
- **Created**: 2022-01-12
- **Last Updated**: 2024-09-11
## Categories & Tags
**Categories**: Uncategorized
**Tags**: None
## README
## 从零开发一个webssh的后端
要开发一个webssh,需要对接两个对象,一个是用户,一个ssh服务器,我们需要做的是接收用户传入的输入,转发到ssh服务器上,并且将ssh的输出发送到前端页面进行展示
功能:
1. ssh连接 √
2. 使用rz sz命令进行上传下载 √
3. 回放操作 √
4. 实时查看操作
5. 切断会话
web:
1. 用户
- 新增
- 修改
- 删除
- 查询
2. 资产
- 新增
- 修改
- 删除
- 查询
3. 账号
- 新增
- 修改
- 删除
- 查询
4.
**前端技术栈:**
- Vue3
- Vite2
- NaiveUI
- Xtermjs
- Zmodemjs
**后端技术栈:**
- golang1.17
- gin
### 1. 对ssh的认识
SSH是一种网络协议,用于计算机之间的加密登录。如果一个用户从本地计算机,使用SSH协议登录另一台远程计算机,我们就可以认为,这种登录是安全的,即使被中途截获,密码也不会泄露。
最早的时候,互联网通信都是明文通信,一旦被截获,内容就暴露无疑。1995年,芬兰学者Tatu Ylonen设计了SSH协议,将登录信息全部加密,成为互联网安全的一个基本解决方案,迅速在全世界获得推广,目前已经成为Linux系统的标准配置。[以上摘自廖雪峰博客](https://www.ruanyifeng.com/blog/2011/12/ssh_remote_login.html)
#### 1.1 go代码里的连接方式
golang 创建一个sshClient,并且通过client创建一个会话,并将会话跟远程pty进行绑定,会话有三个通道
- stdin 向ssh发送数据的通道
- stderr ssh报错输出的通道
- stdout ssh正常输出的通道(包括命令执行错误 如不存在的命令其实是正常的输出)
正常情况下,我们只需要建立连接以后使用 stdin 向ssh会话发送数据,使用stdout、stderr 读取数据即可
首先我们需要使用Gin开启一个服务来监听请求:
```go
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文件
```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 前端的实现
```vue
```
### 2.支持Zmodem 文件传输
golang逻辑分析:
1. 下载逻辑分析:
- 拿到输出的数据,判断当前是否是下载状态,是直接发给web
- 否 查看数据包中是否含有下载的参数 是修改当前下载状态,直接发给web
- 否 编辑成JSON代码发给前端
2.