WebSocket+xterm+springboot+vue 实现 xshell 操作linux终端功能

news2024/11/17 14:42:52

效果图

在这里插入图片描述

1.工具介绍与安装

1.1 xterm.js

xterm 是一个使用 TypeScript 编写的前端终端组件,可以直接在浏览器中实现一个命令行终端应用。Xterm.js 适用于大多数终端应用程序,如 bash,vim 和 tmux,这包括对基于curses的应用程序和鼠标事件的支持。

1.2 安装

// 1、安装 xterm
npm install --save xterm

// 2、安装xterm-addon-fit
// xterm.js的插件,使终端的尺寸适合包含元素。
npm install --save xterm-addon-fit

// 3、安装xterm-addon-attach(这个你不用就可以不装)
// xterm.js的附加组件,用于附加到Web Socket
npm install --save xterm-addon-attach

安装完之后可以在package.json看到依赖
在这里插入图片描述

1.3 websocket

websocket主要用于将前端的指令传递到后端,后端做出响应在传回前端显示。

springboot中安装依赖

 <!--  WebSocket 支持  -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-websocket</artifactId>
        </dependency>

2.前端实现

2.1 模板部分

<template>
      <div class="console" id="terminal" style="min-height: cala(100vh)"></div>
</template>

2.2 逻辑部分

这一部分是填写你后端连接的地址,注意不要弄错

const WebSocketUrl = "wss://localhost:8080/ws/ssh";
<script>
import "xterm/css/xterm.css";
import { Terminal } from "xterm";
import { FitAddon } from "xterm-addon-fit";
import { AttachAddon } from "xterm-addon-attach";

export default {
  name: "Xterm",
  props: {
    socketURI: {
      type: String,
      default: ""
    }
  },
  data () {
    return {
      term: null,
      socket: null,
      rows: 32,
      cols: 20,
      SetOut: false,
      isKey: false,
    };
  },
  mounted () {
    this.initSocket();

  },
  beforeDestroy () {
    this.socket.close();
    // this.term.dispose();
  },
  methods: {
    //Xterm主题
    initTerm () {
      const term = new Terminal({
        rendererType: "canvas", //渲染类型
        rows: this.rows, //行数
        // cols: this.cols,// 设置之后会输入多行之后覆盖现象
        convertEol: true, //启用时,光标将设置为下一行的开头
        // scrollback: 10,//终端中的回滚量
        fontSize: 14, //字体大小
        disableStdin: false, //是否应禁用输入。
        cursorStyle: "block", //光标样式
        // cursorBlink: true, //光标闪烁
        scrollback: 30,
        tabStopWidth: 4,
        theme: {
          foreground: "yellow", //字体
          background: "#060101", //背景色
          cursor: "help" //设置光标
        }
      });
      const attachAddon = new AttachAddon(this.socket);
      const fitAddon = new FitAddon();
      term.loadAddon(attachAddon);
      term.loadAddon(fitAddon);
     
      term.open(document.getElementById("terminal"));
     
      term.focus();
      let _this = this;
      //限制和后端交互,只有输入回车键才显示结果
      term.prompt = () => {
        term.write("\r\n$ ");
      };
      term.prompt();
      
      function runFakeTerminal (_this) {
        if (term._initialized) {
          return;
        }
        // 初始化
        term._initialized = true;
        term.writeln();//控制台初始化报错处
        term.prompt();
        // / **
        //     *添加事件监听器,用于按下键时的事件。事件值包含
        //     *将在data事件以及DOM事件中发送的字符串
        //     *触发了它。
        //     * @返回一个IDisposable停止监听。
        //  * /
        //   / ** 更新:xterm 4.x(新增)
        //  *为数据事件触发时添加事件侦听器。发生这种情况
        //  *用户输入或粘贴到终端时的示例。事件值
        //  *是`string`结果的结果,在典型的设置中,应该通过
        //  *到支持pty。
        //  * @返回一个IDisposable停止监听。
        //  * /
        // 支持输入与粘贴方法
        term.onData(function (key) {
          let order = {
            Data: key,
            Op: "stdin"
          };
          _this.onSend(order);
        });
        _this.term = term;
      }
      runFakeTerminal(_this);
    },
    //webShell主题
    initSocket () {
     // const WebSocketUrl = "ws://localhost:8080/ws/ssh";
      const WebSocketUrl = "wss://localhost:8080/ws/ssh";
      this.socket = new WebSocket(
        WebSocketUrl
      );
      this.socketOnClose(); //关闭
      this.socketOnOpen(); //
      this.socketOnError();
    },
    //webshell链接成功之后操作
    socketOnOpen () {
      this.socket.onopen = () => {
        // 链接成功后
        this.initTerm();
      };
    },
    //webshell关闭之后操作
    socketOnClose () {
      this.socket.onclose = () => {
        console.log("close socket");
      };
    },
    //webshell错误信息
    socketOnError () {
      this.socket.onerror = () => {
        console.log("socket 链接失败");
      };
    },
    //特殊处理
    onSend (data) {
      data = this.base.isObject(data) ? JSON.stringify(data) : data;
      data = this.base.isArray(data) ? data.toString() : data;
      data = data.replace(/\\\\/, "\\");
      this.shellWs.onSend(data);
    },
    //删除左右两端的空格
    trim (str) {
      return str.replace(/(^\s*)|(\s*$)/g, "");
    }
  }
};
</script>

2.3 样式 以及自适应屏幕大小

这一部分是因为xterm.js的FitAddon 只会横向的去适应屏幕大小,纵向他会有留白。这里是覆盖了xterm的一个原来的样式,下面的scope里面的是对字体的一些修饰

<style>
.xterm-screen{
  min-height: calc(100vh);
}
</style>

在这里插入图片描述

<!-- Add "scoped" attribute to limit CSS to this component only -->
<style>
.xterm-screen{
  min-height: calc(100vh);
}
</style>
<style scoped>
 h1, h2 {
  font-weight: normal;
}
ul {
  list-style-type: none;
  padding: 0;
}
li {
  display: inline-block;
  margin: 0 10px;
}
a {
  color: #42b983;
}
</style> 

3.后端实现

3.1 先配置一个WebSocketConfig类

@Configuration
@ComponentScan
@EnableAutoConfiguration
public class WebSocketConfig implements ServletContextInitializer {
    /**
     * 给spring容器注入这个ServerEndpointExporter对象
     * 相当于xml:
     * <beans>
     * <bean id="serverEndpointExporter" class="org.springframework.web.socket.server.standard.ServerEndpointExporter"/>
     * </beans>
     * <p>
     * 检测所有带有@serverEndpoint注解的bean并注册他们。
     *
     * @return
     */
    @Bean
    public ServerEndpointExporter serverEndpointExporter() {
        return new ServerEndpointExporter();
    }

    @Override
    public void onStartup(ServletContext servletContext) throws ServletException {
        servletContext.addListener(WebAppRootListener.class);
        servletContext.setInitParameter("org.apache.tomcat.websocket.textBufferSize","52428800");
        servletContext.setInitParameter("org.apache.tomcat.websocket.binaryBufferSize","52428800");
    }
}

3.2 SshHander类

主要的实现是这两个核心类,这里是通过ssh去连接服务器的。
这里填写你的服务器用户名,密码等

@OnOpen
        public void onOpen(javax.websocket.Session session) throws Exception {
            SessionSet.add(session);
            SshModel sshItem = new SshModel();
            sshItem.setHost("xxxx");
            sshItem.setPort(xxxx);
            sshItem.setUser("xxxx");
            sshItem.setPassword("xxxxx");
            int cnt = OnlineCount.incrementAndGet(); // 在线数加1
            log.info("有连接加入,当前连接数为:{},sessionId={}", cnt,session.getId());
            SendMessage(session, "连接成功,sessionId="+session.getId());
            HandlerItem handlerItem = new HandlerItem(session, sshItem);
            handlerItem.startRead();
            HANDLER_ITEM_CONCURRENT_HASH_MAP.put(session.getId(), handlerItem);
        }
@ServerEndpoint("/ws/ssh")
@Component
public class SshHandler {
        private static final ConcurrentHashMap<String, HandlerItem> HANDLER_ITEM_CONCURRENT_HASH_MAP = new ConcurrentHashMap<>();

        @PostConstruct
        public void init() {
            System.out.println("websocket 加载");
        }
        private static Logger log = LoggerFactory.getLogger(SshHandler.class);
        private static final AtomicInteger OnlineCount = new AtomicInteger(0);
        // concurrent包的线程安全Set,用来存放每个客户端对应的Session对象。
        private static CopyOnWriteArraySet<javax.websocket.Session> SessionSet = new CopyOnWriteArraySet<javax.websocket.Session>();


        /**
         * 连接建立成功调用的方法
         */
        @OnOpen
        public void onOpen(javax.websocket.Session session) throws Exception {
            SessionSet.add(session);
            SshModel sshItem = new SshModel();
            sshItem.setHost("xxxx");
            sshItem.setPort(xxxx);
            sshItem.setUser("xxxx");
            sshItem.setPassword("xxxxx");
            int cnt = OnlineCount.incrementAndGet(); // 在线数加1
            log.info("有连接加入,当前连接数为:{},sessionId={}", cnt,session.getId());
            SendMessage(session, "连接成功,sessionId="+session.getId());
            HandlerItem handlerItem = new HandlerItem(session, sshItem);
            handlerItem.startRead();
            HANDLER_ITEM_CONCURRENT_HASH_MAP.put(session.getId(), handlerItem);
        }

        /**
         * 连接关闭调用的方法
         */
        @OnClose
        public void onClose(javax.websocket.Session session) {
            SessionSet.remove(session);
            int cnt = OnlineCount.decrementAndGet();
            log.info("有连接关闭,当前连接数为:{}", cnt);
        }

        /**
         * 收到客户端消息后调用的方法
         * @param message
         * 客户端发送过来的消息
         */
        @OnMessage
        public void onMessage(String message, javax.websocket.Session session) throws Exception {
            log.info("来自客户端的消息:{}",message);
            //        SendMessage(session, "收到消息,消息内容:"+message);
            HandlerItem handlerItem = HANDLER_ITEM_CONCURRENT_HASH_MAP.get(session.getId());
            this.sendCommand(handlerItem, message);
        }

        /**
         * 出现错误
         * @param session
         * @param error
         */
        @OnError
        public void onError(javax.websocket.Session session, Throwable error) {
            log.error("发生错误:{},Session ID: {}",error.getMessage(),session.getId());
            error.printStackTrace();
        }

        private void sendCommand(HandlerItem handlerItem, String data) throws Exception {
            if (handlerItem.checkInput(data)) {
                handlerItem.outputStream.write(data.getBytes());
            } else {
                handlerItem.outputStream.write("没有执行相关命令权限".getBytes());
                handlerItem.outputStream.flush();
                handlerItem.outputStream.write(new byte[]{3});
            }
            handlerItem.outputStream.flush();
        }

        /**
         * 发送消息,实践表明,每次浏览器刷新,session会发生变化。
         * @param session
         * @param message
         */
        public static void SendMessage(javax.websocket.Session session, String message) {
            try {
                //            session.getBasicRemote().sendText(String.format("%s (From Server,Session ID=%s)",message,session.getId()));
                session.getBasicRemote().sendText(message);
                session.getBasicRemote().sendText("dhhw>$");
            } catch (IOException e) {
                log.error("发送消息出错:{}", e.getMessage());
                e.printStackTrace();
            }
        }

        private class HandlerItem implements Runnable {
            private final javax.websocket.Session session;
            private final InputStream inputStream;
            private final OutputStream outputStream;
            private final Session openSession;
            private final ChannelShell channel;
            private final SshModel sshItem;
            private final StringBuilder nowLineInput = new StringBuilder();

            HandlerItem(javax.websocket.Session session, SshModel sshItem) throws IOException {
                this.session = session;
                this.sshItem = sshItem;
                this.openSession = JschUtil.openSession(sshItem.getHost(), sshItem.getPort(), sshItem.getUser(), sshItem.getPassword());
                this.channel = (ChannelShell) JschUtil.createChannel(openSession, ChannelType.SHELL);
                this.inputStream = channel.getInputStream();
                this.outputStream = channel.getOutputStream();
            }

            void startRead() throws JSchException {
                this.channel.connect();
                ThreadUtil.execute(this);
            }


            /**
             * 添加到命令队列
             *
             * @param msg 输入
             * @return 当前待确认待所有命令
             */
            private String append(String msg) {
                char[] x = msg.toCharArray();
                if (x.length == 1 && x[0] == 127) {
                    // 退格键
                    int length = nowLineInput.length();
                    if (length > 0) {
                        nowLineInput.delete(length - 1, length);
                    }
                } else {
                    nowLineInput.append(msg);
                }
                return nowLineInput.toString();
            }

            public boolean checkInput(String msg) {
                String allCommand = this.append(msg);
                boolean refuse;
                if (StrUtil.equalsAny(msg, StrUtil.CR, StrUtil.TAB)) {
                    String join = nowLineInput.toString();
                    if (StrUtil.equals(msg, StrUtil.CR)) {
                        nowLineInput.setLength(0);
                    }
                    refuse = SshModel.checkInputItem(sshItem, join);
                } else {
                    // 复制输出
                    refuse = SshModel.checkInputItem(sshItem, msg);
                }
                return refuse;
            }


            @Override
            public void run() {
                try {
                    byte[] buffer = new byte[1024];
                    int i;
                    //如果没有数据来,线程会一直阻塞在这个地方等待数据。
                    while ((i = inputStream.read(buffer)) != -1) {
                        sendBinary(session, new String(Arrays.copyOfRange(buffer, 0, i), sshItem.getCharsetT()));
                    }
                } catch (Exception e) {
                    if (!this.openSession.isConnected()) {
                        return;
                    }

                    SshHandler.this.destroy(this.session);
                }
            }
        }

        public void destroy(javax.websocket.Session session) {
            HandlerItem handlerItem = HANDLER_ITEM_CONCURRENT_HASH_MAP.get(session.getId());
            if (handlerItem != null) {
                IoUtil.close(handlerItem.inputStream);
                IoUtil.close(handlerItem.outputStream);
                JschUtil.close(handlerItem.channel);
                JschUtil.close(handlerItem.openSession);
            }
            IoUtil.close(session);
            HANDLER_ITEM_CONCURRENT_HASH_MAP.remove(session.getId());
        }

        private static void sendBinary(javax.websocket.Session session, String msg) {
            //		if (!session.isOpen()) {
            //			// 会话关闭不能发送消息
            //			return;
            //		}

            //		synchronized (session.getId()) {
            //			BinaryMessage byteBuffer = new BinaryMessage(msg.getBytes());
            try {
              //  System.out.println("#####:"+msg);
                session.getBasicRemote().sendText(msg);
            } catch (IOException e) {
            }
            //		}
        }
    }

以上就是终端的实现了,希望能帮助到大家。

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

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

相关文章

反击爬虫,前端工程师的脑洞可以有多大?

1. 前言对于一张网页&#xff0c;我们往往希望它是结构良好&#xff0c;内容清晰的&#xff0c;这样搜索引擎才能准确地认知它。而反过来&#xff0c;又有一些情景&#xff0c;我们不希望内容能被轻易获取&#xff0c;比方说电商网站的交易额&#xff0c;教育网站的题目等。因为…

港科夜闻|广州市市长郭永航先生与香港科大校董会廖长城先生一行举行座谈交流...

关注并星标每周阅读港科夜闻建立新视野 开启新思维1、广州市市长郭永航先生与香港科大校董会廖长城先生一行举行座谈交流。2月9日上午&#xff0c;双方就推进香港科技大学(广州)建设发展进行深入交流&#xff0c;并一致表示&#xff0c;将全力推动落实《南沙方案》中“打造高等…

【基础篇】一文掌握css的盒子模型(margin、padding)

1、CSS 盒子模型(Box Model) 所有HTML元素可以看作盒子,在CSS中,"box model"这一术语是用来设计和布局时使用。CSS盒模型本质上是一个盒子,封装周围的HTML元素,它包括:边距,边框,填充,和实际内容。盒模型允许我们在其它元素和周围元素边框之间的空间放置元素…

文献的阅读的习惯与方法

文献阅读是每个研究人员都要做的事情&#xff0c;然而虽然每个人都在阅读&#xff0c;但是每个人的阅读效率不一样&#xff0c;总结有效的方式是非常重要的。本笔记将梳理我在阅读文献中的方法和所在其中的关注点。 阅读文献有两种目的&#xff0c;第一种目的是日常阅读和学习…

ThinkPHP5美食商城系统

有需要请私信或看评论链接哦 可远程调试 ThinkPHP5美食商城系统一 介绍 此美食商城系统基于ThinkPHP5框架开发&#xff0c;数据库mysql&#xff0c;前端bootstrap。系统角色分为用户和管理员。用户注册登录后可购买美食&#xff0c;个人中心&#xff0c;评论和反馈等&#xff…

一手教你如何搭建Hadoop基于Zookeeper的集群(5台主机)

文章目录一、设计集群图二、准备五台虚拟机2.1、下载安装文件2.2、创建虚拟机2.3、配置网络2.4、修改主机名称2.5、关闭防火墙2.6、同步时间2.7、设置/etc/hosts文件2.8、设置免密登录2.9、为后面可以主备替换安装psmisc三、安装JDK3.1、安装jdk3.2、测试jdk是否安装成功3.3、将…

Android笔记:动画

文章目录1.View Animation&#xff08;视图动画&#xff09;1.1 Tween Animation&#xff08;补间动画&#xff09;Animation 继承属性透明度alpha缩放scale移动translate旋转rotateset标签Animation父类共有函数1.2Frame Animation &#xff08;逐帧动画&#xff09;2.Propert…

spark3.0源码分析-driver-executor心跳机制

前言 driver和executor心跳机制分为两种机制&#xff1a; 1、executor发送心跳机制 2、driver接受心跳机制 至于为何要分为两种&#xff0c;原因是在分布式场景中&#xff0c;服务的稳定性是无法保障的&#xff0c;例如executor宕机后无法发送心跳&#xff0c;故driver端需要…

3、按键扫描检测处理

说明&#xff1a;本文处理按键的短按、长按检测执行&#xff0c;非矩阵按键 硬件可以类似如下连接即可&#xff0c;无需放置上下拉电阻&#xff1b; 按键动作分长按、短按(弹起时执行)两种 按下不放执行长按&#xff0c;但松开按键时不予执行短按函数 多个按键可以同时操作 按…

内网渗透(三十四)之横向移动篇-IPC配合计划任务横向移动

系列文章第一章节之基础知识篇 内网渗透(一)之基础知识-内网渗透介绍和概述 内网渗透(二)之基础知识-工作组介绍 内网渗透(三)之基础知识-域环境的介绍和优点 内网渗透(四)之基础知识-搭建域环境 内网渗透(五)之基础知识-Active Directory活动目录介绍和使用 内网渗透(六)之基…

Altium Designer输出生产文件Gerber、IPC、NC Drill、坐标文件--AD

AD软件版本&#xff1a;22.2.1 gerber文件输出共有两部分&#xff1a; 1、Gerber Files:铜皮 和 外形分别导出 2、Nc Drill Files 分3次导出 一、Gerber Files 导出2次 设定原点 ** Edit->Origin->Set** 一般板边左下角为原点&#xff0c;可以根据自己板子形状确定 导…

使用MAT工具分析OOM问题

1、添加jvm参数 保存堆内存快照 -XX:HeapDumpOnOutOfMemoryError -XX:HeapDumpPath存放路径 2、下载MAT工具 下载地址&#xff1a; https://www.eclipse.org/mat/downloads.php 3、启动如果遇到 Version 1.8.0 102 of the JVM is not suitable for this product, Version: 11…

2023LaCTFWriteup

文章目录2023LaCTFEBEa hackers notes2023LaCTF 前言&#xff1a; 累了&#xff0c;没有话&#xff0c;下次一定 EBE I was trying to send a flag to my friend over UDP, one character at a time, but it got corrupted! I think someone else was messing around with me…

【C++】类与对象(三)

前言 本章我们接替前一章继续深入理解类的默认成员函数&#xff0c;赋值重载&#xff0c;取地址重载&#xff0c;及const取地址操作符重载 但是在讲剩下的三个默认成员函数之前&#xff0c;我们要先来了解运算符重载&#xff0c;因为赋值重载&#xff0c;取地址重载&#xff0c…

Compose-Navigation简单案例上手

Navigation 快速上手 下面案例简要展示使用 Compose 版本的 Navigation 库来实现两个页面之间的跳转 这是完整的结构&#xff08;忽略掉红线划过的那个包&#xff09; 安装适用于 kotlin 的 navigation 依赖 dependencies {implementation("androidx.navigation:navigati…

兼职任务平台收集(一)分享给有需要的朋友们

互联网时代&#xff0c;给人们带来了很大的便利。信息交流、生活缴费、足不出户购物、便捷出行、线上医疗、线上教育等等很多。可以说&#xff0c;网络的时代会一直存在着。很多人也在互联网上赚到了第一桶金&#xff0c;这跟他们的努力和付出是息息相关的。所谓一份耕耘&#…

使用kubeadm部署k8s1.24.0版本,遇到的坑总结

使用kubeadm部署k8s1.24.0版本&#xff0c;遇到的坑总结环境安装遇到的问题环境 操作系统&#xff1a;centos7 内核&#xff1a;5.4.231-1.el7.elrepo.x86_64 kubeadm&#xff1a;1.24.0 kubelet&#xff1a;1.24.0 kubectl&#xff1a;1.24.0 cri&#xff1a;docker cni&…

nginx rewrite 规则

生效位置 rewrite 可以写在 server 、 location、 if 配置段内 格式 rewrite regex replacement [flag] regex是匹配的正则表达式。 只会处理$request_uri部分&#xff0c;不会匹配$host replacement 是跳转的URI。 可以以http:// 或者https:// 开头&#xff0c; 也…

自适应多因素认证:构建不可破解的企业安全防线|身份云研究院

打开本文意味着你理解信息安全的重要性&#xff0c;并且希望获取行业最佳实践来保护你所在组织的信息安全。本文将带你了解多因素认证&#xff08;MFA&#xff1a;Multi-Factor-Authentication&#xff09;对于企业信息安全的重要性以及实施方法。 多因素认证&#xff08;MFA&…

尚医通(十五)医院排班管理

目录一、医院排班管理需求1、页面效果2、接口分析二、科室列表&#xff08;接口&#xff09;1、添加service接口和实现2、添加DepartmentController方法三、科室列表&#xff08;前端&#xff09;1、添加隐藏路由2、封装api方法3、添加/views/hosp/schedule.vue组件四、排班日期…