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

news2024/11/24 22:50:57

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
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1873812.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

《分析模式》漫谈08-单继承不是“唯一继承”

DDD领域驱动设计批评文集 做强化自测题获得“软件方法建模师”称号 《软件方法》各章合集 《分析模式》第2章这一段&#xff1a; 划线处的single inheritance&#xff0c;2004中译本的翻译&#xff1a; 翻译为“单继承”&#xff0c;是正确的。 2020中译本的翻译&#xff1a…

【人工智能】—XGBoost、CatBoost、LightGBM算法构建信用卡欺骗识别模型

引言 在金融领域&#xff0c;信用卡欺诈行为一直是银行和金融机构面临的一大挑战。随着电子商务的快速发展&#xff0c;信用卡欺诈事件的数量和复杂性都在不断增加。据统计&#xff0c;全球每年因信用卡欺诈造成的损失高达数十亿美元。因此&#xff0c;开发有效的欺诈检测系统…

i-Health

技术栈&#xff1a;HTMLCSSJavascriptPHP

同步模式之保护性暂停模式

1. Guarded Suspension&#xff1a;一个线程需要等待另一个线程的执行结果 2. 理解 一个线程需要将结果传递给另一个线程&#xff0c;将这两个线程关联到到同一个 GuardedObject 如果需要源源不断地传递结果&#xff0c;需要使用消息队列&#xff08;生产者-消费者模型&…

SpringBoot(二)SpringBoot多环境配置

Spring框架常用注解简单介绍 SpringMVC常用注解简单介绍 SpringBoot&#xff08;一&#xff09;创建一个简单的SpringBoot工程 SpringBoot&#xff08;二&#xff09;SpringBoot多环境配置 SpringBoot&#xff08;三&#xff09;SpringBoot整合MyBatis SpringBoot&#xff08;四…

met和set的特性及区别

1、关联式容器 在c初阶阶段&#xff0c;我们已经接触了STL的部分容器&#xff0c;比如&#xff1a;vector,list,deque&#xff0c;forward_list等。 这些容器统称为序列式容器&#xff0c;因为其底层为线性序列的数据结构&#xff0c;里面存储的就是数据本身。 而关联式容器…

[论文阅读笔记32] Object-Centric Multiple Object Tracking (ICCV2023)

最近Object centric learning比较火, 其借助了心理学的概念, 旨在将注意力集中在图像或视频中的独立对象&#xff08;objects&#xff09;上&#xff0c;而不是整个图像。这个方法与传统的基于像素或区域的方法有所不同&#xff0c;它试图通过识别和分离图像中的各个对象来进行…

详细的介绍匀加速运动的物理方程是如何转化为卡尔曼滤波的状态空间模型的

详细的介绍匀加速运动的物理方程是如何转化为卡尔曼滤波的状态空间模型的 flyfish 加速度是描述物体速度变化快慢的物理量&#xff0c;定义为速度对时间的变化率。数学上&#xff0c;它表示为&#xff1a; a Δ v Δ t a \frac{\Delta v}{\Delta t} aΔtΔv​ 其中&#xf…

[图解]SysML和EA建模住宅安全系统-02-现有运营领域-块定义图

1 00:00:00,840 --> 00:00:02,440 首先我们来看画在哪里 2 00:00:02,570 --> 00:00:08,310 你看&#xff0c;这是图的类型&#xff0c;图里面内容 3 00:00:08,320 --> 00:00:10,780 这是元素类型 4 00:00:10,790 --> 00:00:14,900 这是位置&#xff0c;哪个包 …

中国计量大学2024年成人高等继续教育招生简章

中国计量大学&#xff0c;作为一所享有盛誉的高等学府&#xff0c;始终秉持着“精益求精&#xff0c;追求卓越”的办学理念&#xff0c;致力于为社会培养各类优秀人才。在2024年&#xff0c;我校继续秉承这一传统&#xff0c;全面启动成人高等继续教育招生工作&#xff0c;为广…

KVB App:金价看涨动能不足,关注美国PCE数据

当前市场状况 截至2024年6月28日&#xff0c;现货黄金价格震荡走弱&#xff0c;一度失守2320美元/盎司关口&#xff0c;目前交投于2322.65美元/盎司附近。KVB首席分析师Valeria Bednarik指出&#xff0c;黄金价格目前缺乏看涨动能&#xff0c;市场焦点转向美国个人消费支出(PC…

python中pip换源

目录 1. 背景2. Python 的 pip 换源2.1 临时换源&#xff08;命令行中使用参数&#xff09;2.2 永久换源&#xff08;修改配置文件&#xff09;2.2.1 Windows系统2.2.2 Linux/macOS系统 2.3 使用 pip-config 命令换源&#xff08;Linux/macOS 特定&#xff09; 3. 常用的 PyPI …

嵌入式Linux系统编程 — 4.7 regcomp、regexec、regfree正则表达式函数

目录 1 为什么需要正则表达式 2 正则表达式简介 3 正则表达式规则 4 regcomp、regexec、regfree函数 4.1 函数介绍 4.2 URL格式案例 1 为什么需要正则表达式 在许多的应用程序当中&#xff0c; 有这样的应用场景&#xff1a; 给定一个字符串&#xff0c;检查该字符串是否…

数字信号处理实验三(IIR数字滤波器设计)

IIR数字滤波器设计&#xff08;2学时&#xff09; 要求&#xff1a; 产生一复合信号序列&#xff0c;该序列包含幅度相同的28Hz、50Hz、100Hz、150Hz的单音&#xff08;单频&#xff09;信号&#xff1b;其中&#xff0c;50Hz及其谐波为工频干扰&#xff08;注&#xff1a;采样…

Git与GitLab的企业实战--尚硅谷git课程

Git与GitLab的企业实战 第1章 Git概述 Git是一个免费的、开源的分布式版本控制系统&#xff0c;可以快速高效地处理从小型到大型的各种项目。 Git易于学习&#xff0c;占地面积小&#xff0c;性能极快。 它具有廉价的本地库&#xff0c;方便的暂存区域和多个工作流分支等特性…

揭秘系统架构:从零开始,探索技术世界的无限可能

文章目录 引言一、系统架构的基本概念二、系统架构的设计原则模块化可扩展性高可用性安全性 三、常见的系统架构模式1. **分层架构&#xff08;Layered Architecture&#xff09;**&#xff1a;2. **微服务架构&#xff08;Microservices Architecture&#xff09;**&#xff1…

微信视频号里面的视频怎么下载,分享4个视频号视频下载方法!可长期使用

如何在微信视频号里下载视频,虽然互联网上微信视频号视频下载方法千千万&#xff0c;奈何总有一些方法不起任何作用. 如何解决这一问题&#xff0c;今天就分享3个可以下载微信视频号的视频方法仅供参考。 1:提取器助手 手机搜索提取器助收/扫码获取视频号下载小助手二维码。该…

G882磁力仪拖鱼位置是如何计算的?

根据参考文献&#xff0c;磁力仪拖鱼位置计算有两种方法&#xff1a; 1、直线法 直线计算法是假设不考虑海流、船摆等动态因素的影响&#xff0c;拖鱼与拖点始终和航向相同&#xff0c;即整个拖拽系统与船舶是刚性连接。 2、曲线法 实际海洋磁力测量中&#xff0c;在海风、海…

6.24.4.2 YOLO- logo:一种基于变压器的YOLO分割模型,用于数字乳房x光片中乳腺肿块的检测和分割

背景与目的:数字化乳房x光片的肿块检测和分割在乳腺癌的早期发现和治疗中起着至关重要的作用。此外&#xff0c;临床经验表明&#xff0c;它们是乳腺病变病理分类的上游任务。深度学习的最新进展使分析更快、更准确。本研究旨在开发一种用于乳房x线摄影的乳腺癌质量检测和分割的…

柯桥在职学历提升|专科本科之自考本科哪些专业不考数学

一、管理类专业 这类专业综合性和理论性比较强&#xff0c;除了涉及到管理学相关的理论知识外&#xff0c;还有相应的专业知识&#xff0c;目前比较典型的专业有&#xff1a;行政管理、人力资源管理、工商管理&#xff08;现代企业管理&#xff09;、工商管理&#xff08;商务管…