赞
踩
依赖包
"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>
采用的是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 }
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。