vue+go实现web端连接Linux终端
实现效果
实现逻辑1——vue
依赖包
"xterm": "^5.3.0","xterm-addon-attach": "^0.9.0","xterm-addon-fit": "^0.8.0"
样式和代码逻辑
<template><a-modalv-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 mainimport ("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 = argsres.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 defimport ("bytes""fmt""github.com/gorilla/websocket""golang.org/x/crypto/ssh""io""log""sync""time"
)type wsBufferWriter struct {buffer bytes.Buffermu sync.Mutex
}
type XtermService struct {stdinPipe io.WriteClosercomboOutput *wsBufferWritersession *ssh.SessionwsConn *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连接接收数据并发送到WebSocketsession, 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 = wsBuffersession.Stderr = wsBuffermodes := 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.wsConndefer 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.wsConndefer setQuit(quitChan) //告诉其他携程退出for {select {case <-quitChan:returndefault://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
}