效果图

1、功能设计说明

 终端连接之后界面上输入字符信息,后端接口是一个一个字符进行接收的,需要进行输入字符拼接简单一点只考虑回车表示需要执行命令, 退格表示删除已输入字符信息考虑到键盘上的输入光标进行移动,需要设计光标左右两边字符串记录,左箭头(将左边输入的字符截取一个拼接到右边字符前边),右箭头同理以及上下箭头切换,会切换已执行命令,①考虑将已执行的命令进行保存 ②将控制台中按到上下箭头之后打印的命令进行记录,作为输入命令字符信息(输出字符和输入字符功能联合)Home、End切换,和左右箭头一样特殊字符(ESC、DEL、CTRL等特殊输入处理)Tab自动补全功能,此功能参考上下箭头,将控制台输入信息作为输入字符,并且联合左右箭头方式,因为不确定是在哪个部位进行补全(可能会在中间进行补全),并且补全信息后端打印不是全部信息,及您是补全信息,需要拼接在左输入字符串中操作命令记录(判断是否是回车字符,如果是回车就需要进行记录日志信息)操作命令结果日志记录

经确定,每一个连接后的终端Panel中只会有一个正在运行的命令信息按照上述想法,可以在终端Panel中配置正在运行中的命令日志信息在需要执行时进行保存日志,并且配置在运行Panel中,在命令输出时进行更新日志信息需要对各种情况进行验证,并且输出字符中存在很多特殊字符,需要单独处理(需自己验证)日志记录拍错(需要自己验证各种情况)并且不能够记录敏感信息(密码等)

2、引入依赖

后端

 

       

            org.springframework.boot

            spring-boot-starter-websocket

       

前端

// 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

本文参考地址,

WebSocket+xterm+springboot+vue 实现 xshell 操作linux终端功能_vue xterm-CSDN博客

在此基础上进行更新,终端连接,并进行记录操作命令及日志信息

注:本文主要是后端部分,前端参考上述链接

3、后端代码逻辑

后端中主要有如下部分东西, 主链接Socket + Panel(终端连接信息、界面终端) + SshModel(终端连接命令信息) + SshLog(终端日志)

①终端连接Panel界面Model

package com.develop.domain.monitor.ssh;

import lombok.Data;

import lombok.Getter;

import java.util.HashMap;

import java.util.List;

import java.util.Map;

/**

* 控制台 命令行信息集合

* @author chenwentao

* @date 2024年4月24日

*/

@Getter

@Data

public class Panel {

/**

* 光标左侧字符信息 集合

*/

private StringBuilder leftBuilder;

/**

* 光标右侧字符信息 集合

*/

private StringBuilder rightBuilder;

/**

* 当前输入字符信息

*/

private StringBuilder nowBuilder;

/**

* 当前控制台窗口连接服务器信息

*/

private SshInfo sshInfo;

/**

* 当前终端连接请求参数信息集合

*/

private Map> parameterMap = new HashMap<>();

/**

* 正在执行命令

*/

private boolean running = false;

/**

* 当前正在运行中的命令日志信息, 用于校验是否进行保存命令执行结果

*/

private SshExecuteLog executeLog;

public Panel(StringBuilder leftBuilder, StringBuilder rightBuilder, StringBuilder nowBuilder) {

this.leftBuilder = leftBuilder;

this.rightBuilder = rightBuilder;

this.nowBuilder = nowBuilder;

}

public Panel() {

this.leftBuilder = new StringBuilder();

this.rightBuilder = new StringBuilder();

this.nowBuilder = new StringBuilder();

}

public Panel(SshInfo sshInfo) {

this();

this.sshInfo = sshInfo;

}

public Panel(SshInfo sshInfo, Map> parameterMap) {

this();

this.sshInfo = sshInfo;

this.parameterMap = parameterMap;

}

public String getNow() {

return nowBuilder.toString();

}

public String getSshName() {

return sshInfo.getName();

}

public String getHost() {

return sshInfo.getIp();

}

public String getSshUserName() {

return sshInfo.getUsername();

}

public String getSysName() {

return sshInfo.getName();

}

public Long getLoginUserId() {

return parameterMap.containsKey("userId") ? Long.parseLong(parameterMap.get("userId").get(0)) : 0L;

}

public String getLoginUserName() {

return parameterMap.containsKey("userName") ? parameterMap.get("userName").get(0) : "";

}

public String getUserAgent() {

return parameterMap.containsKey("userAgent") ? parameterMap.get("userAgent").get(0) : "";

}

/**

* 配置当前终端输入命令信息

*/

public Panel reBuild(StringBuilder leftBuilder, StringBuilder rightBuilder, StringBuilder nowBuilder) {

this.leftBuilder = leftBuilder;

this.rightBuilder = rightBuilder;

this.nowBuilder = nowBuilder;

return this;

}

/**

* 配置当前终端连接信息

*/

public Panel reBuild(SshInfo sshInfo) {

this.leftBuilder = new StringBuilder();

this.rightBuilder = new StringBuilder();

this.nowBuilder = new StringBuilder();

this.sshInfo = sshInfo;

return this;

}

/**

* 配置当前终端命令执行状态

*/

public Panel reBuildRun(boolean running) {

this.running = running;

return this;

}

/**

* 配置终端运行信息日志

*/

public Panel reSetLog(SshExecuteLog executeLog) {

this.executeLog = executeLog;

return this;

}

/**

* 清除日志相关信息

*/

public Panel clearLogMsg() {

this.running = false;

this.executeLog = null;

return this;

}

}

②终端SSH连接信息(包括终端链接 ip、port、name、pwd等信息)

package com.develop.domain.monitor.ssh;

import cn.hutool.core.util.CharsetUtil;

import cn.hutool.core.util.StrUtil;

import com.develop.common.core.annotation.Excel;

import com.develop.domain.ResSoftware;

import com.fasterxml.jackson.annotation.JsonFormat;

import lombok.Data;

import lombok.EqualsAndHashCode;

import java.nio.charset.Charset;

import java.util.Date;

import java.util.List;

/**

* @author wwangqian

* @date 2024/4/24

* @description ssh管理实体类

*/

@EqualsAndHashCode(callSuper = true)

@Data

public class SshInfo extends ResSoftware {

@Excel(name = "名称")

private String name;

/**

* ip地址

*/

@Excel(name = "host")

private String ip;

/**

* 系统名

*/

@Excel(name = "系统名")//暂定 后续可能改为版本名

private String resTypeName;

/**

* CPU

*/

private String cpu;

/**

* 内存

*/

private String memory;

/**

* 硬盘

*/

private String hardDrive;

/**

* 连接状态

*/

private String status;

/**

* 创建时间

*/

@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")

private Date createTime;

/**

* 修改时间

*/

@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")

private Date updateTime;

/**

* 不允许执行的命令

*/

private String notAllowedCommand;

/**

* 编码格式

*/

private String charset;

/**

* 检查是否包含禁止命令

*

* @param sshItem 实体

* @param inputItem 输入的命令

* @return false 存在禁止输入的命令

*/

public static boolean checkInputItem(SshInfo sshItem, String inputItem) {

// 检查禁止执行的命令

String notAllowedCommand = StrUtil.emptyToDefault(sshItem.getNotAllowedCommand(), StrUtil.EMPTY).toLowerCase();

if (StrUtil.isEmpty(notAllowedCommand)) {

return true;

}

List split = StrUtil.split(notAllowedCommand, StrUtil.COMMA);

inputItem = inputItem.toLowerCase();

List commands = StrUtil.split(inputItem, StrUtil.CR);

commands.addAll(StrUtil.split(inputItem, "&"));

for (String s : split) {

boolean anyMatch = commands.stream().anyMatch(item -> StrUtil.startWithAny(item, s + StrUtil.SPACE, ("&" + s + StrUtil.SPACE), StrUtil.SPACE + s + StrUtil.SPACE));

if (anyMatch) {

return false;

}

anyMatch = commands.stream().anyMatch(item -> StrUtil.equals(item, s));

if (anyMatch) {

return false;

}

}

return true;

}

public Charset getCharsetT() {

Charset charset;

try {

charset = Charset.forName(this.getCharset());

} catch (Exception e) {

charset = CharsetUtil.CHARSET_UTF_8;

}

return charset;

}

}

③终端连接日志Model

package com.develop.domain.monitor.ssh;

import com.fasterxml.jackson.annotation.JsonFormat;

import lombok.Data;

import java.util.Date;

/**

* @author wwangqian

* @date 2024/4/24

* @description

*/

@Data

public class SshExecuteLog {

/**

* id

*/

private Long id;

/**

* 终端id

*/

private Long sshId;

/**

* ssh名称

*/

private String sshName;

/**

* host(IP)

*/

private String host;

/**

* 登录终端用户名

*/

private String sshUserName;

/**

* 用户id

*/

private Long userId;

/**

* 用户名

*/

private String userName;

/**

* 系统名

*/

private String sysName;

/**

* 执行命令

*/

private String command;

/**

* 浏览器标识

*/

private String userAgent;

/**

* 操作时间

*/

@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")

private Date createTime;

/**

* 操作结果(是否成功)未成功0/成功1

*/

private Integer result;

/**

* 日志详情

*/

private String logText;

}

④终端操作命令特殊字符

package com.develop.common.core.constant;

import java.util.Arrays;

import java.util.List;

/**

* SSH 连接,键盘输入静态特殊字符常量

*/

public class SshConstants {

/**

* 终端执行命令 字符

*/

public static final String Str_CR = "\r";

/**

* 终端退格字符

*/

public static final String Str_DEL = "\u007F";

/**

* 刷新连接

*/

public static final String Str_REFRESH = "{\"data\":\"jpom-heart\"}";

/**

* 操作字符 上

*/

public static final String Str_UP = "\u001B[A";

/**

* 操作字符 下

*/

public static final String Str_DOWN = "\u001B[B";

/**

* Tab 字符自动不全

*/

public static final String Str_TAB = "\t";

/**

* 操作字符 左

*/

public static final String Str_LEFT = "\u001B[D";

/**

* 操作字符 右

*/

public static final String Str_RIGHT = "\u001B[C";

/**

* 占位符

*/

public static final String Str_BLANK = "\b";

/**

* 跳转首字符 HOME

*/

public static final String Str_HOME = "\u001B[H";

/**

* 跳转末尾字符 END

*/

public static final String Str_END = "\u001B[F";

/**

* 输出字符中 特殊字符

*/

public static final String Out_Spe_01 = "\u001B[0m";

/**

* 输出字符中 特殊字符

*/

public static final String Out_Spe_02 = "\u001B[01;34m";

/**

* 输出字符中 特殊字符 带斜杠(替换用)

*/

public static final String Out_Spe_01_Bar = "\u001B\\[0m";

/**

* 输出字符中 特殊字符 带斜杠(替换用)

*/

public static final String Out_Spe_02_Bar = "\u001B\\[01;34m";

/**

* 输出字符中 特殊字符 带斜杠(替换用)

*/

public static final String Out_Spe_03_Bar = "\u001B\\[H";

/**

* 输出字符中 特殊字符 带斜杠(替换用)

*/

public static final String Out_Spe_04_Bar = "\u001B\\[J";

/**

* 输出字符中 特殊字符 带斜杠(替换用)

*/

public static final String Out_Spe_05_Bar = "\u001B\\[01;31m";

/**

* 输出字符中 特殊字符 带斜杠(替换用)

*/

public static final String Out_Spe_06_Bar = "\u001B\\[K";

/**

* 输出字符中 特殊字符 带斜杠(替换用)

*/

public static final String Out_Spe_07_Bar = "\u001B\\[m";

/**

* 输出字符中 特殊字符 带斜杠(替换用)

*/

public static final String Out_Spe_08_Bar = "\u001B\\[01;32m";

/**

* 输出字符中 特殊字符 带斜杠(替换用)

*/

public static final String Out_Spe_09_Bar = "\u001B\\[33C";

/**

* 输出字符中 特殊字符 带斜杠(替换用)

*/

public static final String Out_Spe_10_Bar = "\u001B\\[10C";

/**

* 执行命令时,先输出 换行回车

*/

public static final String Str_RUN = "\r\n";

/**

* 左中括号

*/

public static final String Str_L_BRACKET = "[";

/**

* 右中括号

*/

public static final String Str_R_BRACKET = "]";

public static final String EMPTY = "\u0003";

/**

* 忽略需要记录日志的命令集合, 不记录日志

*/

public static final List IGNORE_LOG_CMD = Arrays.asList(

"vim", // 文件查看或者编辑模式

"tail" // 文件查看模式

);

/**

* 加密字符信息,不需要记录

*/

public static final List IGNORE_CMD_ENCRYPT = Arrays.asList(

"密码:", "password:", "密码:", "password:"

);

}

⑤使用WebSocket进行发布接口

SshWebSocketServer 接口类

package com.develop.machine.controller.monitor.ssh;

import cn.hutool.core.io.IoUtil;

import cn.hutool.core.thread.ThreadUtil;

import cn.hutool.core.util.StrUtil;

import cn.hutool.extra.ssh.ChannelType;

import cn.hutool.extra.ssh.JschUtil;

import com.develop.common.core.utils.StringUtils;

import com.develop.domain.monitor.ssh.Panel;

import com.develop.domain.monitor.ssh.SshExecuteLog;

import com.develop.domain.monitor.ssh.SshInfo;

import com.develop.machine.service.ssh.ISshExecuteLogService;

import com.develop.machine.service.ssh.ISshInfoService;

import com.jcraft.jsch.ChannelShell;

import com.jcraft.jsch.JSchException;

import org.slf4j.Logger;

import org.slf4j.LoggerFactory;

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.stereotype.Component;

import org.springframework.web.socket.WebSocketSession;

import org.springframework.web.socket.handler.TextWebSocketHandler;

import javax.annotation.PostConstruct;

import javax.websocket.*;

import javax.websocket.server.PathParam;

import javax.websocket.server.ServerEndpoint;

import java.io.IOException;

import java.io.InputStream;

import java.io.OutputStream;

import java.util.*;

import java.util.concurrent.ConcurrentHashMap;

import java.util.concurrent.CopyOnWriteArraySet;

import java.util.concurrent.atomic.AtomicInteger;

import static com.develop.common.core.constant.SshConstants.*;

/**

* @author chenwentao

* @des ssh终端连接 WebSocket

* @date 2024年4月23日

*/

@ServerEndpoint("/ws/ssh/{terminalId}")

@Component

public class SshWebSocketServer {

/**

* 终端连接 终端日志信息查询服务

*/

private static ISshExecuteLogService iSshExecuteLogService;

/**

* 终端连接 终端信息服务

*/

private static ISshInfoService sshInfoService;

@Autowired

public void setISshExecuteLogService(ISshExecuteLogService iSshExecuteLogService) {

SshWebSocketServer.iSshExecuteLogService = iSshExecuteLogService;

}

@Autowired

public void setSshInfoService(ISshInfoService sshInfoService) {

SshWebSocketServer.sshInfoService = sshInfoService;

}

/**

* 所有的终端连接信息 key:sessionID, value:终端连接

*/

private static final ConcurrentHashMap HANDLER_ITEM_CONCURRENT_HASH_MAP = new ConcurrentHashMap<>();

@PostConstruct

public void init() {

logger.info("SSH 终端连接服务 WebSocket 初始化 SUCCESS");

}

private static final Logger logger = LoggerFactory.getLogger(SshWebSocketServer.class);

/**

* 在线用户数量

*/

private static final AtomicInteger OnlineCount = new AtomicInteger(0);

/**

* concurrent包的线程安全Set,用来存放每个客户端对应的Session对象。

*/

private static final CopyOnWriteArraySet SessionSet = new CopyOnWriteArraySet<>();

/**

* 存放当前连接终端的 命令行信息

* value 处理命令行, 左移、右移,用两个字符串进行拼接

* value 集合固定存在三个值,0 光标左侧字符, 1 光标右侧字符, 2 当前输入字符, 后期处理为实体

*/

private static final Map CMD_LINE_MAP = new ConcurrentHashMap<>();

/**

* 连接建立成功调用的方法

*

* @param session 连接 session

* @param terminalId 终端ID

*/

@OnOpen

public void onOpen(Session session, @PathParam("terminalId") Long terminalId) throws Exception {

SessionSet.add(session);

// 配置 ssh 连接信息, 目前先做定值,之后从库表进行 查询处理

SshInfo sshInfo = sshInfoService.selectSshInfoById(terminalId);

sshInfo.setIp("192.168.168.129");

sshInfo.setPort("22");

sshInfo.setUsername("root");

sshInfo.setPassword("123456");

int cnt = OnlineCount.incrementAndGet(); // 在线数加1

logger.info("有连接加入,当前连接数为:{},sessionId={}", cnt, session.getId());

SendMessage(session, "连接成功,sessionId=" + session.getId());

// 终端开始连接

HandlerItem handlerItem = new HandlerItem(session, sshInfo);

handlerItem.startRead();

// 获取请求参数信息集合

// 传参:userAgent 浏览器版本信息, userId 登录用户id信息, userName 登录用户名信息

Map> parameterMap = session.getRequestParameterMap();

// 终端连接成功, 初始化当前终端命令行集合

CMD_LINE_MAP.put(session, new Panel(sshInfo, parameterMap));

// 终端初始化连接时,将连接信息进行存储

HANDLER_ITEM_CONCURRENT_HASH_MAP.put(session.getId(), handlerItem);

}

/**

* 连接关闭调用的方法

*/

@OnClose

public void onClose(Session session) {

SessionSet.remove(session);

int cnt = OnlineCount.decrementAndGet();

logger.info("有连接关闭,当前连接数为:{}", cnt);

}

/**

* 收到客户端消息后调用的方法

*

* @param message 客户端发送过来的消息

* @param session 连接 session 信息

*/

@OnMessage

public void onMessage(String message, Session session, @PathParam("terminalId") Long terminalId) throws Exception {

// 前端定时刷新终端 特殊字符,保证终端不断链, 进行过滤,不执行

if (Str_REFRESH.equals(message)) {

return;

}

// 终端不包括当前session ,需要进行重新连接

if (!SessionSet.contains(session)) {

onOpen(session, terminalId);

return;

}

// 校验链接信息

Panel panel = CMD_LINE_MAP.get(session);

if (Objects.isNull(panel.getSshInfo()) || Objects.isNull(panel.getParameterMap())) {

CMD_LINE_MAP.remove(session);

SessionSet.remove(session);

onOpen(session, terminalId); // 重连接

return;

}

// 先记录一条日志,然后在进行修改状态

// 处理当前终端命令集合

// 目前需要特殊处理字符 Enter + 退格 + ↑ ↓ ← → + Tab + Home + End

StringBuilder leftBuilder = panel.getLeftBuilder();

StringBuilder rightBuilder = panel.getRightBuilder();

SshExecuteLog executeLog = null; // 运行中命令记录信息, 用于后续保存命令执行结果

if (Str_CR.equals(message)) {

logger.info("执行人:{}, 当前终端输入命令:{}", session.getId(), dealCmdBlank(leftBuilder.append(rightBuilder).toString()));

executeLog = insertSSHCmdLog(dealCmdBlank(leftBuilder.append(rightBuilder).toString()), panel);

panel = panel.reBuild(panel.getSshInfo());

if (Objects.nonNull(executeLog)) panel = panel.reSetLog(executeLog);

}

// 控制台命令执行结果

boolean cmdRunSuccess = true;

try {

HandlerItem handlerItem = HANDLER_ITEM_CONCURRENT_HASH_MAP.get(session.getId());

this.sendCommand(handlerItem, message);

} catch (Exception e) {

cmdRunSuccess = false;

logger.error("执行命令出错:", e);

}

if (Str_CR.equals(message)) {

if (!cmdRunSuccess && Objects.nonNull(executeLog)) {

// 如果是需要记录日志,在上边已经记录过,并且是运行成功日志,此处进行跟更新状态,如果是失败,继续更新

executeLog.setResult(0); // 执行失败

iSshExecuteLogService.updateExecuteLog(executeLog);

}

}

else if (Str_DEL.equals(message) && leftBuilder.length() != 0) {

// 退格,需要将控制台待执行命令删除一个字符(字符不为空)

panel = panel.reBuild(leftBuilder.deleteCharAt(leftBuilder.length() - 1), rightBuilder, new StringBuilder(message));

}

else if (Str_LEFT.equals(message)) {

// 左移,需要将控制台待执行命令左移一个字符(字符不为空)

panel = builderLeftMove(leftBuilder, rightBuilder, message, panel);

}

else if (Str_RIGHT.equals(message)) {

// 右移,需要将控制台待执行命令右移一个字符(字符不为空)

panel = builderRightMove(leftBuilder, rightBuilder, message, panel);

}

else if (StringUtils.equalsAny(message, Str_UP, Str_DOWN)) {

// 上下箭头,表示处理待执行命令为上一个或者下一个已执行命令

panel = panel.reBuild(new StringBuilder(message), new StringBuilder(), new StringBuilder(message));

}

else if (Str_HOME.equals(message)) {

// 跳转首字符

panel = panel.reBuild(new StringBuilder(), leftBuilder.append(rightBuilder), new StringBuilder(message));

}

else if (Str_END.equals(message)) {

// 跳转末尾字符

panel = panel.reBuild(leftBuilder.append(rightBuilder), new StringBuilder(), new StringBuilder(message));

}

else if (Str_TAB.equals(message)) {

// Tab 自动补全时,使用控制台输出数据作为待执行命令,补全需要替换的永远是左边字符串

panel = panel.reBuild(leftBuilder, rightBuilder, new StringBuilder(message));

}

else {

// 将日志添加到到执行行

panel = panel.reBuild(leftBuilder.append(message), rightBuilder, new StringBuilder(message));

}

CMD_LINE_MAP.put(session, panel);

}

/**

* 保存执行日志信息

* @param runCmd 执行命令

*/

private SshExecuteLog insertSSHCmdLog(String runCmd, Panel panel) {

// 存在命令数据时,进行保存日志,不存在不保存,并且需要过滤无用的日志信息

if (StringUtils.isEmpty(runCmd)) {

return null;

}

// 过滤无用的日志信息 + 密码输入信息

if (StringUtils.equalsAny(runCmd, Str_REFRESH, Str_CR, Str_DEL, Str_LEFT, Str_RIGHT, Str_UP, Str_DOWN, Str_HOME, Str_END, Str_TAB)

|| IGNORE_CMD_ENCRYPT.stream().anyMatch(runCmd::startsWith)) {

return null;

}

try {

SshExecuteLog executeLog = getSshExecuteLog(runCmd, panel);

iSshExecuteLogService.insertExecuteLog(executeLog);

logger.info("保存执行日志信息成功, 日志 ID【{}】", executeLog.getId());

return executeLog;

} catch (Exception e) {

logger.error("保存执行日志信息出错, 执行命令信息【{}】 :", runCmd, e);

}

return null;

}

/**

* 构造执行命令日志 信息

* @param runCmd 执行命令

* @param panel 当前连接 panel

* @return 执行命令日志信息

*/

private static SshExecuteLog getSshExecuteLog(String runCmd, Panel panel) {

while (runCmd.startsWith(EMPTY)) {

runCmd = runCmd.substring(EMPTY.length()); // 处理掉 空字符

}

SshExecuteLog executeLog = new SshExecuteLog();

executeLog.setSshId(panel.getSshInfo().getId());

executeLog.setSshName(panel.getSshName());

executeLog.setHost(panel.getHost());

executeLog.setSshUserName(panel.getSshUserName());

executeLog.setUserName(panel.getLoginUserName());

executeLog.setUserId(panel.getLoginUserId());

executeLog.setSysName(panel.getSysName());

executeLog.setCommand(runCmd);

executeLog.setUserAgent(panel.getUserAgent());

executeLog.setCreateTime(new Date());

executeLog.setResult(1); // 默认执行成功

return executeLog;

}

/**

* 处理执行命令中的 特殊字符信息

* @param cmdBuilder 执行命令

* @return 处理后执行命令

*/

private String dealCmdBlank(String cmdBuilder) {

while (cmdBuilder.startsWith(Str_BLANK)) {

cmdBuilder = cmdBuilder.substring(Str_BLANK.length());

}

return cmdBuilder;

}

/**

* 输入字符串左移 (因为鼠标光标可能会左移右移)

* @param leftBuilder 左边字符串

* @param rightBuilder 右边字符串

* @param nowInput 当前输入字符

* @param panel 当前连接 panel

* @return 命令行

*/

private Panel builderLeftMove(StringBuilder leftBuilder, StringBuilder rightBuilder, String nowInput, Panel panel) {

if (leftBuilder.length() == 0) { // 无需左移

return panel.reBuild(leftBuilder, rightBuilder, new StringBuilder(nowInput));

}

rightBuilder = new StringBuilder(leftBuilder.substring(leftBuilder.length() - 1) + rightBuilder);

leftBuilder = new StringBuilder(leftBuilder.substring(0, leftBuilder.length() - 1));

return panel.reBuild(leftBuilder, rightBuilder, new StringBuilder(nowInput));

}

/**

* 输入字符串右移 (因为鼠标光标可能会左移右移)

* @param leftBuilder 左边字符串

* @param rightBuilder 右边字符串

* @param nowInput 当前输入字符

* @param panel 当前连接 panel

* @return 命令行

*/

private Panel builderRightMove(StringBuilder leftBuilder, StringBuilder rightBuilder, String nowInput, Panel panel) {

if (rightBuilder.length() == 0) { // 无需右移

return panel.reBuild(leftBuilder, rightBuilder, new StringBuilder(nowInput));

}

leftBuilder = new StringBuilder(leftBuilder + rightBuilder.substring(0, 1));

rightBuilder = new StringBuilder(rightBuilder.substring(1));

return panel.reBuild(leftBuilder, rightBuilder, new StringBuilder(nowInput));

}

/**

* 出现错误

*

* @param session 连接 session

* @param error 异常信息

*/

@OnError

public void onError(Session session, Throwable error) {

logger.error("发生错误:{},Session ID: {}", error.getMessage(), session.getId());

logger.error("终端连接异常", error);

}

/**

* 终端进行执行命令

* @param handlerItem 终端连接信息

* @param data 终端待执行 命令

* @throws Exception 终端异常

*/

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 session 信息

* @param message 前端终端输出信息

*/

public static void SendMessage(Session session, String message) {

try {

session.getBasicRemote().sendText(Str_REFRESH.equals(message) ? "" : message);

} catch (IOException e) {

logger.error("发送消息出错:{}", e.getMessage());

}

}

/**

* 终端连接model

*/

private class HandlerItem implements Runnable {

private final Session session; // 终端 session

private final InputStream inputStream; // 终端输出信息

private final OutputStream outputStream; // 终端待执行信息

private final com.jcraft.jsch.Session openSession; // 正在打开的session

private final ChannelShell channel; // 终端面板

private final SshInfo sshItem; // 当前终端连接配置

private final StringBuilder nowLineInput = new StringBuilder(); // 当前行输入信息

HandlerItem(Session session, SshInfo sshItem) throws IOException {

this.session = session;

this.sshItem = sshItem;

this.openSession = JschUtil.openSession(sshItem.getIp(), Integer.parseInt(sshItem.getPort()), sshItem.getUsername(), 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 输入

*/

private void 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);

}

}

/**

* 校验输入信息

* @param msg 输入命令 字符

* @return 校验结果

*/

public boolean checkInput(String msg) {

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 = SshInfo.checkInputItem(sshItem, join);

} else {

// 复制输出

refuse = SshInfo.checkInputItem(sshItem, msg);

}

return refuse;

}

@Override

public void run() {

try {

byte[] buffer = new byte[1024];

int i;

//如果没有数据来,线程会一直阻塞在这个地方等待数据。

while ((i = inputStream.read(buffer)) != -1) {

String result = new String(Arrays.copyOfRange(buffer, 0, i), sshItem.getCharsetT());

// 处理已输入数据信息, 上下箭头,切换指标

changeUpDownTabCmd(session, result);

// 处理命令行输入 密码相关信息,不进行保存

dealPwdCmd(result, session, CMD_LINE_MAP.get(session));

SendMessage(session, result);

}

} catch (Exception e) {

logger.error("终端连接异常", e);

if (!this.openSession.isConnected()) {

return;

}

SshWebSocketServer.this.destroy(this.session);

}

}

}

/**

* 处理命令行输入 密码相关信息,不进行保存

* @param result 输出填写密码信息

* @param session session

* @param panel 终端

*/

private void dealPwdCmd(String result, Session session, Panel panel) {

if (IGNORE_CMD_ENCRYPT.stream().anyMatch(result::equals)) {

CMD_LINE_MAP.put(session, panel.reBuild(new StringBuilder(result), new StringBuilder(), new StringBuilder(result)));

}

}

/**

* 使用上下箭头切换时、Tab自动补全,处理命令行结果记录

* @param session session信息

* @param printMsg 控制台输出信息(上一条命令、下一条命令)

*/

private void changeUpDownTabCmd(Session session, String printMsg) {

if (!CMD_LINE_MAP.containsKey(session)) {

return;

}

Panel panel = CMD_LINE_MAP.get(session);

if (Str_RUN.equals(printMsg)) {

// 设置当前终端正在运行中

CMD_LINE_MAP.put(session, panel.reBuildRun(true));

return;

}

// 正常情况下是先输出 \r\n 进行换行,存在某些情况 \r\n 是和结果一块输出的

if (panel.isRunning()) {

updateLogResult(session, panel, printMsg);

return;

}

if (printMsg.startsWith(Str_RUN)) { // \r\n 是和结果一块输出的

wrapInOut(session, panel, printMsg);

return;

}

// 获取输入命令

StringBuilder leftBuilder = panel.getLeftBuilder();

StringBuilder rightBuilder = panel.getRightBuilder();

StringBuilder nowInput = panel.getNowBuilder();

// 如果使用 上下 箭头进行切换命令时,将控制台结果添加到命令行记录里

if (StringUtils.equalsAny(nowInput, Str_UP, Str_DOWN)) {

CMD_LINE_MAP.put(session, panel.reBuild(new StringBuilder(printMsg), new StringBuilder(), new StringBuilder(nowInput)));

}

else if (StringUtils.equalsAny(nowInput, Str_TAB)) {

// Tab 数据自动补全时,将补全数据拼接在光标左侧数据中

leftBuilder.append(printMsg);

CMD_LINE_MAP.put(session, panel.reBuild(leftBuilder, rightBuilder, new StringBuilder(Str_TAB)));

}

}

private void wrapInOut(Session session, Panel panel, String printMsg) {

// 保存执行日志信息

if (Objects.isNull(panel.getExecuteLog())) {

return;

}

// 特殊命令字符,不需要进行记录命令执行结果

if (ignoreSaveLog(panel.getExecuteLog())) {

return;

}

String result = printMsg.substring(Str_RUN.length());

result = replaceOutSpe(result);

if (result.contains(Str_L_BRACKET)) result = result.substring(0, result.lastIndexOf(Str_L_BRACKET)).trim();

updateLogAndResetSession(result, session, panel);

}

private void updateLogResult(Session session, Panel panel, String printMsg) {

// 保存执行日志信息

if (Objects.isNull(panel.getExecuteLog())) {

return;

}

// 特殊命令字符,不需要进行记录命令执行结果

if (ignoreSaveLog(panel.getExecuteLog())) {

return;

}

// 处理待保存日志

String result = replaceOutSpe(printMsg);

// 处理掉最后一行的 [root@localhost home]# 数据

if (result.contains(Str_L_BRACKET)) result = result.substring(0, result.lastIndexOf(Str_L_BRACKET)).trim();

updateLogAndResetSession(result, session, panel);

}

/**

* 是否需要忽略记录命令结果

* @param log 命令日志信息

* @return 是否需要忽略

*/

private boolean ignoreSaveLog(SshExecuteLog log) {

return IGNORE_LOG_CMD.stream().anyMatch(log.getCommand()::startsWith);

}

/**

* 更新命令执行结果并且 更新缓存信息

* @param result 命令执行结果

* @param session session

* @param panel 终端连接信息

*/

private void updateLogAndResetSession(String result, Session session, Panel panel) {

if (StringUtils.isEmpty(result)) {

return;

}

SshExecuteLog executeLog = panel.getExecuteLog();

executeLog.setLogText(result);

iSshExecuteLogService.updateExecuteLog(executeLog);

CMD_LINE_MAP.put(session, panel.clearLogMsg());

}

/**

* 终端连接销毁

* @param session 终端连接session信息

*/

public void destroy(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());

}

/**

* 处理输出字符中的特殊字符信息

* @param printMsg 特殊字符

* @return 除了字符信息

*/

private String replaceOutSpe(String printMsg) {

return printMsg.replaceAll(Out_Spe_01_Bar, "").replaceAll(Out_Spe_02_Bar, "")

.replaceAll(Out_Spe_03_Bar, "").replaceAll(Out_Spe_04_Bar, "")

.replaceAll(Out_Spe_05_Bar, "").replaceAll(Out_Spe_06_Bar, "")

.replaceAll(Out_Spe_07_Bar, "").replaceAll(Out_Spe_08_Bar, "")

.replaceAll(Out_Spe_09_Bar, "").replaceAll(Out_Spe_10_Bar, "");

}

}

4、日志记录

        上述功能中记录的比较简单的日志信息,截图如下

精彩文章

评论可见,请评论后查看内容,谢谢!!!
 您阅读本篇文章共花了: