@myassis/gateway 1.0.49 → 1.0.51
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.
|
@@ -158,20 +158,19 @@ function writeGatewayLauncherScript(exePath, scriptPath, workDir) {
|
|
|
158
158
|
"$workDir = '" + workDir.replace(/\\/g, '\\\\') + "'",
|
|
159
159
|
"$pidFile = '" + GATEWAY_PID_FILE.replace(/\\/g, '\\\\') + "'",
|
|
160
160
|
"",
|
|
161
|
-
"# 启动 Gateway
|
|
161
|
+
"# 启动 Gateway(无参启动,直接进入 HTTP Server 模式,监听默认端口 3001)",
|
|
162
162
|
"$psi = New-Object System.Diagnostics.ProcessStartInfo",
|
|
163
163
|
"$psi.FileName = $exe",
|
|
164
164
|
"$psi.WorkingDirectory = $workDir",
|
|
165
165
|
"$psi.UseShellExecute = $false",
|
|
166
|
-
"$psi.RedirectStandardOutput = $true",
|
|
167
|
-
"$psi.RedirectStandardError = $true",
|
|
168
166
|
"$psi.CreateNoWindow = $true",
|
|
167
|
+
"# 注意:不重定向 stdout/stderr,避免匿名管道 4KB 缓冲区打满后阻塞 gateway 的事件循环",
|
|
169
168
|
"$psi.Environment['NODE_ENV'] = 'production'",
|
|
170
169
|
"$proc = [System.Diagnostics.Process]::Start($psi)",
|
|
171
170
|
"if ($proc) {",
|
|
172
171
|
" $proc.Id.ToString() | Out-File -FilePath $pidFile -Encoding UTF8",
|
|
173
|
-
" $proc.WaitForExit()",
|
|
174
172
|
"}",
|
|
173
|
+
"# fire-and-forget:不 WaitForExit,PowerShell 立即退出,省 ~30MB 常驻内存",
|
|
175
174
|
].join('\r\n');
|
|
176
175
|
// 确保目标目录存在(%LOCALAPPDATA%\我的助手 首次启动时可能不存在)
|
|
177
176
|
try {
|
|
@@ -437,11 +436,22 @@ async function ensureAutoStartHealthy() {
|
|
|
437
436
|
// 2) 未注册→开发机或从未安装,不做任何事
|
|
438
437
|
if (!currentValue)
|
|
439
438
|
return;
|
|
440
|
-
// 3)
|
|
439
|
+
// 3) 评估健康度(Run key + 脚本内容双重检查)
|
|
441
440
|
const lower = currentValue.toLowerCase();
|
|
442
441
|
const hasPowershell = lower.includes('powershell.exe');
|
|
443
442
|
const pointsToNewPath = lower.includes(GATEWAY_LAUNCHER_FILE.toLowerCase());
|
|
444
|
-
|
|
443
|
+
// 脚本内容健康:不能含 RedirectStandardOutput(旧版本会因管道阻塞卡死)
|
|
444
|
+
let scriptHealthy = false;
|
|
445
|
+
try {
|
|
446
|
+
if (fs_1.default.existsSync(GATEWAY_LAUNCHER_FILE)) {
|
|
447
|
+
const scriptContent = fs_1.default.readFileSync(GATEWAY_LAUNCHER_FILE, 'utf8');
|
|
448
|
+
// 老版含 RedirectStandardOutput(管道阻塞 bug)或 WaitForExit(陪跑浪费内存)→ 判定不健康
|
|
449
|
+
scriptHealthy = !scriptContent.includes('RedirectStandardOutput')
|
|
450
|
+
&& !scriptContent.includes('WaitForExit');
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
catch { /* ignore */ }
|
|
454
|
+
if (hasPowershell && pointsToNewPath && scriptHealthy)
|
|
445
455
|
return; // 已健康
|
|
446
456
|
logger.warn(`检测到 Run key 不健康,自动修复。current='${currentValue}'`);
|
|
447
457
|
// 4) 重写 launcher 脚本到新路径
|
|
@@ -6,8 +6,6 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
6
6
|
exports.execTool = exports.cleanupExpiredApprovals = exports.getAndRemovePendingApproval = exports.addPendingApproval = exports.clearSessionCwd = exports.setSessionCwd = exports.getSessionCwd = void 0;
|
|
7
7
|
const child_process_1 = require("child_process");
|
|
8
8
|
const util_1 = require("util");
|
|
9
|
-
const path_1 = __importDefault(require("path"));
|
|
10
|
-
const os_1 = __importDefault(require("os"));
|
|
11
9
|
const shared_1 = require("@myassis/shared");
|
|
12
10
|
const crypto_1 = __importDefault(require("crypto"));
|
|
13
11
|
const logger = (0, shared_1.getLogger)('exec');
|
|
@@ -17,39 +15,6 @@ const pendingApprovals = new Map();
|
|
|
17
15
|
// 会话级工作目录状态:sessionId -> cwd
|
|
18
16
|
// 让 cd 命令能跨多次 exec 调用持久化工作目录
|
|
19
17
|
const sessionCwdMap = new Map();
|
|
20
|
-
/** 解析 cd 参数,返回目标绝对路径。支持 ~、相对路径、绝对路径 */
|
|
21
|
-
function resolveCdTarget(cdArg, currentCwd) {
|
|
22
|
-
const arg = cdArg.trim();
|
|
23
|
-
if (!arg)
|
|
24
|
-
return os_1.default.homedir(); // cd 无参数 → 回家目录
|
|
25
|
-
if (arg === '~' || arg.startsWith('~/')) {
|
|
26
|
-
return path_1.default.join(os_1.default.homedir(), arg.slice(1));
|
|
27
|
-
}
|
|
28
|
-
// Windows 盘符路径或 Unix 绝对路径
|
|
29
|
-
if (path_1.default.isAbsolute(arg))
|
|
30
|
-
return path_1.default.resolve(arg);
|
|
31
|
-
// 相对路径:基于当前 cwd 解析
|
|
32
|
-
return path_1.default.resolve(currentCwd, arg);
|
|
33
|
-
}
|
|
34
|
-
/** 检查命令是否以 cd 开头,若是则提取目标路径(支持 cd /d D:\path 这种 Windows 写法)*/
|
|
35
|
-
function parseCdCommand(command) {
|
|
36
|
-
const trimmed = command.trim();
|
|
37
|
-
// 匹配:cd arg 或 cd /d arg(Windows)或 cd "path with spaces"
|
|
38
|
-
const match = trimmed.match(/^cd(?:\s+\/d)?\s+(.+)$/is);
|
|
39
|
-
if (!match) {
|
|
40
|
-
// 裸 cd(无参数)→ 返回 home
|
|
41
|
-
if (/^cd\s*$/i.test(trimmed))
|
|
42
|
-
return '';
|
|
43
|
-
return null;
|
|
44
|
-
}
|
|
45
|
-
// 去掉末尾可能存在的 && 其他命令,只取 cd 的目标
|
|
46
|
-
let target = match[1].trim();
|
|
47
|
-
// 去掉引号
|
|
48
|
-
target = target.replace(/^['"]|['"]$/g, '');
|
|
49
|
-
// 如果 target 里包含 && 或 ; ,只取第一部分
|
|
50
|
-
const cut = target.split(/&&|\|/)[0].trim();
|
|
51
|
-
return cut || '';
|
|
52
|
-
}
|
|
53
18
|
/** 获取某个 session 的当前工作目录 */
|
|
54
19
|
function getSessionCwd(sessionId) {
|
|
55
20
|
return sessionCwdMap.get(sessionId);
|
|
@@ -72,32 +37,99 @@ function generateApprovalToken() {
|
|
|
72
37
|
function tokenizeCommand(command) {
|
|
73
38
|
return command.match(/"[^"]*"|'[^']*'|&&|\|\||[;&|\r\n]|\S+/g) ?? [];
|
|
74
39
|
}
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
40
|
+
/** 去掉外层成对引号 */
|
|
41
|
+
function stripOuterQuotes(token) {
|
|
42
|
+
if (token.length >= 2) {
|
|
43
|
+
const first = token[0];
|
|
44
|
+
const last = token[token.length - 1];
|
|
45
|
+
if ((first === '"' || first === "'") && first === last) {
|
|
46
|
+
return token.slice(1, -1);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return token;
|
|
50
|
+
}
|
|
51
|
+
function getCommandName(token) {
|
|
52
|
+
const normalized = stripOuterQuotes(token)
|
|
53
|
+
.replace(/\\/g, '/')
|
|
54
|
+
.toLowerCase();
|
|
55
|
+
return (normalized.split('/').pop() ?? normalized)
|
|
56
|
+
.replace(/\.(exe|cmd|bat|ps1|sh)$/i, '');
|
|
57
|
+
}
|
|
58
|
+
// 参数被视为「文本/脚本」而非「可执行动词」的命令:
|
|
59
|
+
// - 文本处理类(参数通常是正则/字符串):grep/egrep/fgrep/findstr/sed/awk/printf/echo
|
|
60
|
+
// - 其它高频误报:head/tail/cat 后的文件名一般不含危险词,这里不特殊处理
|
|
61
|
+
const TEXT_COMMANDS = new Set([
|
|
62
|
+
'echo', 'printf',
|
|
63
|
+
'findstr',
|
|
64
|
+
'grep', 'egrep', 'fgrep',
|
|
65
|
+
'sed', 'awk', 'gawk',
|
|
66
|
+
]);
|
|
67
|
+
// Shell 执行器:这些命令后面若跟 -c/-lc/-Command 等参数,
|
|
68
|
+
// 紧随其后的字符串是「内嵌脚本」,需要递归解析
|
|
69
|
+
const SHELL_EXECUTORS = new Set([
|
|
70
|
+
'bash', 'sh', 'zsh', 'dash', 'ksh',
|
|
71
|
+
'pwsh', 'powershell',
|
|
72
|
+
'cmd',
|
|
73
|
+
'wsl', // wsl -- bash -c "..." 之类
|
|
74
|
+
'ssh', // ssh host "remote-cmd" 情况
|
|
75
|
+
]);
|
|
76
|
+
// -c / -lc / --command / /c / /k / -Command 等:其下一个 token 是脚本
|
|
77
|
+
const SCRIPT_ARG_FLAGS = new Set([
|
|
78
|
+
'-c', '-lc', '-ic', '-lic',
|
|
79
|
+
'--command',
|
|
80
|
+
'/c', '/k',
|
|
81
|
+
'-command',
|
|
82
|
+
]);
|
|
83
|
+
const SEPARATORS = new Set(['&&', '||', '&', '|', ';', '\r', '\n']);
|
|
84
|
+
/**
|
|
85
|
+
* 构建「安全审计视图」:
|
|
86
|
+
* - 文本类命令(grep/echo/…)的参数被剥离,避免正则里的 reboot/shutdown 触发误报
|
|
87
|
+
* - Shell 执行器(bash -c "...")的内嵌脚本被递归展开,让内部真实命令仍能被审计
|
|
88
|
+
*/
|
|
89
|
+
function buildCommandSafetyView(command, depth = 0) {
|
|
90
|
+
if (depth > 4)
|
|
91
|
+
return ''; // 防递归爆炸
|
|
78
92
|
const tokens = tokenizeCommand(command);
|
|
79
93
|
const view = [];
|
|
80
94
|
let skipTextArgs = false;
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
.replace(/^["']|["']$/g, '')
|
|
84
|
-
.replace(/\\/g, '/')
|
|
85
|
-
.toLowerCase();
|
|
86
|
-
return (normalized.split('/').pop() ?? normalized)
|
|
87
|
-
.replace(/\.(exe|cmd|bat|ps1|sh)$/i, '');
|
|
88
|
-
};
|
|
95
|
+
let expectScriptArg = false; // 上一个 token 是 -c / -lc 等,本 token 是脚本字符串
|
|
96
|
+
let inShellExecutor = false; // 当前命令词是 bash/sh/pwsh 之类
|
|
89
97
|
for (const token of tokens) {
|
|
90
|
-
if (
|
|
98
|
+
if (SEPARATORS.has(token)) {
|
|
91
99
|
view.push(token);
|
|
92
100
|
skipTextArgs = false;
|
|
101
|
+
expectScriptArg = false;
|
|
102
|
+
inShellExecutor = false;
|
|
93
103
|
continue;
|
|
94
104
|
}
|
|
105
|
+
// 情况 A:这个 token 是 shell 执行器的脚本参数 —— 递归展开
|
|
106
|
+
if (expectScriptArg) {
|
|
107
|
+
const inner = stripOuterQuotes(token);
|
|
108
|
+
const innerView = buildCommandSafetyView(inner, depth + 1);
|
|
109
|
+
if (innerView)
|
|
110
|
+
view.push(innerView);
|
|
111
|
+
expectScriptArg = false;
|
|
112
|
+
skipTextArgs = false;
|
|
113
|
+
inShellExecutor = false;
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
// 情况 B:文本类命令的参数 —— 直接丢弃
|
|
95
117
|
if (skipTextArgs) {
|
|
96
118
|
continue;
|
|
97
119
|
}
|
|
98
120
|
view.push(token);
|
|
99
|
-
|
|
121
|
+
const cmdName = getCommandName(token);
|
|
122
|
+
// 记录命令词 → 决定后续策略
|
|
123
|
+
if (TEXT_COMMANDS.has(cmdName)) {
|
|
100
124
|
skipTextArgs = true;
|
|
125
|
+
inShellExecutor = false;
|
|
126
|
+
}
|
|
127
|
+
else if (SHELL_EXECUTORS.has(cmdName)) {
|
|
128
|
+
inShellExecutor = true;
|
|
129
|
+
}
|
|
130
|
+
else if (inShellExecutor && SCRIPT_ARG_FLAGS.has(token.toLowerCase())) {
|
|
131
|
+
// bash -c / pwsh -Command 后面的字符串是脚本
|
|
132
|
+
expectScriptArg = true;
|
|
101
133
|
}
|
|
102
134
|
}
|
|
103
135
|
return view.join(' ');
|