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

news2024/11/18 23:50:08

一、需求

在系统中使用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+c
          dataWrapper += "\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、rows
      const 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 server
                  this.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<>();

    /**
     * 初始化连接
     */
    @Override
    public 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 TODO
        LoginNodeInfo 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);
    }

    /**
     * 处理客户端发送的数据
     */
    @Override
    public 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());
        //连接超时时间30s
        session.connect(30 * 1000);

        //查询上次登录时间
        showLastLogin(session, webSocketSession, loginNodeInfo.getUsername());

        //开启交互式shell通道
        ChannelShell channel = (ChannelShell) session.openChannel("shell");
        //设置channel
        shellConnectInfo.setChannel(channel);

        //通道连接超时时间3s
        channel.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());
            }
        }
    }
}

三、效果展示

在这里插入图片描述

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

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

相关文章

day01-切片和索引

day01-切片和索引 ndarray对象的内容可以通过索引或切片来访问和修改&#xff0c;与 Python 中list 的切片操作一样。 ndarray数组可以基于0-n的下标进行索引 注意&#xff0c;数组切片并不像列表切片会重新开辟一片空间&#xff0c;而是地址引用&#xff0c;需要使用.copy()…

【Threejs进阶教程-着色器篇】2. Uniform的基本用法与Uniform的调试

Uniform的基本用法与Uniform的调试 关于本Shader教程优化上一篇的效果优化光栅栏高度让透明度和颜色变的更平滑pow()函数借助数学工具更好的理解函数 Unifoms简介编写uniforms修改片元着色器代码借助lil.gui调试uniforms使用uniform控制颜色继续在uniforms添加颜色在着色器中接…

动态住宅代理IP的3个优点

在大数据时代的背景下&#xff0c;代理IP成为了很多企业顺利开展的重要工具。代理IP地址可以分为住宅代理IP地址和数据中心代理IP地址。选择住宅代理IP的好处是可以实现真正的高匿名性&#xff0c;而使用数据中心代理IP可能会暴露自己使用代理的情况。 住宅代理IP是指互联网服务…

JavaScript中location对象的主要属性和方法

属性 href&#xff1a;获取或设置整个URL。protocol&#xff1a;获取URL的协议部分&#xff0c;如"http:"或"https:"。host&#xff1a;获取URL的主机名&#xff08;包括端口号&#xff0c;如果有的话&#xff09;。hostname&#xff1a;获取URL的主机名&…

Android studio 打包低版本的Android项目报错

一、报错内容 Execution failed for task :app:packageRelease. > A failure occurred while executing com.android.build.gradle.internal.tasks.Workers$ActionFacade> com.android.ide.common.signing.KeytoolException: Failed to read key key0 from store "…

【Portswigger 学院】路径遍历

路径遍历&#xff08;Path traversal&#xff09;又称目录遍历&#xff08;Directory traversal&#xff09;&#xff0c;允许攻击者通过应用程序读取或写入服务器上的任意文件&#xff0c;例如读取应用程序源代码和数据、凭证和操作系统文件&#xff0c;或写入应用程序所访问或…

10 Posix API与网络协议栈

POSIX概念 POSIX是由IEEE指定的一系列标准,用于澄清和统一Unix-y操作系统提供的应用程序编程接口(以及辅助问题,如命令行shell实用程序),当您编写程序以依赖POSIX标准时,您可以非常肯定能够轻松地将它们移植到大量的Unix衍生产品系列中(包括Linux,但不限于此!)。 如…

奇瑞被曝强制加班,“896”成常态且没有加班费

ChatGPT狂飙160天&#xff0c;世界已经不是之前的样子。 更多资源欢迎关注 7 月 2 日消息&#xff0c;一位认证为“奇瑞员工”的网友近期发帖引发热议&#xff0c;奇瑞汽车内部存在强制加班行为&#xff0c;每周加班时长需大于 20 小时并且没有加班费&#xff0c;仅补贴 10 元…

人口萎缩,韩国釜山“进入消失阶段”

KlipC报道&#xff1a;调查显示&#xff0c;随着低生育率和人口老化&#xff0c;釜山人口逐渐萎缩&#xff0c;韩国第二大城市釜山显现出“进入消失阶段”的迹象。 据悉&#xff0c;“消失风险指数”是将20岁至39岁女性人口总数除以65岁及以上人口得到的数值。当该指数大于1.5…

第T3周:天气识别

&#x1f368; 本文为&#x1f517;365天深度学习训练营 中的学习记录博客&#x1f356; 原作者&#xff1a;K同学啊 一、前期工作 本文将采用CNN实现多云、下雨、晴、日出四种天气状态的识别。较上篇文章&#xff0c;本文为了增加模型的泛化能力&#xff0c;新增了Dropout层并…

【计算机体系结构】缓存的false sharing

在介绍缓存的false sharing之前&#xff0c;本文先介绍一下多核系统中缓存一致性是如何维护的。 目前主流的多核系统中的缓存一致性协议是MESI协议及其衍生协议。 MESI协议 MESI协议的4种状态 MESI协议有4种状态。MESI是4种状态的首字母缩写&#xff0c;缓存行的4种状态分别…

snap和apt的区别简单了解

Linux中没有tree命令的时候提示安装的时候出现了两个命令&#xff0c;简单看了看两者有何区别&#xff08;一般用apt就可以了&#xff09;&#xff1a; sudo snap install tree 和 sudo apt install tree 这两个命令都是用来安装 tree 命令行工具的&#xff0c;但它们使用的是不…

uniapp零基础入门Vue3组合式API语法版本开发咸虾米壁纸项目实战

嗨&#xff0c;大家好&#xff0c;我是爱搞知识的咸虾米。 今天给大家带来的是零基础入门uniapp&#xff0c;课程采用的是最新的Vue3组合式API版本&#xff0c;22年发布的uniappVue2版本获得了官方推荐&#xff0c;有很多同学等着我这个vue3版本的那&#xff0c;如果没有学过vu…

数字信号处理教程(2)——时域离散信号与时域离散系统

上回书说到数字信号处理中基本的一个通用模型框架图。今天咱们继续&#xff0c;可以说今天要讲的东西必须是学习数字信号处理必备的观念——模拟与数字&#xff0c;连续和离散。 时域离散序列 由于数字信号基本都来自模拟信号&#xff0c;所以先来谈谈模拟信号。模拟信号就是…

umi项目中的一些趣事

前言 出于某些安全问题&#xff0c;需要把HTML中框架注入的umi版本信息去掉&#xff0c;那要怎么搞呢~ 方案 查找官方文档&#xff0c;没发现可以去掉注入信息的方法&#xff0c;但在一番折腾后&#x1f609;终究还是解决了~ 发现 版本信息是从这里注入的~ Object.define…

企业短视频-直播运营团队打造课,手把手带你从0-1 搭建运营团队-15节

如何获取精准客户? 一套抖音营销系统打造课 能定位 懂运营 建团队 持续获客 课程目录 1-01、每个老板都应该学习博商团队的打造方法1.mp4 2-02、如何从0-1快速搭建运营团队1.mp4 3-03、怎么才能招聘到运营人才&#xff1f;1.mp4 4-04、怎么才能快速筛选简历招到符合要求…

程序烧录原理

程序烧录原理 ISP(In System Programming)&#xff0c;在系统编程&#xff0c;单片机不须脱离应用系统而直接在产品上烧写/升级程序。 条件&#xff1a;系统须引出单片机的串口引脚&#xff08;TXD、RXD&#xff09;ISP相对于传统的编程方式&#xff0c;在传统的编程方式中我们…

kvm虚拟机启用console登录

kvm虚拟机console登录&#xff0c;就是执行 virsh console 的时候&#xff0c;宿主机可以控制虚拟机。 一、centos7的kvm虚拟机开启console登录&#xff08;在虚拟中操作&#xff09; 1、备份文件 [roothadoop51 ~]# cp /etc/grub2.cfg /etc/grub2.cfg_back 2、用下面命令可…

2024 AIGC 技术创新应用研讨会暨数字造型设计师高级研修班通知

尊敬的老师、领导您好! 为深入响应国家关于教育综合改革的战略部署&#xff0c;深化职业教育、高等教育改革&#xff0c;发挥企业主体重要作用&#xff0c;促进人才培养供给侧和产业需求侧结构要素全方位融合&#xff0c;充分把握人工智能创意式生成(AIGC)技术在教育领域的发展…

强连通分量

强连通分量 强连通定义 有向图 G G G 的强连通是指 G G G 中任意两个节点都可以直接或间接到达。 下方两幅图都是强连通。一个特殊一点&#xff0c;任意两点都可以直接到达&#xff1b;一个则是最常见的强连通图。 特殊强连通图&#xff0c;任意两点都可以直接到达 常见的…