vue+go实现web端连接Linux终端

3 篇文章 0 订阅

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
}

  • 14
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值