jpom-parent 542 B

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267
  1. commit d053fa5449f328407b88d6d6092a6bfcd69acfd6
  2. Author: bwcx_jzy <bwcx_jzy@163.com>
  3. Date: Wed Jun 11 16:38:28 2025 +0800
  4. fix(server): 修复终端输入命令时按 Backspace 会退出终端的问题
  5. - 在 KeyEventCycle 类中添加了对 Backspace 按键的处理逻辑
  6. - 优化了 SshHandler 类中的数据接收流程,增加了异常捕获
  7. - 新增了两个测试类 SSHCommandRecorder 和 TerminalParserRecorder,用于记录和解析 SSH 终端命令
  8. diff --git a/CHANGELOG-BETA.md b/CHANGELOG-BETA.md
  9. index 98e437387..5c2378056 100644
  10. --- a/CHANGELOG-BETA.md
  11. +++ b/CHANGELOG-BETA.md
  12. @@ -9,6 +9,7 @@
  13. ### 🐞 解决BUG、优化功能
  14. 1. 【server】优化 数据库表支持配置前缀 `jpom.db.table-prefix` (感谢@ccx2480)
  15. +2. 【server】修复 终端输入命令,按Backspace 会退出终端(感谢[@dgs](https://gitee.com/dgs0924) [Gitee issues ICA57K](https://gitee.com/dromara/Jpom/issues/ICA57K) )
  16. ------
  17. diff --git a/modules/server/src/main/java/org/dromara/jpom/socket/handler/KeyEventCycle.java b/modules/server/src/main/java/org/dromara/jpom/socket/handler/KeyEventCycle.java
  18. index ac6da4493..f7189094e 100644
  19. --- a/modules/server/src/main/java/org/dromara/jpom/socket/handler/KeyEventCycle.java
  20. +++ b/modules/server/src/main/java/org/dromara/jpom/socket/handler/KeyEventCycle.java
  21. @@ -165,7 +165,8 @@ public class KeyEventCycle {
  22. }
  23. }
  24. str = new String(Arrays.copyOfRange(bytes, 0, bytes.length - backCount), charset);
  25. - buffer.insert(inputSelection-1, str);
  26. + // #https://gitee.com/dromara/Jpom/issues/ICA57K
  27. + buffer.insert(inputSelection - 1, str);
  28. inputSelection += str.length();
  29. }
  30. }
  31. @@ -175,7 +176,7 @@ public class KeyEventCycle {
  32. /**
  33. * 查找指定字节数组在原始字节数组中的位置
  34. *
  35. - * @param originalArray 原始字节数组
  36. + * @param originalArray 原始字节数组
  37. * @param byteArrayToFind 要查找的字节数组
  38. * @return 找到的位置索引,如果找不到返回 -1
  39. */
  40. diff --git a/modules/server/src/main/java/org/dromara/jpom/socket/handler/SshHandler.java b/modules/server/src/main/java/org/dromara/jpom/socket/handler/SshHandler.java
  41. index 965d4cbb0..1ac6bc69e 100644
  42. --- a/modules/server/src/main/java/org/dromara/jpom/socket/handler/SshHandler.java
  43. +++ b/modules/server/src/main/java/org/dromara/jpom/socket/handler/SshHandler.java
  44. @@ -23,7 +23,6 @@ import com.alibaba.fastjson2.JSONValidator;
  45. import com.jcraft.jsch.ChannelShell;
  46. import com.jcraft.jsch.JSchException;
  47. import com.jcraft.jsch.Session;
  48. -import lombok.Setter;
  49. import lombok.extern.slf4j.Slf4j;
  50. import org.dromara.jpom.common.i18n.I18nMessageUtil;
  51. import org.dromara.jpom.common.i18n.I18nThreadUtil;
  52. @@ -42,7 +41,6 @@ import org.springframework.http.HttpHeaders;
  53. import org.springframework.web.socket.TextMessage;
  54. import org.springframework.web.socket.WebSocketSession;
  55. -import java.io.ByteArrayOutputStream;
  56. import java.io.IOException;
  57. import java.io.InputStream;
  58. import java.io.OutputStream;
  59. @@ -52,7 +50,6 @@ import java.util.Collections;
  60. import java.util.List;
  61. import java.util.Map;
  62. import java.util.concurrent.ConcurrentHashMap;
  63. -import java.util.function.Consumer;
  64. /**
  65. * ssh 处理2
  66. @@ -304,7 +301,11 @@ public class SshHandler extends BaseTerminalHandler {
  67. //如果没有数据来,线程会一直阻塞在这个地方等待数据。
  68. while ((i = inputStream.read(buffer)) != NioUtil.EOF) {
  69. byte[] tempBytes = Arrays.copyOfRange(buffer, 0, i);
  70. - keyEventCycle.receive(tempBytes);
  71. + try {
  72. + keyEventCycle.receive(tempBytes);
  73. + } catch (Exception e) {
  74. + log.warn("keyEventCycle.receive", e);
  75. + }
  76. sendBinary(session, new String(tempBytes, machineSshModel.charset()));
  77. }
  78. } catch (Exception e) {
  79. diff --git a/modules/sub-plugin/ssh-jsch/src/test/java/SSHCommandRecorder.java b/modules/sub-plugin/ssh-jsch/src/test/java/SSHCommandRecorder.java
  80. new file mode 100644
  81. index 000000000..8a863ba39
  82. --- /dev/null
  83. +++ b/modules/sub-plugin/ssh-jsch/src/test/java/SSHCommandRecorder.java
  84. @@ -0,0 +1,725 @@
  85. +/**
  86. + * @author bwcx_jzy
  87. + * @since 2025/6/11
  88. + */
  89. +import com.jcraft.jsch.*;
  90. +import java.io.*;
  91. +import java.nio.charset.StandardCharsets;
  92. +import java.util.*;
  93. +import java.util.concurrent.*;
  94. +import java.util.regex.Pattern;
  95. +
  96. +/**
  97. + * SSH终端命令记录器 - 重构版
  98. + * 通过双向流拦截和智能解析准确记录用户执行的所有命令
  99. + */
  100. +public class SSHCommandRecorder {
  101. +
  102. + // ANSI转义序列模式
  103. + private static final Pattern ANSI_ESCAPE = Pattern.compile("\\x1B\\[[0-?]*[ -/]*[@-~]");
  104. +
  105. + // 命令提示符模式(支持多种shell)
  106. + private static final Pattern PROMPT_PATTERN = Pattern.compile(
  107. + ".*?[\\$#>%]\\s*$|.*?\\w+[@:].*?[\\$#>%]\\s*$|.*?\\]\\s*[\\$#>%]\\s*$"
  108. + );
  109. +
  110. + // 会话相关
  111. + private Session session;
  112. + private ChannelShell channel;
  113. + private InputStream channelInput;
  114. + private OutputStream channelOutput;
  115. +
  116. + // 数据处理
  117. + private final BlockingQueue<String> inputQueue = new LinkedBlockingQueue<>();
  118. + private final BlockingQueue<String> outputQueue = new LinkedBlockingQueue<>();
  119. + private final List<CommandRecord> commandHistory = Collections.synchronizedList(new ArrayList<>());
  120. +
  121. + // 状态管理
  122. + private volatile boolean recording = false;
  123. + private final TerminalState terminalState = new TerminalState();
  124. +
  125. + // 线程池
  126. + private final ExecutorService executorService = Executors.newFixedThreadPool(4);
  127. +
  128. + /**
  129. + * 命令记录实体
  130. + */
  131. + public static class CommandRecord {
  132. + private final String command;
  133. + private final String user;
  134. + private final String host;
  135. + private final long timestamp;
  136. + private final String sessionId;
  137. + private String output;
  138. + private CommandType type;
  139. + private long duration;
  140. +
  141. + public enum CommandType {
  142. + TYPED, // 直接输入
  143. + HISTORY_UP, // 上键选择
  144. + HISTORY_DOWN, // 下键选择
  145. + SEARCH, // 搜索选择
  146. + TAB_COMPLETE // Tab补全
  147. + }
  148. +
  149. + public CommandRecord(String command, String user, String host, String sessionId) {
  150. + this.command = command.trim();
  151. + this.user = user;
  152. + this.host = host;
  153. + this.sessionId = sessionId;
  154. + this.timestamp = System.currentTimeMillis();
  155. + this.type = CommandType.TYPED;
  156. + }
  157. +
  158. + // Getters and Setters
  159. + public String getCommand() { return command; }
  160. + public String getUser() { return user; }
  161. + public String getHost() { return host; }
  162. + public long getTimestamp() { return timestamp; }
  163. + public String getSessionId() { return sessionId; }
  164. + public String getOutput() { return output; }
  165. + public CommandType getType() { return type; }
  166. + public long getDuration() { return duration; }
  167. +
  168. + public void setOutput(String output) { this.output = output; }
  169. + public void setType(CommandType type) { this.type = type; }
  170. + public void setDuration(long duration) { this.duration = duration; }
  171. +
  172. + @Override
  173. + public String toString() {
  174. + return String.format("[%s] %s@%s [%s] $ %s",
  175. + new Date(timestamp), user, host, type, command);
  176. + }
  177. + }
  178. +
  179. + /**
  180. + * 终端状态跟踪
  181. + */
  182. + private static class TerminalState {
  183. + private final StringBuilder currentLine = new StringBuilder();
  184. + private final StringBuilder commandBuffer = new StringBuilder();
  185. + private String lastPrompt = "";
  186. + private String currentCommand = "";
  187. + private boolean inCommand = false;
  188. + private boolean waitingForOutput = false;
  189. + private long commandStartTime = 0;
  190. + private int cursorPosition = 0;
  191. +
  192. + // 特殊键检测
  193. + private boolean upKeyPressed = false;
  194. + private boolean downKeyPressed = false;
  195. + private boolean ctrlRPressed = false;
  196. + private boolean tabPressed = false;
  197. +
  198. + public synchronized void reset() {
  199. + currentLine.setLength(0);
  200. + commandBuffer.setLength(0);
  201. + currentCommand = "";
  202. + inCommand = false;
  203. + waitingForOutput = false;
  204. + commandStartTime = 0;
  205. + resetKeyStates();
  206. + }
  207. +
  208. + public synchronized void resetKeyStates() {
  209. + upKeyPressed = false;
  210. + downKeyPressed = false;
  211. + ctrlRPressed = false;
  212. + tabPressed = false;
  213. + }
  214. + }
  215. +
  216. + /**
  217. + * 输入流拦截器
  218. + */
  219. + private class InputStreamInterceptor extends InputStream {
  220. + private final InputStream originalInput;
  221. + private final ByteArrayOutputStream buffer = new ByteArrayOutputStream();
  222. +
  223. + public InputStreamInterceptor(InputStream originalInput) {
  224. + this.originalInput = originalInput;
  225. + }
  226. +
  227. + @Override
  228. + public int read() throws IOException {
  229. + int data = originalInput.read();
  230. + if (data != -1) {
  231. + buffer.write(data);
  232. + processInputByte((byte) data);
  233. + }
  234. + return data;
  235. + }
  236. +
  237. + @Override
  238. + public int read(byte[] b, int off, int len) throws IOException {
  239. + int bytesRead = originalInput.read(b, off, len);
  240. + if (bytesRead > 0) {
  241. + buffer.write(b, off, bytesRead);
  242. + processInputBytes(b, off, bytesRead);
  243. + }
  244. + return bytesRead;
  245. + }
  246. +
  247. + private void processInputByte(byte b) {
  248. + processInputBytes(new byte[]{b}, 0, 1);
  249. + }
  250. +
  251. + private void processInputBytes(byte[] bytes, int offset, int length) {
  252. + try {
  253. + String input = new String(bytes, offset, length, StandardCharsets.UTF_8);
  254. + if (!input.isEmpty()) {
  255. + inputQueue.offer(input);
  256. + }
  257. + } catch (Exception e) {
  258. + System.err.println("Error processing input: " + e.getMessage());
  259. + }
  260. + }
  261. + }
  262. +
  263. + /**
  264. + * 输出流拦截器
  265. + */
  266. + private class OutputStreamInterceptor extends OutputStream {
  267. + private final OutputStream originalOutput;
  268. + private final ByteArrayOutputStream buffer = new ByteArrayOutputStream();
  269. +
  270. + public OutputStreamInterceptor(OutputStream originalOutput) {
  271. + this.originalOutput = originalOutput;
  272. + }
  273. +
  274. + @Override
  275. + public void write(int b) throws IOException {
  276. + originalOutput.write(b);
  277. + buffer.write(b);
  278. + processOutputByte((byte) b);
  279. + }
  280. +
  281. + @Override
  282. + public void write(byte[] b, int off, int len) throws IOException {
  283. + originalOutput.write(b, off, len);
  284. + buffer.write(b, off, len);
  285. + processOutputBytes(b, off, len);
  286. + }
  287. +
  288. + @Override
  289. + public void flush() throws IOException {
  290. + originalOutput.flush();
  291. + }
  292. +
  293. + private void processOutputByte(byte b) {
  294. + processOutputBytes(new byte[]{b}, 0, 1);
  295. + }
  296. +
  297. + private void processOutputBytes(byte[] bytes, int offset, int length) {
  298. + try {
  299. + String output = new String(bytes, offset, length, StandardCharsets.UTF_8);
  300. + if (!output.isEmpty()) {
  301. + outputQueue.offer(output);
  302. + }
  303. + } catch (Exception e) {
  304. + System.err.println("Error processing output: " + e.getMessage());
  305. + }
  306. + }
  307. + }
  308. +
  309. + /**
  310. + * 建立SSH连接并开始记录
  311. + */
  312. + public void connect(String host, int port, String username, String password) throws Exception {
  313. + JSch jsch = new JSch();
  314. + session = jsch.getSession(username, host, port);
  315. + session.setPassword(password);
  316. + session.setConfig("StrictHostKeyChecking", "no");
  317. + session.connect();
  318. +
  319. + channel = (ChannelShell) session.openChannel("shell");
  320. +
  321. + // 设置终端参数
  322. + channel.setPtyType("xterm-256color");
  323. + channel.setPtySize(120, 40, 960, 640);
  324. +
  325. + // 创建管道流
  326. + PipedInputStream toChannelInput = new PipedInputStream();
  327. + PipedOutputStream fromUserInput = new PipedOutputStream(toChannelInput);
  328. +
  329. + PipedInputStream fromChannelOutput = new PipedInputStream();
  330. + PipedOutputStream toUserOutput = new PipedOutputStream(fromChannelOutput);
  331. +
  332. + // 设置拦截器
  333. + channelInput = new InputStreamInterceptor(System.in);
  334. + channelOutput = new OutputStreamInterceptor(System.out);
  335. +
  336. + channel.setInputStream(toChannelInput);
  337. + channel.setOutputStream(toUserOutput);
  338. +
  339. + channel.connect();
  340. +
  341. + // 获取真实的输入输出流
  342. + InputStream realChannelOutput = channel.getInputStream();
  343. + OutputStream realChannelInput = channel.getOutputStream();
  344. +
  345. + recording = true;
  346. +
  347. + // 启动处理线程
  348. + startProcessingThreads(realChannelOutput, realChannelInput, fromUserInput, fromChannelOutput);
  349. +
  350. + System.out.println("SSH连接已建立,开始记录命令...");
  351. + }
  352. +
  353. + /**
  354. + * 启动处理线程
  355. + */
  356. + private void startProcessingThreads(InputStream channelOut, OutputStream channelIn,
  357. + OutputStream userIn, InputStream userOut) {
  358. +
  359. + // 处理用户输入 -> SSH服务器
  360. + executorService.submit(() -> {
  361. + try {
  362. + Scanner scanner = new Scanner(System.in);
  363. + while (recording && scanner.hasNextLine()) {
  364. + String line = scanner.nextLine();
  365. + processUserInput(line);
  366. + channelIn.write((line + "\r\n").getBytes(StandardCharsets.UTF_8));
  367. + channelIn.flush();
  368. + }
  369. + } catch (Exception e) {
  370. + if (recording) {
  371. + e.printStackTrace();
  372. + }
  373. + }
  374. + });
  375. +
  376. + // 处理SSH服务器输出 -> 用户
  377. + executorService.submit(() -> {
  378. + try {
  379. + byte[] buffer = new byte[1024];
  380. + int bytesRead;
  381. + while (recording && (bytesRead = channelOut.read(buffer)) != -1) {
  382. + String output = new String(buffer, 0, bytesRead, StandardCharsets.UTF_8);
  383. + processServerOutput(output);
  384. + System.out.print(output);
  385. + }
  386. + } catch (Exception e) {
  387. + if (recording) {
  388. + e.printStackTrace();
  389. + }
  390. + }
  391. + });
  392. +
  393. + // 输入分析线程
  394. + executorService.submit(this::processInputQueue);
  395. +
  396. + // 输出分析线程
  397. + executorService.submit(this::processOutputQueue);
  398. + }
  399. +
  400. + /**
  401. + * 处理用户输入
  402. + */
  403. + private void processUserInput(String input) {
  404. + synchronized (terminalState) {
  405. + // 检测特殊键序列
  406. + if (input.contains("\u001B[A")) { // 上键
  407. + terminalState.upKeyPressed = true;
  408. + } else if (input.contains("\u001B[B")) { // 下键
  409. + terminalState.downKeyPressed = true;
  410. + } else if (input.contains("\u0012")) { // Ctrl+R
  411. + terminalState.ctrlRPressed = true;
  412. + } else if (input.contains("\t")) { // Tab
  413. + terminalState.tabPressed = true;
  414. + }
  415. +
  416. + // 处理正常命令输入
  417. + if (!input.trim().isEmpty() && !containsOnlyControlChars(input)) {
  418. + terminalState.currentCommand = input.trim();
  419. + terminalState.inCommand = true;
  420. + terminalState.commandStartTime = System.currentTimeMillis();
  421. + terminalState.waitingForOutput = true;
  422. + }
  423. + }
  424. + }
  425. +
  426. + /**
  427. + * 处理服务器输出
  428. + */
  429. + private void processServerOutput(String output) {
  430. + synchronized (terminalState) {
  431. + String cleanOutput = cleanAnsiEscapes(output);
  432. + terminalState.currentLine.append(cleanOutput);
  433. +
  434. + // 检测命令提示符
  435. + if (isPromptLine(cleanOutput)) {
  436. + if (terminalState.waitingForOutput && !terminalState.currentCommand.isEmpty()) {
  437. + // 记录命令
  438. + recordCommand(terminalState.currentCommand, determineCommandType());
  439. + terminalState.reset();
  440. + }
  441. +
  442. + // 提取新的提示符
  443. + String[] lines = cleanOutput.split("\\r?\\n");
  444. + for (String line : lines) {
  445. + if (isPromptLine(line.trim())) {
  446. + terminalState.lastPrompt = line.trim();
  447. + break;
  448. + }
  449. + }
  450. + }
  451. + }
  452. + }
  453. +
  454. + /**
  455. + * 处理输入队列
  456. + */
  457. + private void processInputQueue() {
  458. + while (recording) {
  459. + try {
  460. + String input = inputQueue.poll(100, TimeUnit.MILLISECONDS);
  461. + if (input != null) {
  462. + analyzeInput(input);
  463. + }
  464. + } catch (InterruptedException e) {
  465. + Thread.currentThread().interrupt();
  466. + break;
  467. + }
  468. + }
  469. + }
  470. +
  471. + /**
  472. + * 处理输出队列
  473. + */
  474. + private void processOutputQueue() {
  475. + while (recording) {
  476. + try {
  477. + String output = outputQueue.poll(100, TimeUnit.MILLISECONDS);
  478. + if (output != null) {
  479. + analyzeOutput(output);
  480. + }
  481. + } catch (InterruptedException e) {
  482. + Thread.currentThread().interrupt();
  483. + break;
  484. + }
  485. + }
  486. + }
  487. +
  488. + /**
  489. + * 分析输入数据
  490. + */
  491. + private void analyzeInput(String input) {
  492. + // 检测控制序列
  493. + if (input.contains("\u001B[A")) {
  494. + terminalState.upKeyPressed = true;
  495. + } else if (input.contains("\u001B[B")) {
  496. + terminalState.downKeyPressed = true;
  497. + } else if (input.contains("\u0012")) {
  498. + terminalState.ctrlRPressed = true;
  499. + }
  500. +
  501. + // 检测回车键(命令执行)
  502. + if (input.contains("\r") || input.contains("\n")) {
  503. + synchronized (terminalState) {
  504. + if (terminalState.inCommand) {
  505. + terminalState.waitingForOutput = true;
  506. + }
  507. + }
  508. + }
  509. + }
  510. +
  511. + /**
  512. + * 分析输出数据
  513. + */
  514. + private void analyzeOutput(String output) {
  515. + String cleanOutput = cleanAnsiEscapes(output);
  516. +
  517. + // 检测命令回显和提示符
  518. + String[] lines = cleanOutput.split("\\r?\\n");
  519. + for (String line : lines) {
  520. + String trimmedLine = line.trim();
  521. +
  522. + if (isPromptLine(trimmedLine)) {
  523. + synchronized (terminalState) {
  524. + if (terminalState.waitingForOutput && !terminalState.currentCommand.isEmpty()) {
  525. + recordCommand(terminalState.currentCommand, determineCommandType());
  526. + terminalState.reset();
  527. + }
  528. + terminalState.lastPrompt = trimmedLine;
  529. + }
  530. + } else if (!trimmedLine.isEmpty() && !terminalState.inCommand) {
  531. + // 可能是命令回显
  532. + synchronized (terminalState) {
  533. + if (isPotentialCommand(trimmedLine)) {
  534. + terminalState.currentCommand = trimmedLine;
  535. + terminalState.inCommand = true;
  536. + terminalState.commandStartTime = System.currentTimeMillis();
  537. + }
  538. + }
  539. + }
  540. + }
  541. + }
  542. +
  543. + /**
  544. + * 判断命令类型
  545. + */
  546. + private CommandRecord.CommandType determineCommandType() {
  547. + if (terminalState.upKeyPressed || terminalState.downKeyPressed) {
  548. + return terminalState.upKeyPressed ?
  549. + CommandRecord.CommandType.HISTORY_UP : CommandRecord.CommandType.HISTORY_DOWN;
  550. + } else if (terminalState.ctrlRPressed) {
  551. + return CommandRecord.CommandType.SEARCH;
  552. + } else if (terminalState.tabPressed) {
  553. + return CommandRecord.CommandType.TAB_COMPLETE;
  554. + } else {
  555. + return CommandRecord.CommandType.TYPED;
  556. + }
  557. + }
  558. +
  559. + /**
  560. + * 记录命令
  561. + */
  562. + private void recordCommand(String command, CommandRecord.CommandType type) {
  563. + if (command == null || command.trim().isEmpty() || !isValidCommand(command)) {
  564. + return;
  565. + }
  566. +
  567. + try {
  568. + String username = session.getUserName();
  569. + String hostname = session.getHost();
  570. + String sessionId = Integer.toHexString(session.hashCode());
  571. +
  572. + CommandRecord record = new CommandRecord(command, username, hostname, sessionId);
  573. + record.setType(type);
  574. +
  575. + if (terminalState.commandStartTime > 0) {
  576. + record.setDuration(System.currentTimeMillis() - terminalState.commandStartTime);
  577. + }
  578. +
  579. + commandHistory.add(record);
  580. +
  581. + // 记录到控制台和日志
  582. + System.out.println("\n[AUDIT] " + record);
  583. +
  584. + // 异步写入文件或数据库
  585. + executorService.submit(() -> writeToAuditLog(record));
  586. +
  587. + } catch (Exception e) {
  588. + System.err.println("Error recording command: " + e.getMessage());
  589. + }
  590. + }
  591. +
  592. + /**
  593. + * 写入审计日志
  594. + */
  595. + private void writeToAuditLog(CommandRecord record) {
  596. + try {
  597. + String logFile = "ssh_audit_" + new java.text.SimpleDateFormat("yyyy-MM-dd").format(new Date()) + ".log";
  598. + try (PrintWriter writer = new PrintWriter(new FileWriter(logFile, true))) {
  599. + writer.printf("%d|%s|%s|%s|%s|%s|%d%n",
  600. + record.getTimestamp(),
  601. + record.getUser(),
  602. + record.getHost(),
  603. + record.getSessionId(),
  604. + record.getType(),
  605. + record.getCommand().replace("|", "\\|"),
  606. + record.getDuration());
  607. + }
  608. + } catch (IOException e) {
  609. + System.err.println("Error writing to audit log: " + e.getMessage());
  610. + }
  611. + }
  612. +
  613. + /**
  614. + * 清理ANSI转义序列
  615. + */
  616. + private String cleanAnsiEscapes(String input) {
  617. + if (input == null) return "";
  618. + return ANSI_ESCAPE.matcher(input).replaceAll("");
  619. + }
  620. +
  621. + /**
  622. + * 检测是否为命令提示符行
  623. + */
  624. + private boolean isPromptLine(String line) {
  625. + return PROMPT_PATTERN.matcher(line).matches() ||
  626. + line.endsWith("$ ") || line.endsWith("# ") || line.endsWith("> ") ||
  627. + line.matches(".*?\\w+[@:].*?[\\$#>].*");
  628. + }
  629. +
  630. + /**
  631. + * 检测是否为潜在命令
  632. + */
  633. + private boolean isPotentialCommand(String line) {
  634. + return line.length() > 0 &&
  635. + line.length() < 500 &&
  636. + !line.matches("^\\s*$") &&
  637. + !line.startsWith("Welcome") &&
  638. + !line.startsWith("Last login") &&
  639. + !line.matches(".*?\\d{4}-\\d{2}-\\d{2}.*?");
  640. + }
  641. +
  642. + /**
  643. + * 验证是否为有效命令
  644. + */
  645. + private boolean isValidCommand(String command) {
  646. + if (command == null || command.trim().isEmpty()) {
  647. + return false;
  648. + }
  649. +
  650. + String trimmed = command.trim();
  651. + return trimmed.length() > 0 &&
  652. + trimmed.length() < 1000 &&
  653. + !trimmed.matches("^\\s*$") &&
  654. + !trimmed.matches("^\\d+$") &&
  655. + !containsOnlyControlChars(trimmed);
  656. + }
  657. +
  658. + /**
  659. + * 检查是否只包含控制字符
  660. + */
  661. + private boolean containsOnlyControlChars(String str) {
  662. + return str.chars().allMatch(ch -> ch < 32 || ch == 127);
  663. + }
  664. +
  665. + /**
  666. + * 发送命令(用于程序化调用)
  667. + */
  668. + public void sendCommand(String command) throws Exception {
  669. + if (channel != null && channel.isConnected()) {
  670. + OutputStream out = channel.getOutputStream();
  671. + out.write((command + "\r\n").getBytes(StandardCharsets.UTF_8));
  672. + out.flush();
  673. + }
  674. + }
  675. +
  676. + /**
  677. + * 获取命令历史记录
  678. + */
  679. + public List<CommandRecord> getCommandHistory() {
  680. + synchronized (commandHistory) {
  681. + return new ArrayList<>(commandHistory);
  682. + }
  683. + }
  684. +
  685. + /**
  686. + * 根据条件查询命令
  687. + */
  688. + public List<CommandRecord> queryCommands(String userFilter, long startTime, long endTime,
  689. + CommandRecord.CommandType typeFilter) {
  690. + synchronized (commandHistory) {
  691. + return commandHistory.stream()
  692. + .filter(cmd -> userFilter == null || userFilter.equals(cmd.getUser()))
  693. + .filter(cmd -> cmd.getTimestamp() >= startTime && cmd.getTimestamp() <= endTime)
  694. + .filter(cmd -> typeFilter == null || typeFilter.equals(cmd.getType()))
  695. + .collect(ArrayList::new, (list, item) -> list.add(item), (list1, list2) -> list1.addAll(list2));
  696. + }
  697. + }
  698. +
  699. + /**
  700. + * 导出审计报告
  701. + */
  702. + public void exportAuditReport(String filename) throws IOException {
  703. + try (PrintWriter writer = new PrintWriter(new FileWriter(filename))) {
  704. + writer.println("SSH Command Audit Report");
  705. + writer.println("Generated: " + new Date());
  706. + // writer.println("=".repeat(80));
  707. + writer.println();
  708. +
  709. + synchronized (commandHistory) {
  710. + Map<String, Long> userStats = new HashMap<>();
  711. + Map<CommandRecord.CommandType, Long> typeStats = new HashMap<>();
  712. +
  713. + for (CommandRecord record : commandHistory) {
  714. + userStats.merge(record.getUser(), 1L, Long::sum);
  715. + typeStats.merge(record.getType(), 1L, Long::sum);
  716. + }
  717. +
  718. + writer.println("Statistics:");
  719. + writer.println("Total Commands: " + commandHistory.size());
  720. + writer.println("Users: " + userStats);
  721. + writer.println("Command Types: " + typeStats);
  722. + writer.println();
  723. +
  724. + writer.println("Command Details:");
  725. + // writer.println("-".repeat(80));
  726. + for (CommandRecord record : commandHistory) {
  727. + writer.printf("[%s] %s@%s [%s] [%dms] $ %s%n",
  728. + new Date(record.getTimestamp()),
  729. + record.getUser(),
  730. + record.getHost(),
  731. + record.getType(),
  732. + record.getDuration(),
  733. + record.getCommand());
  734. + }
  735. + }
  736. + }
  737. + }
  738. +
  739. + /**
  740. + * 断开连接
  741. + */
  742. + public void disconnect() {
  743. + recording = false;
  744. +
  745. + executorService.shutdown();
  746. + try {
  747. + if (!executorService.awaitTermination(5, TimeUnit.SECONDS)) {
  748. + executorService.shutdownNow();
  749. + }
  750. + } catch (InterruptedException e) {
  751. + executorService.shutdownNow();
  752. + Thread.currentThread().interrupt();
  753. + }
  754. +
  755. + if (channel != null && channel.isConnected()) {
  756. + channel.disconnect();
  757. + }
  758. +
  759. + if (session != null && session.isConnected()) {
  760. + session.disconnect();
  761. + }
  762. +
  763. + System.out.println("SSH连接已断开,命令记录已停止。");
  764. + }
  765. +
  766. + /**
  767. + * 使用示例和测试
  768. + */
  769. + public static void main(String[] args) {
  770. + SSHCommandRecorder recorder = new SSHCommandRecorder();
  771. +
  772. + try {
  773. + // 连接到SSH服务器
  774. + System.out.println("正在连接SSH服务器...");
  775. + recorder.connect("192.168.30.29", 22, "user", "123456+.");
  776. +
  777. + // 保持连接,让用户交互
  778. + System.out.println("请在终端中执行命令,所有命令将被记录...");
  779. + System.out.println("输入 'exit' 或按 Ctrl+C 结束记录");
  780. +
  781. + // 等待用户操作
  782. + Scanner scanner = new Scanner(System.in);
  783. + while (scanner.hasNextLine()) {
  784. + String input = scanner.nextLine();
  785. + if ("exit".equals(input)) {
  786. + break;
  787. + }
  788. + }
  789. +
  790. + } catch (Exception e) {
  791. + e.printStackTrace();
  792. + } finally {
  793. + // 导出审计报告
  794. + try {
  795. + List<CommandRecord> history = recorder.getCommandHistory();
  796. + System.out.println("\n=== 命令执行历史 ===");
  797. + history.forEach(System.out::println);
  798. +
  799. + recorder.exportAuditReport("ssh_audit_report.txt");
  800. + System.out.println("审计报告已导出到: ssh_audit_report.txt");
  801. +
  802. + } catch (Exception e) {
  803. + e.printStackTrace();
  804. + }
  805. +
  806. + recorder.disconnect();
  807. + }
  808. + }
  809. +}
  810. diff --git a/modules/sub-plugin/ssh-jsch/src/test/java/TerminalParserRecorder.java b/modules/sub-plugin/ssh-jsch/src/test/java/TerminalParserRecorder.java
  811. new file mode 100644
  812. index 000000000..02d14044d
  813. --- /dev/null
  814. +++ b/modules/sub-plugin/ssh-jsch/src/test/java/TerminalParserRecorder.java
  815. @@ -0,0 +1,444 @@
  816. +/**
  817. + * @author bwcx_jzy
  818. + * @since 2025/6/11
  819. + */
  820. +import com.jcraft.jsch.*;
  821. +import java.io.*;
  822. +import java.util.*;
  823. +import java.util.regex.Pattern;
  824. +import java.util.concurrent.BlockingQueue;
  825. +import java.util.concurrent.LinkedBlockingQueue;
  826. +
  827. +/**
  828. + * 基于终端解析的轻量级命令记录器
  829. + * 通过解析终端输出流来识别和记录执行的命令
  830. + */
  831. +public class TerminalParserRecorder {
  832. +
  833. + private static final Pattern PROMPT_PATTERNS = Pattern.compile(
  834. + ".*?[\\$#>]\\s*$|.*?\\w+@\\w+[:\\s]+.*?[\\$#>]\\s*$"
  835. + );
  836. +
  837. + private static final Pattern COMMAND_COMPLETION_PATTERN = Pattern.compile(
  838. + ".*?\\[\\d+\\]\\s*.*?|.*?\\+\\s*.*?|.*?>\\s*.*?"
  839. + );
  840. +
  841. + private Session session;
  842. + private ChannelShell channel;
  843. + private BlockingQueue<String> outputQueue;
  844. + private List<ExecutedCommand> commandLog;
  845. + private TerminalState currentState;
  846. + private Thread parsingThread;
  847. + private volatile boolean isRecording = false;
  848. +
  849. + public TerminalParserRecorder() {
  850. + this.outputQueue = new LinkedBlockingQueue<>();
  851. + this.commandLog = Collections.synchronizedList(new ArrayList<>());
  852. + this.currentState = new TerminalState();
  853. + }
  854. +
  855. + /**
  856. + * 终端状态跟踪
  857. + */
  858. + private static class TerminalState {
  859. + private StringBuilder currentLine = new StringBuilder();
  860. + private String lastPrompt = "";
  861. + private String pendingCommand = "";
  862. + private boolean waitingForPrompt = false;
  863. + private int cursorPosition = 0;
  864. +
  865. + public void reset() {
  866. + currentLine.setLength(0);
  867. + pendingCommand = "";
  868. + waitingForPrompt = false;
  869. + cursorPosition = 0;
  870. + }
  871. + }
  872. +
  873. + /**
  874. + * 执行命令记录
  875. + */
  876. + public static class ExecutedCommand {
  877. + private final String command;
  878. + private final String user;
  879. + private final String host;
  880. + private final long timestamp;
  881. + private final String sessionId;
  882. + private String output;
  883. + private int exitCode = -1;
  884. +
  885. + public ExecutedCommand(String command, String user, String host, String sessionId) {
  886. + this.command = command;
  887. + this.user = user;
  888. + this.host = host;
  889. + this.sessionId = sessionId;
  890. + this.timestamp = System.currentTimeMillis();
  891. + }
  892. +
  893. + // Getters
  894. + public String getCommand() { return command; }
  895. + public String getUser() { return user; }
  896. + public String getHost() { return host; }
  897. + public long getTimestamp() { return timestamp; }
  898. + public String getSessionId() { return sessionId; }
  899. + public String getOutput() { return output; }
  900. + public int getExitCode() { return exitCode; }
  901. +
  902. + public void setOutput(String output) { this.output = output; }
  903. + public void setExitCode(int exitCode) { this.exitCode = exitCode; }
  904. +
  905. + @Override
  906. + public String toString() {
  907. + return String.format("[%s] %s@%s $ %s",
  908. + new Date(timestamp).toString(), user, host, command);
  909. + }
  910. + }
  911. +
  912. + /**
  913. + * 连接SSH并开始记录
  914. + */
  915. + public void connect(String host, int port, String username, String password) throws Exception {
  916. + JSch jsch = new JSch();
  917. + session = jsch.getSession(username, host, port);
  918. + session.setPassword(password);
  919. + session.setConfig("StrictHostKeyChecking", "no");
  920. + session.connect();
  921. +
  922. + channel = (ChannelShell) session.openChannel("shell");
  923. +
  924. + // 设置终端类型以获得更好的解析效果
  925. + channel.setPtyType("vt100");
  926. + channel.setPtySize(80, 24, 640, 480);
  927. +
  928. + // 创建输出拦截流
  929. + ByteArrayOutputStream baos = new ByteArrayOutputStream();
  930. + TeeOutputStream teeOut = new TeeOutputStream(System.out, baos);
  931. + channel.setOutputStream(teeOut);
  932. +
  933. + channel.connect();
  934. +
  935. + // 启动解析线程
  936. + startParsing(channel.getInputStream());
  937. + isRecording = true;
  938. + }
  939. +
  940. + /**
  941. + * 开始解析终端输出
  942. + */
  943. + private void startParsing(InputStream inputStream) {
  944. + parsingThread = new Thread(() -> {
  945. + try (BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream))) {
  946. + StringBuilder outputBuffer = new StringBuilder();
  947. + int ch;
  948. +
  949. + while (isRecording && (ch = reader.read()) != -1) {
  950. + char character = (char) ch;
  951. +
  952. + // 处理特殊字符
  953. + if (character == '\r') {
  954. + continue; // 忽略回车符
  955. + } else if (character == '\n') {
  956. + // 处理换行
  957. + String line = outputBuffer.toString();
  958. + processLine(line);
  959. + outputBuffer.setLength(0);
  960. + } else if (character == '\b') {
  961. + // 处理退格
  962. + if (outputBuffer.length() > 0) {
  963. + outputBuffer.deleteCharAt(outputBuffer.length() - 1);
  964. + }
  965. + } else if (character == '\u001B') {
  966. + // 处理ANSI转义序列
  967. + String escapeSequence = readEscapeSequence(reader);
  968. + processEscapeSequence(escapeSequence);
  969. + } else {
  970. + outputBuffer.append(character);
  971. + }
  972. + }
  973. + } catch (IOException e) {
  974. + if (isRecording) {
  975. + e.printStackTrace();
  976. + }
  977. + }
  978. + });
  979. +
  980. + parsingThread.setDaemon(true);
  981. + parsingThread.start();
  982. + }
  983. +
  984. + /**
  985. + * 处理每一行输出
  986. + */
  987. + private void processLine(String line) {
  988. + String cleanLine = cleanAnsiEscapes(line);
  989. +
  990. + // 检测命令提示符
  991. + if (isPromptLine(cleanLine)) {
  992. + if (currentState.waitingForPrompt && !currentState.pendingCommand.isEmpty()) {
  993. + // 记录已执行的命令
  994. + recordCommand(currentState.pendingCommand.trim());
  995. + currentState.reset();
  996. + }
  997. + currentState.lastPrompt = cleanLine;
  998. + } else if (!cleanLine.trim().isEmpty()) {
  999. + // 可能是命令行
  1000. + String potentialCommand = extractPotentialCommand(cleanLine);
  1001. + if (potentialCommand != null) {
  1002. + currentState.pendingCommand = potentialCommand;
  1003. + currentState.waitingForPrompt = true;
  1004. + }
  1005. + }
  1006. + }
  1007. +
  1008. + /**
  1009. + * 读取ANSI转义序列
  1010. + */
  1011. + private String readEscapeSequence(BufferedReader reader) throws IOException {
  1012. + StringBuilder seq = new StringBuilder("\u001B");
  1013. + int ch;
  1014. +
  1015. + // 读取完整的转义序列
  1016. + while ((ch = reader.read()) != -1) {
  1017. + char c = (char) ch;
  1018. + seq.append(c);
  1019. +
  1020. + // 大多数转义序列以字母结束
  1021. + if (Character.isLetter(c)) {
  1022. + break;
  1023. + }
  1024. +
  1025. + // 防止无限循环
  1026. + if (seq.length() > 20) {
  1027. + break;
  1028. + }
  1029. + }
  1030. +
  1031. + return seq.toString();
  1032. + }
  1033. +
  1034. + /**
  1035. + * 处理ANSI转义序列
  1036. + */
  1037. + private void processEscapeSequence(String sequence) {
  1038. + // 这里可以处理光标移动、清屏等操作
  1039. + // 对于命令记录,主要关注的是内容而不是格式
  1040. + if (sequence.contains("K")) {
  1041. + // 清除行的部分内容
  1042. + currentState.currentLine.setLength(0);
  1043. + }
  1044. + }
  1045. +
  1046. + /**
  1047. + * 清理ANSI转义字符
  1048. + */
  1049. + private String cleanAnsiEscapes(String input) {
  1050. + return input.replaceAll("\\x1B\\[[0-9;]*[a-zA-Z]", "");
  1051. + }
  1052. +
  1053. + /**
  1054. + * 检测是否为命令提示符行
  1055. + */
  1056. + private boolean isPromptLine(String line) {
  1057. + return PROMPT_PATTERNS.matcher(line).matches() ||
  1058. + line.endsWith("$ ") ||
  1059. + line.endsWith("# ") ||
  1060. + line.endsWith("> ") ||
  1061. + line.matches(".*?\\w+@\\w+.*?[\\$#>].*");
  1062. + }
  1063. +
  1064. + /**
  1065. + * 从行中提取潜在的命令
  1066. + */
  1067. + private String extractPotentialCommand(String line) {
  1068. + // 移除命令提示符部分
  1069. + String[] parts = line.split("[\\$#>]", 2);
  1070. + if (parts.length > 1) {
  1071. + String command = parts[1].trim();
  1072. + if (!command.isEmpty() && isValidCommand(command)) {
  1073. + return command;
  1074. + }
  1075. + }
  1076. +
  1077. + // 如果没有找到提示符,检查是否是直接的命令
  1078. + String trimmed = line.trim();
  1079. + if (isValidCommand(trimmed)) {
  1080. + return trimmed;
  1081. + }
  1082. +
  1083. + return null;
  1084. + }
  1085. +
  1086. + /**
  1087. + * 验证是否为有效命令
  1088. + */
  1089. + private boolean isValidCommand(String command) {
  1090. + if (command == null || command.isEmpty()) {
  1091. + return false;
  1092. + }
  1093. +
  1094. + // 过滤掉明显不是命令的内容
  1095. + return !command.matches(".*?\\d+\\s*$") && // 不是纯数字
  1096. + !command.matches("^[\\s\\-=]+$") && // 不是分隔线
  1097. + !command.startsWith("Welcome") && // 不是欢迎信息
  1098. + !command.startsWith("Last login") && // 不是登录信息
  1099. + command.length() < 1000; // 不是过长的输出
  1100. + }
  1101. +
  1102. + /**
  1103. + * 记录命令
  1104. + */
  1105. + private void recordCommand(String command) {
  1106. + try {
  1107. + String username = session.getUserName();
  1108. + String hostname = session.getHost();
  1109. + String sessionId = Integer.toHexString(session.hashCode());
  1110. +
  1111. + ExecutedCommand record = new ExecutedCommand(command, username, hostname, sessionId);
  1112. + commandLog.add(record);
  1113. +
  1114. + // 记录日志
  1115. + System.out.println("Recorded command: " + record);
  1116. +
  1117. + // 可以在这里添加其他处理逻辑,如写入数据库、发送审计日志等
  1118. +
  1119. + } catch (Exception e) {
  1120. + System.err.println("Error recording command: " + e.getMessage());
  1121. + }
  1122. + }
  1123. +
  1124. + /**
  1125. + * 发送命令
  1126. + */
  1127. + public void sendCommand(String command) throws Exception {
  1128. + if (channel != null && channel.isConnected()) {
  1129. + OutputStream out = channel.getOutputStream();
  1130. + out.write((command + "\n").getBytes());
  1131. + out.flush();
  1132. + }
  1133. + }
  1134. +
  1135. + /**
  1136. + * 获取命令日志
  1137. + */
  1138. + public List<ExecutedCommand> getCommandLog() {
  1139. + return new ArrayList<>(commandLog);
  1140. + }
  1141. +
  1142. + /**
  1143. + * 根据条件查询命令
  1144. + */
  1145. + public List<ExecutedCommand> queryCommands(String userFilter, long startTime, long endTime) {
  1146. + synchronized (commandLog) {
  1147. + return commandLog.stream()
  1148. + .filter(cmd -> userFilter == null || userFilter.equals(cmd.getUser()))
  1149. + .filter(cmd -> cmd.getTimestamp() >= startTime && cmd.getTimestamp() <= endTime)
  1150. + .collect(ArrayList::new, (list, item) -> list.add(item), (list1, list2) -> list1.addAll(list2));
  1151. + }
  1152. + }
  1153. +
  1154. + /**
  1155. + * 导出命令日志
  1156. + */
  1157. + public void exportToFile(String filename) throws IOException {
  1158. + try (PrintWriter writer = new PrintWriter(new FileWriter(filename))) {
  1159. + writer.println("Timestamp,User,Host,SessionId,Command");
  1160. + synchronized (commandLog) {
  1161. + for (ExecutedCommand cmd : commandLog) {
  1162. + writer.printf("%d,%s,%s,%s,\"%s\"%n",
  1163. + cmd.getTimestamp(),
  1164. + cmd.getUser(),
  1165. + cmd.getHost(),
  1166. + cmd.getSessionId(),
  1167. + cmd.getCommand().replace("\"", "\"\""));
  1168. + }
  1169. + }
  1170. + }
  1171. + }
  1172. +
  1173. + /**
  1174. + * 断开连接
  1175. + */
  1176. + public void disconnect() {
  1177. + isRecording = false;
  1178. +
  1179. + if (parsingThread != null) {
  1180. + parsingThread.interrupt();
  1181. + }
  1182. +
  1183. + if (channel != null && channel.isConnected()) {
  1184. + channel.disconnect();
  1185. + }
  1186. +
  1187. + if (session != null && session.isConnected()) {
  1188. + session.disconnect();
  1189. + }
  1190. + }
  1191. +
  1192. + /**
  1193. + * Tee输出流,同时写入两个流
  1194. + */
  1195. + private static class TeeOutputStream extends OutputStream {
  1196. + private OutputStream out1;
  1197. + private OutputStream out2;
  1198. +
  1199. + public TeeOutputStream(OutputStream out1, OutputStream out2) {
  1200. + this.out1 = out1;
  1201. + this.out2 = out2;
  1202. + }
  1203. +
  1204. + @Override
  1205. + public void write(int b) throws IOException {
  1206. + out1.write(b);
  1207. + out2.write(b);
  1208. + }
  1209. +
  1210. + @Override
  1211. + public void flush() throws IOException {
  1212. + out1.flush();
  1213. + out2.flush();
  1214. + }
  1215. +
  1216. + @Override
  1217. + public void close() throws IOException {
  1218. + out1.close();
  1219. + out2.close();
  1220. + }
  1221. + }
  1222. +
  1223. + /**
  1224. + * 使用示例
  1225. + */
  1226. + public static void main(String[] args) {
  1227. + TerminalParserRecorder recorder = new TerminalParserRecorder();
  1228. +
  1229. + try {
  1230. + recorder.connect("192.168.30.29", 22, "user", "123456+.");
  1231. +
  1232. + // 等待连接稳定
  1233. + Thread.sleep(2000);
  1234. +
  1235. + // 发送测试命令
  1236. + recorder.sendCommand("ls -la");
  1237. + Thread.sleep(3000);
  1238. +
  1239. + recorder.sendCommand("pwd");
  1240. + Thread.sleep(2000);
  1241. +
  1242. + recorder.sendCommand("history | tail -5");
  1243. + Thread.sleep(3000);
  1244. +
  1245. + // 查看记录的命令
  1246. + List<ExecutedCommand> commands = recorder.getCommandLog();
  1247. + System.out.println("\n=== Command Log ===");
  1248. + commands.forEach(System.out::println);
  1249. +
  1250. + // 导出到文件
  1251. + recorder.exportToFile("command_audit.csv");
  1252. +
  1253. + } catch (Exception e) {
  1254. + e.printStackTrace();
  1255. + } finally {
  1256. + recorder.disconnect();
  1257. + }
  1258. + }
  1259. +}