起因
最近在开发中涉及到了 webssh 的需求,于是就有了这篇文章,主要是记录一下开发过程中遇到的问题,以及解决方案。
技术架构
- Golang
- Fiber
- Vue
- xterm.js
方案
主要是希望前端通过 websocket 连接后端,后端通过 ssh 连接远程服务器,然后将两者的数据流进行转发,这样就实现了 webssh 的功能。
前端部分代码
<template>
<div id="xterm" class="xterm" />
</template>
<script>
import "xterm/css/xterm.css";
import { Terminal } from "xterm";
import { FitAddon } from "xterm-addon-fit";
import { AttachAddon } from "xterm-addon-attach";
export default {
name: "Terminal",
props: {
url: {
type: String,
default: "",
},
visible: {
type: Boolean,
default: false,
},
},
watch: {
visible: {
handler(value) {
if (
value &&
this.socket !== undefined &&
this.socket.readyState === 3
) {
this.initSocket();
}
},
},
wsUrl: {
handler(value) {
this.initSocket();
},
},
},
mounted() {
this.initSocket();
},
beforeDestroy() {
this.socket.close();
this.term.dispose();
},
methods: {
initTerm() {
const term = new Terminal({
fontSize: 14,
cursorBlink: true,
});
const attachAddon = new AttachAddon(this.socket);
const fitAddon = new FitAddon();
term.loadAddon(attachAddon);
term.loadAddon(fitAddon);
term.open(document.getElementById("xterm"));
fitAddon.fit();
term.focus();
this.term = term;
},
initSocket() {
this.socket = new WebSocket(this.url);
this.socketOnClose();
this.socketOnOpen();
this.socketOnError();
},
socketOnOpen() {
this.socket.onopen = () => {
// 链接成功后
this.initTerm();
};
},
socketOnClose() {
this.socket.onclose = () => {
this.$emit("terminalClose");
this.term.dispose();
};
},
socketOnError() {
this.socket.onerror = () => {
// console.log('socket 链接失败')
};
},
},
};
</script>
<style scoped>
.xterm {
height: 600px;
}
</style>
后端部分代码
package api
import (
"context"
"github.com/gofiber/websocket/v2"
"github.com/google/uuid"
"github.com/helloyi/go-sshclient"
log "github.com/sirupsen/logrus"
"gitlab.com/merico-dev/DevOpsPublic/brooder/db"
"gitlab.com/merico-dev/DevOpsPublic/brooder/services"
"golang.org/x/crypto/ssh"
"io"
)
type WsReaderWriter struct {
*websocket.Conn
}
func (w *WsReaderWriter) Write(p []byte) (n int, err error) {
writer, err := w.Conn.NextWriter(websocket.TextMessage)
if err != nil {
return 0, err
}
defer writer.Close()
return writer.Write(p)
}
func (w *WsReaderWriter) Read(p []byte) (n int, err error) {
var msgType int
var reader io.Reader
for {
msgType, reader, err = w.Conn.NextReader()
if err != nil {
return 0, err
}
if msgType != websocket.TextMessage {
continue
}
return reader.Read(p)
}
}
func Shell(c *websocket.Conn) {
uid, err := uuid.Parse(c.Params("uid"))
if err != nil {
log.Error(err)
}
mc := db.Client.Machine.GetX(context.Background(), uid)
service, err := services.NewSSHService(mc.MachineIP)
if err != nil {
log.Error(err)
}
config := &sshclient.TerminalConfig{
Term: "xterm",
Height: 40,
Weight: 80,
Modes: ssh.TerminalModes{
ssh.ECHO: 1,
ssh.TTY_OP_ISPEED: 14400,
ssh.TTY_OP_OSPEED: 14400,
},
}
rw := &WsReaderWriter{c}
if err = service.Client().Terminal(config).SetStdio(rw, rw, rw).Start(); err != nil {
log.Error(err)
}
}