当前位置: 首页 > news >正文

Vue+Xterm.js+WebSocket+JSch实现Web Shell终端

一、需求

在系统中使用Web Shell连接集群的登录节点

二、实现

前端使用Vue,WebSocket实现前后端通信,后端使用JSch ssh通讯包。

1. 前端核心代码
<template><div class="shell-container"><div id="shell"/></div>
</template><script>import 'xterm/css/xterm.css'
import { Terminal } from 'xterm'
import { FitAddon } from 'xterm-addon-fit'export default {name: 'WebShell',props: {socketURI: {type: String,default: ''},},watch: {socketURI: {deep: true, //对象内部属性的监听,关键。immediate: true,handler() {this.initSocket();},},},data() {return {term: undefined,rows: 24,cols: 80,path: "",isShellConn: false // shell是否连接成功}},mounted() {const { onTerminalResize } = this;this.initSocket();// 通过防抖函数const resizedFunc = this.debounce(function() {onTerminalResize();}, 250); // 250毫秒内只执行一次  window.addEventListener('resize', resizedFunc);},beforeUnmount() {this.socket.close();this.term&&this.term.dispose();window.removeEventListener('resize');},methods: {initTerm() {let term = new Terminal({rendererType: "canvas", //渲染类型rows: this.rows, //行数cols: this.cols, // 不指定行数,自动回车后光标从下一行开始convertEol: true, //启用时,光标将设置为下一行的开头disableStdin: false, //是否应禁用输入windowsMode: true, // 根据窗口换行cursorBlink: true, //光标闪烁theme: {foreground: "#ECECEC", //字体background: "#000000", //背景色cursor: "help", //设置光标lineHeight: 20,},});this.term = term;const fitAddon = new FitAddon();this.term.loadAddon(fitAddon);this.fitAddon = fitAddon;let element = document.getElementById("shell");term.open(element);// 自适应大小(使终端的尺寸和几何尺寸适合于终端容器的尺寸),初始化的时候宽高都是对的fitAddon.fit();term.focus();//监视命令行输入this.term.onData((data) => {let dataWrapper = data;if (dataWrapper === "\r") {dataWrapper = "\n";} else if (dataWrapper === "\u0003") {// 输入ctrl+cdataWrapper += "\n";}// 将输入的命令通知给后台,后台返回数据。this.socket.send(JSON.stringify({ type: "command", data: dataWrapper }));});},onTerminalResize() {this.fitAddon.fit();this.socket.send(JSON.stringify({type: "resize",data: {rows: this.term.rows,cols: this.term.cols,}}));},initSocket() {if (this.socketURI == "") {return;}// 添加path、cols、rowsconst uri = `${this.socketURI}&path=${this.path}&cols=${this.cols}&rows=${this.rows}`;console.log(uri);this.socket = new WebSocket(uri);this.socketOnClose();this.socketOnOpen();this.socketOnmessage();this.socketOnError();},socketOnOpen() {this.socket.onopen = () => {console.log("websocket链接成功");this.initTerm();};},socketOnmessage() {this.socket.onmessage = (evt) => {try {if (typeof evt.data === "string") {const msg = JSON.parse(evt.data);switch(msg.type) {case "command":// 将返回的数据写入xterm,回显在webshell上this.term.write(msg.data);// 当shell首次连接成功时才发送resize事件if (!this.isShellConn) {// when server ready for connection,send resize to serverthis.onTerminalResize();this.isShellConn = true;}break;case "exit":this.term.write("Process exited with code 0");break;}}} catch (e) {console.error(e);console.log("parse json error.", evt.data);}};},socketOnClose() {this.socket.onclose = () => {this.socket.close();console.log("关闭 socket");window.removeEventListener("resize", this.onTerminalResize);};},socketOnError() {this.socket.onerror = () => {console.log("socket 链接失败");};},debounce(func, wait) {  let timeout;  return function() {  const context = this;  const args = arguments;  clearTimeout(timeout);  timeout = setTimeout(function() {  func.apply(context, args);  }, wait);  };  }  }
}
</script><!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
#shell {width: 100%;height: 100%;
}
.shell-container {height: 100%;
}
</style>
2. 后端核心代码
package com.example.webshell.service.impl;import com.alibaba.fastjson.JSONObject;
import com.example.webshell.constant.Constant;
import com.example.webshell.entity.LoginNodeInfo;
import com.example.webshell.entity.ShellConnectInfo;
import com.example.webshell.entity.SocketData;
import com.example.webshell.entity.WebShellParam;
import com.example.webshell.service.WebShellService;
import com.example.webshell.utils.ThreadPoolUtils;
import com.example.webshell.utils.WebShellUtil;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.jcraft.jsch.*;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Arrays;
import java.util.Map;
import java.util.Properties;
import java.util.concurrent.ConcurrentHashMap;import static com.example.webshell.constant.Constant.*;@Slf4j
@Service
public class WebShellServiceImpl implements WebShellService {/*** 存放ssh连接信息的map*/private static final Map<String, Object> SSH_MAP = new ConcurrentHashMap<>();/*** 初始化连接*/@Overridepublic void initConnection(javax.websocket.Session webSocketSession, WebShellParam webShellParam) {JSch jSch = new JSch();ShellConnectInfo shellConnectInfo = new ShellConnectInfo();shellConnectInfo.setJsch(jSch);shellConnectInfo.setSession(webSocketSession);String uuid = WebShellUtil.getUuid(webSocketSession);// 根据集群和登录节点查询IP TODOLoginNodeInfo loginNodeInfo = new LoginNodeInfo("demo_admin", "demo_admin", "192.168.88.102", 22);//启动线程异步处理ThreadPoolUtils.execute(() -> {try {connectToSsh(shellConnectInfo, webShellParam, loginNodeInfo, webSocketSession);} catch (JSchException e) {log.error("web shell连接异常: {}", e.getMessage());sendMessage(webSocketSession, new SocketData(OPERATE_ERROR, e.getMessage()));close(webSocketSession);}});//将这个ssh连接信息放入缓存中SSH_MAP.put(uuid, shellConnectInfo);}/*** 处理客户端发送的数据*/@Overridepublic void handleMessage(javax.websocket.Session webSocketSession, String message) {ObjectMapper objectMapper = new ObjectMapper();SocketData shellData;try {shellData = objectMapper.readValue(message, SocketData.class);String userId = WebShellUtil.getUuid(webSocketSession);//找到刚才存储的ssh连接对象ShellConnectInfo shellConnectInfo = (ShellConnectInfo) SSH_MAP.get(userId);if (shellConnectInfo != null) {if (OPERATE_RESIZE.equals(shellData.getType())) {ChannelShell channel = shellConnectInfo.getChannel();Object data = shellData.getData();Map map = objectMapper.readValue(JSONObject.toJSONString(data), Map.class);System.out.println(map);channel.setPtySize(Integer.parseInt(map.get("cols").toString()), Integer.parseInt(map.get("rows").toString()), 0, 0);} else if (OPERATE_COMMAND.equals(shellData.getType())) {String command = shellData.getData().toString();sendToTerminal(shellConnectInfo.getChannel(), command);// 退出状态码int exitStatus = shellConnectInfo.getChannel().getExitStatus();System.out.println(exitStatus);} else {log.error("不支持的操作");close(webSocketSession);}}} catch (Exception e) {e.printStackTrace();log.error("消息处理异常: {}", e.getMessage());}}/*** 关闭连接*/private void close(javax.websocket.Session webSocketSession) {String userId = WebShellUtil.getUuid(webSocketSession);ShellConnectInfo shellConnectInfo = (ShellConnectInfo) SSH_MAP.get(userId);if (shellConnectInfo != null) {//断开连接if (shellConnectInfo.getChannel() != null) {shellConnectInfo.getChannel().disconnect();}//map中移除SSH_MAP.remove(userId);}}/*** 使用jsch连接终端*/private void connectToSsh(ShellConnectInfo shellConnectInfo, WebShellParam webShellParam, LoginNodeInfo loginNodeInfo, javax.websocket.Session webSocketSession) throws JSchException {Properties config = new Properties();// SSH 连接远程主机时,会检查主机的公钥。如果是第一次该主机,会显示该主机的公钥摘要,提示用户是否信任该主机config.put("StrictHostKeyChecking", "no");//获取jsch的会话Session session = shellConnectInfo.getJsch().getSession(loginNodeInfo.getUsername(), loginNodeInfo.getHost(), loginNodeInfo.getPort());session.setConfig(config);//设置密码session.setPassword(loginNodeInfo.getPassword());//连接超时时间30ssession.connect(30 * 1000);//查询上次登录时间showLastLogin(session, webSocketSession, loginNodeInfo.getUsername());//开启交互式shell通道ChannelShell channel = (ChannelShell) session.openChannel("shell");//设置channelshellConnectInfo.setChannel(channel);//通道连接超时时间3schannel.connect(3 * 1000);channel.setPty(true);//读取终端返回的信息流try (InputStream inputStream = channel.getInputStream()) {//循环读取byte[] buffer = new byte[Constant.BUFFER_SIZE];int i;//如果没有数据来,线程会一直阻塞在这个地方等待数据。while ((i = inputStream.read(buffer)) != -1) {sendMessage(webSocketSession, new SocketData(OPERATE_COMMAND, new String(Arrays.copyOfRange(buffer, 0, i))));}} catch (IOException e) {log.error("读取终端返回的信息流异常:", e);} finally {//断开连接后关闭会话session.disconnect();channel.disconnect();}}/*** 向前端展示上次登录信息*/private void showLastLogin(Session session, javax.websocket.Session webSocketSession, String username) throws JSchException {ChannelExec channelExec = (ChannelExec) session.openChannel("exec");channelExec.setCommand("lastlog -u " + username);channelExec.connect();channelExec.setErrStream(System.err);try (InputStream inputStream = channelExec.getInputStream()) {byte[] buffer = new byte[Constant.BUFFER_SIZE];int i;StringBuilder sb = new StringBuilder();while ((i = inputStream.read(buffer)) != -1) {sb.append(new String(Arrays.copyOfRange(buffer, 0, i)));}// 解析结果String[] split = sb.toString().split("\n");if (split.length > 1) {String[] items = split[1].split("\\s+", 4);String msg = String.format("Last login: %s from %s\n", items[3], items[2]);sendMessage(webSocketSession, new SocketData(OPERATE_COMMAND, msg));}} catch (IOException e) {log.error("读取终端返回的信息流异常:", e);} finally {channelExec.disconnect();}}/*** 数据写回前端*/private void sendMessage(javax.websocket.Session webSocketSession, SocketData data) {try {webSocketSession.getBasicRemote().sendText(JSONObject.toJSONString(data));} catch (IOException e) {log.error("数据写回前端异常:", e);}}/*** 将消息转发到终端*/private void sendToTerminal(Channel channel, String command) {if (channel != null) {try {OutputStream outputStream = channel.getOutputStream();outputStream.write(command.getBytes());outputStream.flush();} catch (IOException e) {log.error("web shell将消息转发到终端异常:{}", e.getMessage());}}}
}

三、效果展示

在这里插入图片描述

相关文章:

Vue+Xterm.js+WebSocket+JSch实现Web Shell终端

一、需求 在系统中使用Web Shell连接集群的登录节点 二、实现 前端使用Vue&#xff0c;WebSocket实现前后端通信&#xff0c;后端使用JSch ssh通讯包。 1. 前端核心代码 <template><div class"shell-container"><div id"shell"/>&l…...

用 adb 来模拟手机插上电源和拔掉电源的情形

实用的 ADB 命令 要模拟手机从 USB 充电器上拔掉的情形&#xff0c;你可以使用&#xff1a; adb shell dumpsys battery set usb 0或者&#xff0c;如果你使用的是 Android 6.0 或更高版本的设备&#xff0c;你可以使用&#xff1a; adb shell dumpsys battery unplug要重新…...

【SPIE独立出版】第四届智能交通系统与智慧城市国际学术会议(ITSSC 2024)

第四届智能交通系统与智慧城市国际学术会议&#xff08;ITSSC 2024&#xff09;将于2024年8月23-25日在中国西安举行。本次会议主要围绕智能交通、交通新能源、无人驾驶、智慧城市、智能家居、智能生活等研究领域展开讨论&#xff0c; 旨在为该研究领域的专家学者们提供一个分享…...

【Unity数据交互】如何Unity中读取Ecxel中的数据

&#x1f468;‍&#x1f4bb;个人主页&#xff1a;元宇宙-秩沅 &#x1f468;‍&#x1f4bb; hallo 欢迎 点赞&#x1f44d; 收藏⭐ 留言&#x1f4dd; 加关注✅! &#x1f468;‍&#x1f4bb; 本文由 秩沅 原创 &#x1f468;‍&#x1f4bb; 专栏交流&#x1f9e7;&…...

基于深度学习LightWeight的人体姿态检测跌倒系统源码

一. LightWeight概述 light weight openpose是openpose的简化版本&#xff0c;使用了openpose的大体流程。 Light weight openpose和openpose的区别是&#xff1a; a 前者使用的是Mobilenet V1&#xff08;到conv5_5&#xff09;&#xff0c;后者使用的是Vgg19&#xff08;前10…...

SpringBoot 生产实践:没有父 starter 的打包问题

文章目录 前言一、搜索引擎二、Chat GPT三、官方文档四、小结推荐阅读 前言 今天刚准备写点文章&#xff0c;需要 SpringBoot 项目来演示效果。一时心血来潮&#xff0c;没有采用传统的方式&#xff08;即通过引入 spring-boot-starter-parent 父工程的方式&#xff09;。 &l…...

IDEA配Git

目录 前言 1.创建Git仓库&#xff0c;获得可提交渠道 2.选择本地提交的项目名 3.配置远程仓库的地址 4.新增远程仓库地址 5.开始进行commit操作 6.push由于邮箱问题被拒绝的解决方法&#xff1a; 后记 前言 以下操作都是基于你已经下载了Git的前提下进行的&#xff0c…...

51单片机STC89C52RC——14.1 直流电机调速

目录 目的/效果 1&#xff1a;电机转速同步LED呼吸灯 2 通过独立按键 控制直流电机转速。 一&#xff0c;STC单片机模块 二&#xff0c;直流电机 2.1 简介 2.2 驱动电路 2.2.1 大功率器件直接驱动 2.2.2 H桥驱动 正转 反转 2.2.3 ULN2003D 引脚、电路 2.3 PWM&…...

AI对于高考和IT行业的深远影响

目录 AI对IT行业的冲击及深远影响1. 工作自动化2. 新的就业机会3. 行业融合4. 技术升级和创新5. 数据的重要性 IT行业的冬天要持续多久&#xff1f;大学的软件开发类专业是否还值得报考&#xff1f;其他问题IT行业是否都是加班严重&#xff1f;35岁后就业困难是否普遍现象&…...

C语言下的文件详解

主要内容 文件概述文件指针文件的打开与关闭文件的读写 文件 把输入和输出的数据以文件的形式保存在计算机的外存储器上&#xff0c;可以确保数据能随时使用&#xff0c;避免反复输入和读取数据 文件概述 文件是指一组相关数据的有序集合 文件是存储数据的基本单位&#…...

Oracle PL / SQL块结构

在PL / SQL中&#xff0c;最小的有意义的代码分组被称为块。 块代码为变量声明和异常处理提供执行和作用域边界。 PL / SQL允许您创建匿名块和命名块。 命名块可以是包&#xff0c;过程&#xff0c;函数&#xff0c;触发器或对象类型。 PL / SQL是SQL的过程语言扩展&#x…...

MySQL的安装和启动

安装 版本 1&#xff0c;社区版&#xff1a;免费&#xff0c;不提供任何技术支持 2&#xff0c;商业版&#xff1a;可以试用30天&#xff0c;官方提供技术支持下载 1&#xff0c;下载地址&#xff1a;https://dev.mysql.com/downloads/mysql/ 2&#xff0c;安装&#xff1a;傻…...

Prometheus概述

1.什么是prometheus Prometheus 是一个开源的服务监控系统和时序数据库&#xff0c;其提供了通用的数据模型和快捷数据采集、存储和查询接口。它的核心组件Prometheus server会定期从静态配置的监控目标或者基于服务发现自动配置的自标中进行拉取数据&#xff0c;当新拉取到的…...

【SQL】什么是最左前缀原则/最左匹配原则

最左前缀原则&#xff08;或最左匹配原则&#xff09;是关系型数据库在使用复合索引时遵循的一条重要规则。该原则指的是&#xff0c;当查询条件使用复合索引时&#xff0c;查询优化器会首先使用索引的最左边的列&#xff0c;依次向右匹配&#xff0c;直到不再满足查询条件为止…...

java项目配置logback日志

在resource目录下添加logback配置文件 <?xml version"1.0" encoding"UTF-8"?> <configuration scan"true" scanPeriod"60 seconds" debug"false"><property name"log_dir" value"/APL/log…...

Python入门 2024/7/6

目录 元组的定义和操作 字符串的定义和操作 字符串 字符串的替换 字符串的分割 字符串的规整操作&#xff08;去除前后空格&#xff09; 字符串的规整操作&#xff08;去掉前后指定字符串&#xff09; 操作 字符串的替换 字符串的分割 字符串的规整操作 统计字符串的…...

ChatGPT4深度解析:探索智能对话新境界

大模型chatgpt4分析功能初探 目录 1、探测目的 2、目标变量分析 3、特征缺失率处理 4、特征描述性分析 5、异常值分析 6、相关性分析 7、高阶特征挖掘 1、探测目的 1、分析chat4的数据分析能力&#xff0c;提高部门人效 2、给数据挖掘提供思路 3、原始数据&#xf…...

触底加载的两种思路(以vue3前端和nodejs后端为例)

一:首先,nodejs后端的代码都是一样的. 需要前端返回page参数,然后nodejs逻辑进行处理,截取页数和每页条数和总条数, 总条数用来作为判断是否有数据的条件,也可以不用,注意看下文 一:不用获取容器高度的. pinia中进行的axios请求处理 在vue文件中进行pinia中数据的导入,继续进…...

tobias实现支付宝支付

tobias是一个为支付宝支付SDK做的Flutter插件。 如何使用 你需要在pubspec.yaml中配置url_scheme。url_scheme是一个独特的字符串&#xff0c;用来重新启动你的app&#xff0c;但是请注意字符串“_”是不合法的。 在iOS端&#xff0c;你还需要配置并传入一个universal link。…...

【音视频 | RTSP】RTSP协议详解 及 抓包例子解析(详细而不赘述)

&#x1f601;博客主页&#x1f601;&#xff1a;&#x1f680;https://blog.csdn.net/wkd_007&#x1f680; &#x1f911;博客内容&#x1f911;&#xff1a;&#x1f36d;嵌入式开发、Linux、C语言、C、数据结构、音视频&#x1f36d; &#x1f923;本文内容&#x1f923;&a…...

idea大量爆红问题解决

问题描述 在学习和工作中&#xff0c;idea是程序员不可缺少的一个工具&#xff0c;但是突然在有些时候就会出现大量爆红的问题&#xff0c;发现无法跳转&#xff0c;无论是关机重启或者是替换root都无法解决 就是如上所展示的问题&#xff0c;但是程序依然可以启动。 问题解决…...

Vue记事本应用实现教程

文章目录 1. 项目介绍2. 开发环境准备3. 设计应用界面4. 创建Vue实例和数据模型5. 实现记事本功能5.1 添加新记事项5.2 删除记事项5.3 清空所有记事 6. 添加样式7. 功能扩展&#xff1a;显示创建时间8. 功能扩展&#xff1a;记事项搜索9. 完整代码10. Vue知识点解析10.1 数据绑…...

椭圆曲线密码学(ECC)

一、ECC算法概述 椭圆曲线密码学&#xff08;Elliptic Curve Cryptography&#xff09;是基于椭圆曲线数学理论的公钥密码系统&#xff0c;由Neal Koblitz和Victor Miller在1985年独立提出。相比RSA&#xff0c;ECC在相同安全强度下密钥更短&#xff08;256位ECC ≈ 3072位RSA…...

日语学习-日语知识点小记-构建基础-JLPT-N4阶段(33):にする

日语学习-日语知识点小记-构建基础-JLPT-N4阶段(33):にする 1、前言(1)情况说明(2)工程师的信仰2、知识点(1) にする1,接续:名词+にする2,接续:疑问词+にする3,(A)は(B)にする。(2)復習:(1)复习句子(2)ために & ように(3)そう(4)にする3、…...

深入浅出:JavaScript 中的 `window.crypto.getRandomValues()` 方法

深入浅出&#xff1a;JavaScript 中的 window.crypto.getRandomValues() 方法 在现代 Web 开发中&#xff0c;随机数的生成看似简单&#xff0c;却隐藏着许多玄机。无论是生成密码、加密密钥&#xff0c;还是创建安全令牌&#xff0c;随机数的质量直接关系到系统的安全性。Jav…...

uniapp微信小程序视频实时流+pc端预览方案

方案类型技术实现是否免费优点缺点适用场景延迟范围开发复杂度​WebSocket图片帧​定时拍照Base64传输✅ 完全免费无需服务器 纯前端实现高延迟高流量 帧率极低个人demo测试 超低频监控500ms-2s⭐⭐​RTMP推流​TRTC/即构SDK推流❌ 付费方案 &#xff08;部分有免费额度&#x…...

WEB3全栈开发——面试专业技能点P2智能合约开发(Solidity)

一、Solidity合约开发 下面是 Solidity 合约开发 的概念、代码示例及讲解&#xff0c;适合用作学习或写简历项目背景说明。 &#x1f9e0; 一、概念简介&#xff1a;Solidity 合约开发 Solidity 是一种专门为 以太坊&#xff08;Ethereum&#xff09;平台编写智能合约的高级编…...

【OSG学习笔记】Day 16: 骨骼动画与蒙皮(osgAnimation)

骨骼动画基础 骨骼动画是 3D 计算机图形中常用的技术&#xff0c;它通过以下两个主要组件实现角色动画。 骨骼系统 (Skeleton)&#xff1a;由层级结构的骨头组成&#xff0c;类似于人体骨骼蒙皮 (Mesh Skinning)&#xff1a;将模型网格顶点绑定到骨骼上&#xff0c;使骨骼移动…...

C++八股 —— 单例模式

文章目录 1. 基本概念2. 设计要点3. 实现方式4. 详解懒汉模式 1. 基本概念 线程安全&#xff08;Thread Safety&#xff09; 线程安全是指在多线程环境下&#xff0c;某个函数、类或代码片段能够被多个线程同时调用时&#xff0c;仍能保证数据的一致性和逻辑的正确性&#xf…...

Mobile ALOHA全身模仿学习

一、题目 Mobile ALOHA&#xff1a;通过低成本全身远程操作学习双手移动操作 传统模仿学习&#xff08;Imitation Learning&#xff09;缺点&#xff1a;聚焦与桌面操作&#xff0c;缺乏通用任务所需的移动性和灵活性 本论文优点&#xff1a;&#xff08;1&#xff09;在ALOHA…...