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

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
}

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

短视频矩阵系统:打造品牌影响力的新方式

一、短视频矩阵概念 短视频营销革命&#xff1a;一站式解决策略&#xff01;短视频矩阵系统是一款专为企业营销设计的高效工具&#xff0c;旨在通过整合和优化众多短视频平台资源&#xff0c;为企业呈现一个全面的短视频营销策略。该系统致力于协助企业以迅速且高效的方式制作…

【ARM】MCU和SOC的区别

【更多软件使用问题请点击亿道电子官方网站】 1、 文档目标 了解SOC芯片和MCU芯片的区别 2、 问题场景 用于了解SOC芯片和MCU芯片的区别&#xff0c;内部结构上的区别。 3、软硬件环境 1&#xff09;、软件版本&#xff1a;无 2&#xff09;、电脑环境&#xff1a;无 3&am…

MySQL高级-MVCC-原理分析(RC级别)

文章目录 1、RC隔离级别下&#xff0c;在事务中每一次执行快照读时生成ReadView2、先来看第一次快照读具体的读取过程&#xff1a;3、再来看第二次快照读具体的读取过程: 1、RC隔离级别下&#xff0c;在事务中每一次执行快照读时生成ReadView 我们就来分析事务5中&#xff0c;两…

java第三十课 —— 面向对象练习题

面向对象编程练习题 第一题 定义一个 Person 类 {name, age, job}&#xff0c;初始化 Person 对象数组&#xff0c;有 3 个 person 对象&#xff0c;并按照 age 从大到小进行排序&#xff0c;提示&#xff0c;使用冒泡排序。 package com.hspedu.homework;import java.util.…

LLM——10个大型语言模型(LLM)常见面试题以及答案解析

今天我们来总结以下大型语言模型面试中常问的问题 1、哪种技术有助于减轻基于提示的学习中的偏见? A.微调 Fine-tuning B.数据增强 Data augmentation C.提示校准 Prompt calibration D.梯度裁剪 Gradient clipping 答案:C 提示校准包括调整提示&#xff0c;尽量减少产生…

数据结构与算法笔记:实战篇 - 剖析Redis常用数据类型对应的数据结构

概述 从本章开始&#xff0c;就进入实战篇的部分。这部分主要通过一些开源醒目、经典系统&#xff0c;真枪实弹地教你&#xff0c;如何将数据结构和算法应用到项目中。所以这部分的内容&#xff0c;更多的是知识点的回顾&#xff0c;相对于基础篇和高级篇&#xff0c;其实这部…

【Web3项目案例】Ethers.js极简入门+实战案例:实现ERC20协议代币查询、交易

苏泽 大家好 这里是苏泽 一个钟爱区块链技术的后端开发者 本篇专栏 ←持续记录本人自学智能合约学习笔记和经验总结 如果喜欢拜托三连支持~ 目录 简介 前景科普-ERC20 Ethers极简入门教程&#xff1a;HelloVitalik&#xff08;非小白可跳&#xff09; 教程概览 开发工具 V…

虚拟机配置与windows之间文件夹共享samba服务:

虚拟机配置与windows之间文件夹共享samba服务: #输入安装命令&#xff1a; 第一步: 下载samba cd /etc/ sudo apt-get install samba第二步: 配置用户 sudo smbpasswd -a 虚拟机用户名第三步: 进入配置文件配置共享文件 sudo vim /etc/samba/smb.conf末尾输入以下内容: [s…

全球最大智能立体书库|北京:3万货位,715万册,自动出库、分拣、搬运

导语 大家好&#xff0c;我是社长&#xff0c;老K。专注分享智能制造和智能仓储物流等内容。 新书《智能物流系统构成与技术实践》 北京城市图书馆的立体书库采用了先进的WMS&#xff08;仓库管理系统&#xff09;和WCS&#xff08;仓库控制系统&#xff09;&#xff0c;与图书…

代码随想录-二叉搜索树(1)

目录 二叉搜索树的定义 700. 二叉搜索树中的搜索 题目描述&#xff1a; 输入输出示例&#xff1a; 思路和想法&#xff1a; 98. 验证二叉搜索树 题目描述&#xff1a; 输入输出示例&#xff1a; 思路和想法&#xff1a; 530. 二叉搜索树的最小绝对差 题目描述&#x…

error: Sandbox: rsync.samba in Xcode project

在Targets 的 Build Settings 搜索&#xff1a;User script sandboxing 设置为NO

基于机器学习的制冷系统过充电和欠充电故障诊断(采用红外热图像数据,MATLAB)

到目前为止&#xff0c;制冷系统故障诊断方法已经产生很多种&#xff0c;概括起来主要有三大类&#xff1a;基于分析的方法&#xff0c;基于知识的方法和基于数据驱动的方法。基于分析的方法主要获得制冷系统的数学模型&#xff0c;通过残差来检测和诊断故障。如果存在残差且很…

SonicSense:声学振动丰富机器人的物体感知能力

在通过声学振动进行物体感知方面&#xff0c;尽管以往的研究已经取得了一些有希望的结果&#xff0c;但目前的解决方案仍然受限于几个方面。首先&#xff0c;大多数现有研究集中在只有少数&#xff08;N < 5&#xff09;基本物体的受限设置上。这些物体通常具有均质材料组成…

电路笔记(电源模块): 基于FT2232HL实现的jtag下载器硬件+jtag的通信引脚说明

JTAG接口说明 JTAG 接口根据需求可以选择20针或14针的配置&#xff0c;具体选择取决于应用场景和需要连接的功能。比如之前的可编程逻辑器件XC9572XL使用JTAG引脚&#xff08;TCK、TDI、TDO、TMS、VREF、GND&#xff09;用于与器件进行调试和编程通信。更详细的内容可以阅读11…

KAIROS复现记录

KAIROS:使用全系统起源的实用入侵检测和调查 Github&#xff1a;https://github.com/ProvenanceAnalytics/kairos KAIROS: Practical Intrusion Detection and Investigation using Whole-system Provenance 1. 论文实验 实验部分使用SCISKIT-LEARN来实现分层特征散列&#xf…

硬核!大佬通过Intel CPU的JTAG接口,DUMP微软原始Xbox的加密BootROM。

这是一篇记录如何通过Intel CPU的JTAG接口,DUMP微软原始Xbox的加密BootROM的文章,内容也记录了老哥如何设计实现JTAG调试器的过程,非常硬核! 原文:JTAG ‘Hacking’ the Original Xbox in 2023 Using Intel CPU JTAG to dump the secret bootrom in Microsoft’s original…

Java代码基础算法练习-求成绩单中最高和第二高的成绩-2024.06.30

任务描述&#xff1a; 输入n(0<n<20)个整数代表成绩&#xff0c;求n个成绩中最高的和第二高成绩 解决思路&#xff1a; 输入的数字 n 为 for 循环的次数&#xff0c;在每次循环中进行值的输入和判断 如果当前输入的分数大于最大值&#xff0c;则更新最大值和次大值 如…

Golang-channel理解

channel golang-channel语雀笔记整理 channelgolang channel的设计动机&#xff1f;chanel的数据结构/设计思考 golang channel的设计动机&#xff1f; channel是一种不同协程之间实现异步通信的数据结构。golang中有一种很经典的说法是要基于通信实现共享内存&#xff0c;而不…

grpc教程——proto文件转go

【1】编写一个proto文件 syntax "proto3"; package myproto;service NC{rpc SayStatus (NCRequest) returns (NCResponse){} }message NCRequest{ string name 1; } message NCResponse{string status 1; } 【2】转换&#xff1a;protoc --go_out. myservice.pro…

重生奇迹MU 正确获取金币的方式

在游戏中&#xff0c;需要消耗大量的金币来购买红药等物品。因此&#xff0c;如何快速赚取金币也成为玩家关注的问题。您知道有哪些方法可以快速地获得金币吗&#xff1f; 一、哪个地图上是最适合打金币的很关键 在选择打钱的地方时&#xff0c;不能盲目行动&#xff0c;需要…