terminal-bridge-setup 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,194 @@
1
+ // Arthas 安全基线
2
+ //
3
+ // 为什么需要:Arthas 直接挂在线上 JVM 上,trace/watch/monitor 等命令通过
4
+ // 字节码增强持续拦截方法调用,高频方法上不加限制会显著拖慢线上服务;
5
+ // retransform 改字节码可能不可逆;profiler 长期采样吃 CPU/内存。
6
+ // 这个模块在代理层拦截 Arthas 命令,按风险分级管控。
7
+ //
8
+ // 设计原则:
9
+ // - 默认严格(保护线上),可通过环境变量放宽
10
+ // - 只拦截 Arthas 命令(不认识的不拦,shell 命令也不拦)
11
+ // - 拦截结果分两类:
12
+ // "transform" —— 自动改写命令(加安全参数)后放行,对 Agent 透明
13
+ // "deny" —— 拒绝,返回 error 让 Agent 走人工确认流程
14
+ // - 对中风险命令做会话级计数,超阈值后拒绝
15
+
16
+ // ===================== 风险分级 =====================
17
+ // 依据 Arthas 官方文档的性能说明和字节码增强机制分类。
18
+
19
+ // 🟢 只读 / 安全:无字节码增强,开销可忽略
20
+ const SAFE_COMMANDS = new Set([
21
+ "help", "version", "pwd", "session", "history",
22
+ "sysenv", "sysprop", "jvm", "memory", "perfcounter",
23
+ "vmoption", "mbean", "logger",
24
+ "dashboard", // 虽然持续刷新但只读,靠超时控制即可
25
+ "options",
26
+ ]);
27
+
28
+ // 🟠 中风险:字节码增强 + 持续监听,必须限次/限时
29
+ // - 不加 -n 会无限监听,高频方法上持续产生开销
30
+ // - 官方建议配合 #cost 过滤慢调用
31
+ const MEDIUM_RISK_COMMANDS = new Set(["watch", "trace", "stack", "monitor"]);
32
+
33
+ // 🔴 高风险:修改字节码 / 长期采样 / 不可逆 / 关闭服务
34
+ // 必须人工确认才能执行
35
+ const HIGH_RISK_COMMANDS = new Set([
36
+ "retransform", // 改字节码,可能不可逆(需 retransform -d 才能回滚)
37
+ "profiler", // async-profiler 长期采样,吃 CPU/内存
38
+ "stop", // 关闭 Arthas server,会话断开
39
+ "reset", // 重置所有增强类(影响面大)
40
+ ]);
41
+
42
+ // ===================== 配置(环境变量可调)=====================
43
+ const MAX_MEDIUM_RISK_PER_SESSION = Number(process.env.ARTHAS_MAX_MEDIUM || 20);
44
+ // 是否对中风险命令自动补 -n(false 时改为 deny,让 Agent 显式确认)
45
+ // 默认 true:自动补 -n 1 对 Agent 透明,体验最好
46
+ const AUTO_PATCH_MEDIUM = process.env.ARTHAS_AUTO_PATCH !== "0";
47
+
48
+ // ===================== 会话级计数 =====================
49
+ // 跟踪本代理进程内执行过的中风险命令次数。超过阈值后拒绝,防止 Agent 失控循环调用。
50
+ let mediumRiskCount = 0;
51
+
52
+ // ===================== 命令解析 =====================
53
+ // 从 Arthas 命令字符串提取子命令名(第一个 token)。
54
+ // "trace com.foo.Bar run" → "trace"
55
+ // "trace -n 2 com.foo.Bar run '#cost>100'" → "trace"
56
+ // 注意:Arthas 命令行用空格分 token,引号内空格不算分隔。
57
+ function parseArthasCommand(cmd) {
58
+ const trimmed = cmd.trim();
59
+ if (!trimmed) return null;
60
+ // 简单 tokenizer:按空格切,但尊重引号
61
+ const tokens = trimmed.match(/\S+|"[^"]*"|'[^']*'/g);
62
+ if (!tokens || tokens.length === 0) return null;
63
+ return tokens[0].toLowerCase();
64
+ }
65
+
66
+ // 判断 cmd 是不是 Arthas 命令(而非 shell 命令)。
67
+ // 启发式:第一个 token 在 Arthas 已知命令集里,就认为是 Arthas 命令。
68
+ // 这样能跟 JumpServer 的 shell 命令区分(ls/ps/cat 不在 Arthas 命令集)。
69
+ const ALL_ARTHAS_COMMANDS = new Set([
70
+ ...SAFE_COMMANDS, ...MEDIUM_RISK_COMMANDS, ...HIGH_RISK_COMMANDS,
71
+ // 这些是一次性查询类,归为低风险(放行但不补参数)
72
+ "thread", "sc", "sm", "jad", "getstatic", "classloader", "vmtool",
73
+ "cat", "echo", "grep", "tee", "base64", "tt", "line", "cls",
74
+ ]);
75
+
76
+ function isArthasCommand(cmd) {
77
+ const name = parseArthasCommand(cmd);
78
+ return name !== null && ALL_ARTHAS_COMMANDS.has(name);
79
+ }
80
+
81
+ // ===================== 参数检查 =====================
82
+ // 中风险命令是否已带 -n 或 #cost(已自带限制就不再补)
83
+ function hasFrequencyLimit(cmd) {
84
+ // -n 数字(如 -n 1、-n2)或 --limits 数字
85
+ if (/\s-n\s*\d+/i.test(cmd) || /\s--limits?\s*\d+/i.test(cmd)) return true;
86
+ // #cost 条件表达式
87
+ if (/#cost\s*[><=]/i.test(cmd)) return true;
88
+ return false;
89
+ }
90
+
91
+ // profiler 已被禁用(高风险),不再需要解析持续时间
92
+
93
+ // ===================== 主审计函数 =====================
94
+ // 返回值:
95
+ // { action: "allow" } —— 放行,cmd 不变
96
+ // { action: "transform", cmd: 新cmd, reason } —— 放行但改写了命令
97
+ // { action: "deny", error, suggest, message } —— 拒绝
98
+ function auditArthasCommand(cmd) {
99
+ const name = parseArthasCommand(cmd);
100
+ if (!name) return { action: "allow" };
101
+
102
+ // 只审计认识的 Arthas 命令;不认识的(可能是 shell)放行
103
+ if (!ALL_ARTHAS_COMMANDS.has(name)) return { action: "allow" };
104
+
105
+ // --- 🟢 安全命令 ---
106
+ if (SAFE_COMMANDS.has(name)) {
107
+ return { action: "allow" };
108
+ }
109
+
110
+ // --- 🔴 高风险命令:无条件禁用 ---
111
+ // 这类命令(retransform/profiler/stop/reset)改字节码/长期采样/不可逆,
112
+ // 通过桥接执行风险太高。一律拒绝,让用户去浏览器终端手动操作。
113
+ if (HIGH_RISK_COMMANDS.has(name)) {
114
+ const riskInfo = HIGH_RISK_INFO[name];
115
+ return {
116
+ action: "deny",
117
+ error: "arthas-forbidden",
118
+ suggest: riskInfo.suggest,
119
+ message: riskInfo.message,
120
+ };
121
+ }
122
+
123
+ // --- 🟠 中风险命令 ---
124
+ if (MEDIUM_RISK_COMMANDS.has(name)) {
125
+ // 会话计数超阈值
126
+ if (mediumRiskCount >= MAX_MEDIUM_RISK_PER_SESSION) {
127
+ return {
128
+ action: "deny",
129
+ error: "arthas-quota-exceeded",
130
+ message: `中风险命令(${name})本会话已执行 ${mediumRiskCount} 次,达到上限 ${MAX_MEDIUM_RISK_PER_SESSION}。为保护线上服务,已拒绝。如需继续,请重启代理重置计数。`,
131
+ };
132
+ }
133
+
134
+ // 已自带 -n 或 #cost,直接放行
135
+ if (hasFrequencyLimit(cmd)) {
136
+ mediumRiskCount++;
137
+ return { action: "allow" };
138
+ }
139
+
140
+ // 没带频率限制
141
+ if (AUTO_PATCH_MEDIUM) {
142
+ // 自动补 -n 1:只采样一次就停,开销可控,对 Agent 透明
143
+ mediumRiskCount++;
144
+ const patched = `${cmd} -n 1`;
145
+ return {
146
+ action: "transform",
147
+ cmd: patched,
148
+ reason: `中风险命令 ${name} 未带 -n/#cost,已自动补 -n 1(只采样一次)`,
149
+ };
150
+ } else {
151
+ // 严格模式:拒绝,让 Agent 显式确认
152
+ return {
153
+ action: "deny",
154
+ error: "arthas-needs-limit",
155
+ suggest: `${cmd} -n 1`,
156
+ message: `中风险命令 ${name} 未带频率限制(-n 或 #cost)。高频方法上持续增强会拖慢线上服务。建议加 -n 1(只采样一次)或 '#cost>100'(只看慢调用)后重试。`,
157
+ };
158
+ }
159
+ }
160
+
161
+ // --- 其他低风险 Arthas 命令(thread/jad/sc 等一次性查询)---
162
+ return { action: "allow" };
163
+ }
164
+
165
+ // 高风险命令的禁用提示信息
166
+ // message 告诉用户为什么禁用 + 去哪里手动操作;suggest 给替代方案
167
+ const HIGH_RISK_INFO = {
168
+ retransform: {
169
+ suggest: "用 jad 查看字节码(只读);如确需 retransform,请在浏览器 Arthas 终端手动执行",
170
+ message: "retransform 会直接修改线上 JVM 字节码,且可能不可逆(需 retransform -d 才能回滚),stop arthas 后改动仍可能生效。此命令已通过桥接禁用,请到浏览器 Arthas 终端手动执行。",
171
+ },
172
+ profiler: {
173
+ suggest: "用 thread/memory/jvm 查看概况;如需火焰图,请在浏览器 Arthas 终端手动执行 profiler start -d 60",
174
+ message: "profiler(async-profiler)会持续采样,消耗 CPU 和内存,通过桥接执行难以控制采样窗口。此命令已禁用,请到浏览器 Arthas 终端手动执行(务必带 -d 限时)。",
175
+ },
176
+ stop: {
177
+ suggest: "如需断开,关闭浏览器 Arthas 页面即可",
178
+ message: "stop 会关闭整个 Arthas server,导致所有诊断会话断开、所有增强被重置,影响其他使用该 arthas 会话的人。此命令已禁用。如需断开,关闭浏览器页面即可。",
179
+ },
180
+ reset: {
181
+ suggest: "用 reset <具体类> 只重置需要的类(但 reset 本身也被禁用,请到浏览器手动执行)",
182
+ message: "reset 会重置所有被增强的类,影响面大。此命令已禁用,请到浏览器 Arthas 终端手动执行(建议只 reset 特定类)。",
183
+ },
184
+ };
185
+
186
+ // ===================== 导出 =====================
187
+ export {
188
+ auditArthasCommand,
189
+ isArthasCommand,
190
+ parseArthasCommand,
191
+ SAFE_COMMANDS,
192
+ MEDIUM_RISK_COMMANDS,
193
+ HIGH_RISK_COMMANDS,
194
+ };
@@ -0,0 +1,189 @@
1
+ // Agent 调用示例:通过本地代理向 JumpServer 终端发命令并拿回输出。
2
+ //
3
+ // 用法:
4
+ // node client-example.mjs # 默认跑 `uname -a`
5
+ // node client-example.mjs "ls -la /etc" # 自定义命令
6
+ // node client-example.mjs "find / -name foo" 30000 # 指定超时 30s
7
+ //
8
+ // 内含两个可复用函数:
9
+ // run(cmd, timeoutMs) —— 单次执行,返回 {ok, output, error}
10
+ // runWithSudoRetry(cmd, timeoutMs) —— 带 sudo 自动重试:检测到 sudo-required
11
+ // 时询问用户是否切 root,确认则切 root
12
+ // 后自动重发原命令。Agent 应优先用这个。
13
+
14
+ import WebSocket from "ws";
15
+ import { randomBytes } from "node:crypto";
16
+ import * as readline from "node:readline/promises";
17
+ import { stdin as input, stdout as output } from "node:process";
18
+
19
+ const BRIDGE = process.env.BRIDGE || "ws://127.0.0.1:8787/ssh";
20
+
21
+ /**
22
+ * 向 JumpServer 终端发一条命令,等执行完返回输出。
23
+ * @param {string} cmd - 要执行的 linux 命令
24
+ * @param {number} timeoutMs - 超时(默认 10s)
25
+ * @returns {Promise<{ok: boolean, output?: string, error?: string, suggest?: string, message?: string}>}
26
+ */
27
+ function run(cmd, timeoutMs = 10000) {
28
+ return new Promise((resolve) => {
29
+ const ws = new WebSocket(BRIDGE);
30
+ const reqId = randomBytes(4).toString("hex");
31
+ let settled = false;
32
+
33
+ const finish = (result) => {
34
+ if (settled) return;
35
+ settled = true;
36
+ try { ws.close(); } catch {}
37
+ resolve(result);
38
+ };
39
+
40
+ ws.on("open", () => {
41
+ ws.send(JSON.stringify({ type: "run", reqId, cmd, timeoutMs }));
42
+ });
43
+
44
+ ws.on("message", (raw) => {
45
+ let msg;
46
+ try { msg = JSON.parse(raw.toString()); } catch { return; }
47
+ if (msg.type === "result" && msg.reqId === reqId) {
48
+ finish({
49
+ ok: !!msg.ok,
50
+ output: msg.output,
51
+ error: msg.error,
52
+ suggest: msg.suggest,
53
+ message: msg.message,
54
+ elapsedMs: msg.elapsedMs
55
+ });
56
+ }
57
+ });
58
+
59
+ ws.on("error", (err) => finish({ ok: false, error: "ws error: " + err.message }));
60
+ setTimeout(() => finish({ ok: false, error: "client timeout" }), timeoutMs + 5000);
61
+ });
62
+ }
63
+
64
+ /**
65
+ * 带sudo 自动重试的执行(Agent 推荐用这个)。
66
+ *
67
+ * 流程:
68
+ * 1. 执行 cmd
69
+ * 2. 如果返回 error === "sudo-required"(检测到 sudo 密码提示):
70
+ * - 询问用户是否切 root
71
+ * - 用户同意 → 执行 "sudo su root" 切换 → 重新执行原 cmd
72
+ * - 用户拒绝 → 返回原错误
73
+ * 3. 其他情况正常返回
74
+ *
75
+ * @param {string} cmd - 要执行的 linux 命令
76
+ * @param {number} timeoutMs - 超时
77
+ * @param {function} ask - 可选的自定义询问函数,默认用 readline 控制台提问。
78
+ * Agent 接入时应传入自己的 AskUserQuestion 逻辑。
79
+ * 签名: async (message) => boolean
80
+ * @returns {Promise<{ok: boolean, output?: string, error?: string}>}
81
+ */
82
+ async function runWithSudoRetry(cmd, timeoutMs = 10000, ask = defaultAsk) {
83
+ let result = await run(cmd, timeoutMs);
84
+
85
+ if (result.error === "sudo-required") {
86
+ const approved = await ask(result.message || "检测到需要 sudo 权限,是否切换到 root 后重试?");
87
+ if (!approved) {
88
+ return { ok: false, error: "user declined sudo", output: "" };
89
+ }
90
+ // 切 root(sudo su root 通常免密,prompt 锚点能正常判定完成)
91
+ console.log("→ 切换到 root: sudo su root");
92
+ const switchResult = await run("sudo su root", timeoutMs);
93
+ if (!switchResult.ok) {
94
+ return { ok: false, error: "sudo su root 失败: " + (switchResult.error || ""), output: switchResult.output || "" };
95
+ }
96
+ // 切成功后重发原命令
97
+ console.log("→ 重新执行原命令");
98
+ result = await run(cmd, timeoutMs);
99
+ }
100
+
101
+ return result;
102
+ }
103
+
104
+ // 默认询问函数:控制台 readline 提问
105
+ async function defaultAsk(message) {
106
+ const rl = readline.createInterface({ input, output });
107
+ try {
108
+ const ans = await rl.question(`\n⚠ ${message} [y/N] `);
109
+ return ans.trim().toLowerCase().startsWith("y");
110
+ } finally {
111
+ rl.close();
112
+ }
113
+ }
114
+
115
+ /**
116
+ * 带 Arthas 安全基线处理的执行(Agent 操作 Arthas 终端时用这个)。
117
+ *
118
+ * 处理代理返回的 guard 错误:
119
+ * - arthas-forbidden —— 高风险命令被禁用,告知用户去浏览器手动操作,不重试
120
+ * - arthas-needs-limit —— 中风险命令未带频率限制(严格模式),按 suggest 补参数重发一次
121
+ * - arthas-quota-exceeded —— 中风险命令会话内超限,告知用户,不重试
122
+ *
123
+ * @param {string} cmd - Arthas 命令
124
+ * @param {number} timeoutMs - 超时
125
+ * @param {function} ask - 可选的自定义询问函数(Agent 应传 AskUserQuestion 逻辑)
126
+ * @returns {Promise<{ok: boolean, output?: string, error?: string}>}
127
+ */
128
+ async function runWithArthasGuard(cmd, timeoutMs = 10000, ask = defaultAsk) {
129
+ let currentCmd = cmd;
130
+
131
+ // 最多重试 2 次(补参数算一次)
132
+ for (let attempt = 0; attempt < 2; attempt++) {
133
+ const result = await run(currentCmd, timeoutMs);
134
+
135
+ if (result.ok) return result;
136
+
137
+ // --- 高风险命令被禁用:告知用户,不重试 ---
138
+ if (result.error === "arthas-forbidden") {
139
+ console.error("⚠ " + (result.message || "高风险 Arthas 命令已被禁用"));
140
+ if (result.suggest) console.error(" 替代方案: " + result.suggest);
141
+ return result;
142
+ }
143
+
144
+ // --- 中风险命令缺频率限制:按建议补参数重发一次 ---
145
+ if (result.error === "arthas-needs-limit") {
146
+ if (result.suggest) {
147
+ console.log("→ 按建议补频率限制: " + result.suggest);
148
+ currentCmd = result.suggest;
149
+ continue;
150
+ }
151
+ return result;
152
+ }
153
+
154
+ // --- 会话超限:不重试 ---
155
+ if (result.error === "arthas-quota-exceeded") {
156
+ console.error("⚠ " + (result.message || "中风险命令超限"));
157
+ return result;
158
+ }
159
+
160
+ // --- sudo-required:复用 sudo 重试逻辑(Arthas 不会有,但保险) ---
161
+ if (result.error === "sudo-required") {
162
+ return runWithSudoRetry(currentCmd, timeoutMs, ask);
163
+ }
164
+
165
+ // 其他错误直接返回
166
+ return result;
167
+ }
168
+
169
+ return { ok: false, error: "max retries exceeded", output: "" };
170
+ }
171
+
172
+ // --- CLI ---
173
+ const cmd = process.argv[2] || "uname -a";
174
+ const timeoutMs = Number(process.argv[3] || 10000);
175
+
176
+ console.log(`→ run: ${cmd}`);
177
+ // 自动判断用哪种 wrapper:Arthas 命令走 guard,其他走 sudo 重试
178
+ const { isArthasCommand } = await import("./arthas-guard.js");
179
+ const result = isArthasCommand(cmd)
180
+ ? await runWithArthasGuard(cmd, timeoutMs)
181
+ : await runWithSudoRetry(cmd, timeoutMs);
182
+ if (result.ok) {
183
+ console.log(`✓ ok (${result.elapsedMs}ms)`);
184
+ console.log(result.output);
185
+ } else {
186
+ console.error(`✗ failed: ${result.error || "unknown"}`);
187
+ if (result.output) console.log("--- partial output ---\n" + result.output);
188
+ process.exit(1);
189
+ }
@@ -0,0 +1,15 @@
1
+ {
2
+ "name": "ws-sniffer-proxy",
3
+ "version": "1.0.0",
4
+ "private": true,
5
+ "type": "module",
6
+ "description": "JumpServer xterm 桥接代理:Agent <-> Chrome 插件,请求-响应配对",
7
+ "main": "server.js",
8
+ "scripts": {
9
+ "start": "node server.js",
10
+ "example": "node client-example.mjs"
11
+ },
12
+ "dependencies": {
13
+ "ws": "^8.18.0"
14
+ }
15
+ }