# 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.