vue+go实现web端连接Linux终端
实现效果
实现逻辑1——vue
依赖包
"xterm": "^5.3.0",
"xterm-addon-attach": "^0.9.0",
"xterm-addon-fit": "^0.8.0"
样式和代码逻辑
<template>
<a-modal
v-model:visible="visible"
:title="$t(`routers.dom_system_terminal`)"
:footer="null"
@cancel="closeWs"
width="80%"
destroyOnClose
>
<div>
<div v-show="showForm" class="form-container">
<a-form :labelCol="{ span: 5 }" :wrapperCol="{ span: 15 }">
<a-form-item :label="$t('routers.table_address')" v-bind="validateInfos.server">
<a-input
:maxlength="60"
v-model:value="modelRef.server"
:placeholder="$t('routers.text_please_address')"
/>
</a-form-item>
<a-form-item :label="$t('routers.dom_username')" v-bind="validateInfos.user">
<a-input
:maxlength="60"
v-model:value="modelRef.user"
:placeholder="$t('routers.text_username')"
/>
</a-form-item>
<a-form-item :label="$t('routers.dom_pass')" v-bind="validateInfos.pwd">
<a-input-password
:maxlength="60"
autocomplete="new-password"
v-model:value="modelRef.pwd"
:placeholder="$t('routers.text_password')"
/>
</a-form-item>
<a-form-item :wrapper-col="{ offset: 5, span: 15 }">
<a-button @click="handleOk" type="primary">{{ $t("routers.dom_save") }}</a-button>
</a-form-item>
</a-form>
</div>
<div v-show="!showForm" style="height: 400px" ref="terminal" />
</div>
</a-modal>
</template>
<script lang="ts">
import { defineComponent, reactive, ref, onBeforeUnmount } from "vue";
import "xterm/css/xterm.css";
import { Terminal } from "xterm";
import { FitAddon } from "xterm-addon-fit";
import { AttachAddon } from "xterm-addon-attach";
import { system } from "@/api";
import { useI18n } from "vue-i18n";
import { Form } from "ant-design-vue";
export default defineComponent({
name: "TermModal",
setup() {
const visible = ref<boolean>(false);
const showForm = ref<boolean>(true);
const modelRef = reactive({
server: "",//带端口号输入
user: "",
pwd: "",
});
const { t } = useI18n();
const rulesRef = reactive({
server: [
{
required: true,
message: t("routers.text_please_address"),
},
],
user: [
{
required: true,
message: t("routers.text_username"),
},
],
pwd: [
{
required: true,
message: t("routers.text_password"),
},
],
});
const show = () => {
visible.value = true;
};
const data = reactive<any>({
term: null,
fitAddon: null,
socketUrl: "ws://" + window.location.host + "/ws", //这里正常应该是后端地址,但我这边前后端都是自己做的,打包以后的ip和端口相同
socket: "",
});
const terminal = ref();
const initTerm = () => {
// 1.xterm终端初始化
let height = document.body.clientHeight;
let rows: number = Number((height / 15).toFixed(0)); //18是字体高度,根据需要自己修改
data.term = new Terminal({
rows: rows,
});
// 2.webSocket初始化
data.socket = new WebSocket(data.socketUrl); // 带 token 发起连接
// 链接成功后
// 3.websocket集成的插件,这里要注意,网上写了很多websocket相关代码.xterm4版本没必要.
const attachAddon = new AttachAddon(data.socket);
data.fitAddon = new FitAddon(); // 全屏插件
attachAddon.activate(data.term);
data.fitAddon.activate(data.term);
data.term.open(terminal.value);
setTimeout(() => {
data.fitAddon.fit();
}, 5);
data.term.focus();
data.socket.onclose = () => {
//网络波动,ws连接断开
data.term && data.term.dispose();
showForm.value = true;
console.log("close socket");
};
data.socket.onmessage = (res: any) => {
//ssh连接失败返回
if (res && res.data && res.data.indexOf("失败") !== -1)
setTimeout(() => {
closeWs();
}, 3000);
};
window.addEventListener("resize", windowChange);
};
onBeforeUnmount(() => {
closeWs();
});
const windowChange = () => {
data.fitAddon.fit();
data.term.scrollToBottom();
};
const closeWs = () => {
resetFields();
data.socket && data.socket.close();
data.term && data.term.dispose();
window.removeEventListener("resize", windowChange);
showForm.value = true;
};
const useForm = Form.useForm;
const { validate, validateInfos, resetFields } = useForm(modelRef, rulesRef);
const handleOk = () => {
validate()
.then(() => {
system
.wsInfo({ server: modelRef.server, user: modelRef.user, pwd: modelRef.pwd })
.then(() => {
showForm.value = false;//连接ws,隐藏表单页
})
.catch((err: any) => {
console.log("error", err);
})
.finally(() => {
initTerm();
});
})
.catch((err: any) => {
console.log("error", err);
});
};
return {
show,
visible,
terminal,
closeWs,
validateInfos,
modelRef,
resetFields,
showForm,
handleOk,
};
},
});
</script>
<style lang="less">
.xterm-screen {
height: 100%;
}
</style>
<style lang="less" scoped>
.form-container {
background-color: black;
padding: 66px 12px 60px 12px;
::v-deep(.ant-form-item-label > label) {
color: white;
}
}
</style>
实现逻辑2——go
采用的是goframe框架
依赖包:
github.com/gogf/gf/v2 v2.5.4
github.com/gorilla/websocket v1.5.0 // indirect
main:
package main
import (
"foxess.ems/router"
"github.com/gogf/gf/v2/frame/g"
)
func main() {
s := g.Server()
router.Bind(s)
s.Run()
}
router:
package router
func Bind(s *ghttp.Server) {
s.Group("/", run)
}
func run(g *ghttp.RouterGroup) {
g.GET("/system/ws/info", system.WsInfo)
g.GET("/ws", system.ConnectWs)
}
system:
package system
import (
"fmt"
"foxess.ems/app/def"
"github.com/gogf/gf/v2/net/ghttp"
"github.com/gorilla/websocket"
"net/http"
)
var wsInfo = &def.ConnectWsArg{}
func WsInfo(r *ghttp.Request) {
res := &def.Response{}
args := &def.ConnectWsArg{}
if e := r.Parse(args); e != nil {
res.Errno = 40000
} else {
wsInfo = args
res.Result = &UploadResultParam{
Access: 1,
}
}
r.Response.WriteJson(res)
}
func ConnectWs(r *ghttp.Request) {
var upGrader = websocket.Upgrader{
ReadBufferSize: 1024,
WriteBufferSize: 1024,
CheckOrigin: func(r *http.Request) bool {
return true
},
}
ws, err := upGrader.Upgrade(r.Response.Writer, r.Request, nil)
if err != nil {
fmt.Println(err)
}
//延迟关闭ws连接
defer ws.Close()
def.SshBridgeHandler(ws, wsInfo)
}
ws文件
package def
import (
"bytes"
"fmt"
"github.com/gorilla/websocket"
"golang.org/x/crypto/ssh"
"io"
"log"
"sync"
"time"
)
type wsBufferWriter struct {
buffer bytes.Buffer
mu sync.Mutex
}
type XtermService struct {
stdinPipe io.WriteCloser
comboOutput *wsBufferWriter
session *ssh.Session
wsConn *websocket.Conn
}
// wsBufferWriter接口实现
func (w *wsBufferWriter) Write(p []byte) (n int, err error) {
w.mu.Lock()
defer w.mu.Unlock()
return w.buffer.Write(p)
}
func (w *wsBufferWriter) Bytes() []byte {
w.mu.Lock()
defer w.mu.Unlock()
return w.buffer.Bytes()
}
func (w *wsBufferWriter) Reset() {
w.mu.Lock()
defer w.mu.Unlock()
w.buffer.Reset()
}
type ConnectWsArg struct {
Server string `json:"server"`
User string `json:"user"`
Pwd string `json:"pwd"`
}
func SshBridgeHandler(ws *websocket.Conn, args *ConnectWsArg) {
// 创建 SSH 连接
config := &ssh.ClientConfig{
User: args.User,
Auth: []ssh.AuthMethod{
ssh.Password(args.Pwd),
},
HostKeyCallback: ssh.InsecureIgnoreHostKey(), // 注意:这会忽略对远程主机密钥的检查,不建议在生产环境中使用
}
client, err := ssh.Dial("tcp", args.Server, config)
if err != nil {
fmt.Println("Failed to dial: ", err)
err := ws.WriteMessage(websocket.TextMessage, []byte("\n第一步:ssh连接失败"+err.Error()))
if err != nil {
return
}
return
}
defer client.Close()
// 从SSH连接接收数据并发送到WebSocket
session, err := client.NewSession()
if err != nil {
err := ws.WriteMessage(websocket.TextMessage, []byte("\n第二步:ssh创建会话失败"+err.Error()))
if err != nil {
return
}
return
}
stdin, err := session.StdinPipe()
if err != nil {
log.Println(err)
return
}
defer stdin.Close()
wsBuffer := new(wsBufferWriter)
session.Stdout = wsBuffer
session.Stderr = wsBuffer
modes := ssh.TerminalModes{
ssh.ECHO: 1,
ssh.TTY_OP_ISPEED: 14400,
ssh.TTY_OP_OSPEED: 14400,
}
//伪造xterm终端
err = session.RequestPty("xterm", 100, 100, modes)
if err != nil {
err := ws.WriteMessage(websocket.TextMessage, []byte("第三步:会话伪造终端失败"+err.Error()))
if err != nil {
return
}
return
}
err = session.Shell()
if err != nil {
err := ws.WriteMessage(websocket.TextMessage, []byte("第四步:启动shell终端失败"+err.Error()))
if err != nil {
return
}
return
}
var xterm = &XtermService{
stdinPipe: stdin,
comboOutput: wsBuffer,
session: session,
wsConn: ws,
}
//defer session.Close()
quitChan := make(chan bool, 3)
//4.以上初始化信息基本结束.下面是携程读写websocket和ssh管道的操作.也就是信息通信
xterm.start(quitChan)
//session 等待
go xterm.Wait(quitChan)
<-quitChan
_, message, err := ws.ReadMessage()
_, err = stdin.Write(message)
if err != nil {
log.Println(err)
return
}
fmt.Println(string(message))
output, err := session.CombinedOutput(string(message))
err = ws.WriteMessage(websocket.TextMessage, output)
if err != nil {
return
}
}
func (s *XtermService) start(quitChan chan bool) {
go s.receiveWsMsg(quitChan)
go s.sendWsOutput(quitChan)
}
// 将客户端信息返回到
func (s *XtermService) sendWsOutput(quitChan chan bool) {
wsConn := s.wsConn
defer setQuit(quitChan)
ticker := time.NewTicker(time.Millisecond * time.Duration(60))
defer ticker.Stop()
for {
select {
case <-ticker.C:
if s.comboOutput == nil {
return
}
bytes := s.comboOutput.Bytes()
if len(bytes) > 0 {
wsConn.WriteMessage(websocket.TextMessage, bytes)
s.comboOutput.buffer.Reset()
}
case <-quitChan:
return
}
}
}
// 读取ws信息写入ssh客户端中.
func (s *XtermService) receiveWsMsg(quitChan chan bool) {
wsConn := s.wsConn
defer setQuit(quitChan) //告诉其他携程退出
for {
select {
case <-quitChan:
return
default:
//1.websocket 读取信息
_, data, err := wsConn.ReadMessage()
fmt.Println("===readMessage===", string(data))
if err != nil {
fmt.Println("receiveWsMsg=>读取ws信息失败", err)
return
}
//2.读取到的数据写入ssh 管道内.
s.stdinPipe.Write(data)
}
}
}
func (s *XtermService) Wait(quitChan chan bool) {
if err := s.session.Wait(); err != nil {
setQuit(quitChan)
}
}
func setQuit(quitChan chan bool) {
quitChan <- true
}