效果图
1、功能设计说明
终端连接之后界面上输入字符信息,后端接口是一个一个字符进行接收的,需要进行输入字符拼接简单一点只考虑回车表示需要执行命令, 退格表示删除已输入字符信息考虑到键盘上的输入光标进行移动,需要设计光标左右两边字符串记录,左箭头(将左边输入的字符截取一个拼接到右边字符前边),右箭头同理以及上下箭头切换,会切换已执行命令,①考虑将已执行的命令进行保存 ②将控制台中按到上下箭头之后打印的命令进行记录,作为输入命令字符信息(输出字符和输入字符功能联合)Home、End切换,和左右箭头一样特殊字符(ESC、DEL、CTRL等特殊输入处理)Tab自动补全功能,此功能参考上下箭头,将控制台输入信息作为输入字符,并且联合左右箭头方式,因为不确定是在哪个部位进行补全(可能会在中间进行补全),并且补全信息后端打印不是全部信息,及您是补全信息,需要拼接在左输入字符串中操作命令记录(判断是否是回车字符,如果是回车就需要进行记录日志信息)操作命令结果日志记录
经确定,每一个连接后的终端Panel中只会有一个正在运行的命令信息按照上述想法,可以在终端Panel中配置正在运行中的命令日志信息在需要执行时进行保存日志,并且配置在运行Panel中,在命令输出时进行更新日志信息需要对各种情况进行验证,并且输出字符中存在很多特殊字符,需要单独处理(需自己验证)日志记录拍错(需要自己验证各种情况)并且不能够记录敏感信息(密码等)
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
本文参考地址,
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
/**
* 正在执行命令
*/
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
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
inputItem = inputItem.toLowerCase();
List
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
"vim", // 文件查看或者编辑模式
"tail" // 文件查看模式
);
/**
* 加密字符信息,不需要记录
*/
public static final List
"密码:", "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
@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
/**
* 存放当前连接终端的 命令行信息
* value 处理命令行, 左移、右移,用两个字符串进行拼接
* value 集合固定存在三个值,0 光标左侧字符, 1 光标右侧字符, 2 当前输入字符, 后期处理为实体
*/
private static final Map
/**
* 连接建立成功调用的方法
*
* @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
// 终端连接成功, 初始化当前终端命令行集合
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、日志记录
上述功能中记录的比较简单的日志信息,截图如下
文章链接
发表评论