koishi-plugin-spawn-modified 1.2.5 → 1.2.7
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.
- package/lib/index.d.ts +1 -0
- package/lib/index.js +43 -11
- package/package.json +1 -1
- package/src/index.ts +37 -4
package/lib/index.d.ts
CHANGED
package/lib/index.js
CHANGED
|
@@ -63,6 +63,7 @@ exports.Config = koishi_1.Schema.object({
|
|
|
63
63
|
encoding: koishi_1.Schema.union(encodings).description('输出内容编码。').default('utf8'),
|
|
64
64
|
timeout: koishi_1.Schema.number().description('最长运行时间。').default(koishi_1.Time.minute),
|
|
65
65
|
renderImage: koishi_1.Schema.boolean().description('是否将命令执行结果渲染为图片(需要安装 puppeteer 插件)。').default(false),
|
|
66
|
+
exemptUsers: koishi_1.Schema.array(String).description('例外用户列表,格式为 "群组ID:用户ID"。私聊时群组ID为0。匹配的用户将无视一切过滤器。').default([]),
|
|
66
67
|
blockedCommands: koishi_1.Schema.array(String).description('违禁命令列表(命令的开头部分)。').default([]),
|
|
67
68
|
restrictDirectory: koishi_1.Schema.boolean().description('是否限制在当前目录及子目录内执行命令(禁止 cd 到上级或其他目录)。').default(false),
|
|
68
69
|
authority: koishi_1.Schema.number().description('exec 命令所需权限等级。').default(4),
|
|
@@ -226,6 +227,33 @@ function validateCdCommand(command, currentDir, rootDir, restrictDirectory) {
|
|
|
226
227
|
}
|
|
227
228
|
return { valid: true };
|
|
228
229
|
}
|
|
230
|
+
function maskCurlOutput(command, output) {
|
|
231
|
+
if (!output)
|
|
232
|
+
return output;
|
|
233
|
+
if (!/\bcurl\b/i.test(command))
|
|
234
|
+
return output;
|
|
235
|
+
var ipv4Regex = /\b(?:(?:25[0-5]|2[0-4]\d|1?\d?\d)\.){3}(?:25[0-5]|2[0-4]\d|1?\d?\d)\b/g;
|
|
236
|
+
return output.replace(ipv4Regex, function (ip) { return (isPrivateIpv4(ip) ? ip : '*.*.*.*'); });
|
|
237
|
+
}
|
|
238
|
+
function isPrivateIpv4(ip) {
|
|
239
|
+
var octets = ip.split('.').map(Number);
|
|
240
|
+
if (octets.length !== 4)
|
|
241
|
+
return false;
|
|
242
|
+
if (octets.some(function (octet) { return Number.isNaN(octet) || octet < 0 || octet > 255; }))
|
|
243
|
+
return false;
|
|
244
|
+
var a = octets[0], b = octets[1];
|
|
245
|
+
if (a === 10)
|
|
246
|
+
return true;
|
|
247
|
+
if (a === 172 && b >= 16 && b <= 31)
|
|
248
|
+
return true;
|
|
249
|
+
if (a === 192 && b === 168)
|
|
250
|
+
return true;
|
|
251
|
+
if (a === 127)
|
|
252
|
+
return true;
|
|
253
|
+
if (a === 169 && b === 254)
|
|
254
|
+
return true;
|
|
255
|
+
return false;
|
|
256
|
+
}
|
|
229
257
|
// 渲染终端输出为图片
|
|
230
258
|
function renderTerminalImage(ctx, workingDir, command, output) {
|
|
231
259
|
return __awaiter(this, void 0, void 0, function () {
|
|
@@ -301,30 +329,34 @@ function apply(ctx, config) {
|
|
|
301
329
|
ctx.i18n.define('zh-CN', require('./locales/zh-CN'));
|
|
302
330
|
ctx.command('exec <command:text>', { authority: (_a = config.authority) !== null && _a !== void 0 ? _a : 4 })
|
|
303
331
|
.action(function (_a, command_1) { return __awaiter(_this, [_a, command_1], void 0, function (_b, command) {
|
|
304
|
-
var filterList, filterMode, sessionId, rootDir, currentDir, cdValidation, pathValidation, timeout, state;
|
|
332
|
+
var guildId, userId, userKey, isExempt, filterList, filterMode, sessionId, rootDir, currentDir, cdValidation, pathValidation, timeout, state;
|
|
305
333
|
var _this = this;
|
|
306
|
-
var _c;
|
|
334
|
+
var _c, _d, _e;
|
|
307
335
|
var session = _b.session;
|
|
308
|
-
return __generator(this, function (
|
|
309
|
-
switch (
|
|
336
|
+
return __generator(this, function (_f) {
|
|
337
|
+
switch (_f.label) {
|
|
310
338
|
case 0:
|
|
311
339
|
if (!command) {
|
|
312
340
|
return [2 /*return*/, session.text('.expect-text')];
|
|
313
341
|
}
|
|
314
342
|
command = (0, koishi_1.h)('', koishi_1.h.parse(command)).toString(true);
|
|
315
|
-
|
|
343
|
+
guildId = session.guildId || '0';
|
|
344
|
+
userId = session.userId || '';
|
|
345
|
+
userKey = "".concat(guildId, ":").concat(userId);
|
|
346
|
+
isExempt = (_d = (_c = config.exemptUsers) === null || _c === void 0 ? void 0 : _c.some(function (entry) { return entry === userKey; })) !== null && _d !== void 0 ? _d : false;
|
|
347
|
+
filterList = (((_e = config.commandList) === null || _e === void 0 ? void 0 : _e.length) ? config.commandList : config.blockedCommands) || [];
|
|
316
348
|
filterMode = config.commandFilterMode || 'blacklist';
|
|
317
|
-
if (isCommandBlocked(command, filterMode, filterList)) {
|
|
349
|
+
if (!isExempt && isCommandBlocked(command, filterMode, filterList)) {
|
|
318
350
|
return [2 /*return*/, session.text('.blocked-command')];
|
|
319
351
|
}
|
|
320
352
|
sessionId = session.uid || session.channelId;
|
|
321
353
|
rootDir = path_1.default.resolve(ctx.baseDir, config.root);
|
|
322
354
|
currentDir = sessionDirs.get(sessionId) || rootDir;
|
|
323
|
-
cdValidation = validateCdCommand(command, currentDir, rootDir, config.restrictDirectory);
|
|
355
|
+
cdValidation = validateCdCommand(command, currentDir, rootDir, !isExempt && config.restrictDirectory);
|
|
324
356
|
if (!cdValidation.valid) {
|
|
325
357
|
return [2 /*return*/, session.text('.restricted-directory')];
|
|
326
358
|
}
|
|
327
|
-
pathValidation = validatePathAccess(command, currentDir, rootDir, config.restrictDirectory);
|
|
359
|
+
pathValidation = validatePathAccess(command, currentDir, rootDir, !isExempt && config.restrictDirectory);
|
|
328
360
|
if (!pathValidation.valid) {
|
|
329
361
|
return [2 /*return*/, session.text('.restricted-path')];
|
|
330
362
|
}
|
|
@@ -333,8 +365,8 @@ function apply(ctx, config) {
|
|
|
333
365
|
if (!!config.renderImage) return [3 /*break*/, 2];
|
|
334
366
|
return [4 /*yield*/, session.send(session.text('.started', state))];
|
|
335
367
|
case 1:
|
|
336
|
-
|
|
337
|
-
|
|
368
|
+
_f.sent();
|
|
369
|
+
_f.label = 2;
|
|
338
370
|
case 2: return [2 /*return*/, new Promise(function (resolve) {
|
|
339
371
|
var start = Date.now();
|
|
340
372
|
var child = (0, child_process_1.exec)(command, {
|
|
@@ -358,7 +390,7 @@ function apply(ctx, config) {
|
|
|
358
390
|
state.code = code;
|
|
359
391
|
state.signal = signal;
|
|
360
392
|
state.timeUsed = Date.now() - start;
|
|
361
|
-
state.output = state.output.trim();
|
|
393
|
+
state.output = maskCurlOutput(command, state.output.trim());
|
|
362
394
|
// 更新当前目录(如果是 cd 命令且执行成功)
|
|
363
395
|
if (cdValidation.newDir && code === 0) {
|
|
364
396
|
sessionDirs.set(sessionId, cdValidation.newDir);
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -21,6 +21,7 @@ export interface Config {
|
|
|
21
21
|
encoding?: typeof encodings[number]
|
|
22
22
|
timeout?: number
|
|
23
23
|
renderImage?: boolean
|
|
24
|
+
exemptUsers?: string[]
|
|
24
25
|
blockedCommands?: string[]
|
|
25
26
|
restrictDirectory?: boolean
|
|
26
27
|
authority?: number
|
|
@@ -34,6 +35,7 @@ export const Config: Schema<Config> = Schema.object({
|
|
|
34
35
|
encoding: Schema.union(encodings).description('输出内容编码。').default('utf8'),
|
|
35
36
|
timeout: Schema.number().description('最长运行时间。').default(Time.minute),
|
|
36
37
|
renderImage: Schema.boolean().description('是否将命令执行结果渲染为图片(需要安装 puppeteer 插件)。').default(false),
|
|
38
|
+
exemptUsers: Schema.array(String).description('例外用户列表,格式为 "群组ID:用户ID"。私聊时群组ID为0。匹配的用户将无视一切过滤器。').default([]),
|
|
37
39
|
blockedCommands: Schema.array(String).description('违禁命令列表(命令的开头部分)。').default([]),
|
|
38
40
|
restrictDirectory: Schema.boolean().description('是否限制在当前目录及子目录内执行命令(禁止 cd 到上级或其他目录)。').default(false),
|
|
39
41
|
authority: Schema.number().description('exec 命令所需权限等级。').default(4),
|
|
@@ -227,6 +229,30 @@ function validateCdCommand(command: string, currentDir: string, rootDir: string,
|
|
|
227
229
|
return { valid: true }
|
|
228
230
|
}
|
|
229
231
|
|
|
232
|
+
function maskCurlOutput(command: string, output: string): string {
|
|
233
|
+
if (!output) return output
|
|
234
|
+
if (!/\bcurl\b/i.test(command)) return output
|
|
235
|
+
|
|
236
|
+
const ipv4Regex = /\b(?:(?:25[0-5]|2[0-4]\d|1?\d?\d)\.){3}(?:25[0-5]|2[0-4]\d|1?\d?\d)\b/g
|
|
237
|
+
return output.replace(ipv4Regex, (ip) => (isPrivateIpv4(ip) ? ip : '*.*.*.*'))
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function isPrivateIpv4(ip: string): boolean {
|
|
241
|
+
const octets = ip.split('.').map(Number)
|
|
242
|
+
if (octets.length !== 4) return false
|
|
243
|
+
if (octets.some(octet => Number.isNaN(octet) || octet < 0 || octet > 255)) return false
|
|
244
|
+
|
|
245
|
+
const [a, b] = octets
|
|
246
|
+
|
|
247
|
+
if (a === 10) return true
|
|
248
|
+
if (a === 172 && b >= 16 && b <= 31) return true
|
|
249
|
+
if (a === 192 && b === 168) return true
|
|
250
|
+
if (a === 127) return true
|
|
251
|
+
if (a === 169 && b === 254) return true
|
|
252
|
+
|
|
253
|
+
return false
|
|
254
|
+
}
|
|
255
|
+
|
|
230
256
|
// 渲染终端输出为图片
|
|
231
257
|
async function renderTerminalImage(ctx: Context, workingDir: string, command: string, output: string): Promise<h> {
|
|
232
258
|
if (!ctx.puppeteer) {
|
|
@@ -417,21 +443,28 @@ export function apply(ctx: Context, config: Config) {
|
|
|
417
443
|
}
|
|
418
444
|
|
|
419
445
|
command = h('', h.parse(command)).toString(true)
|
|
446
|
+
|
|
447
|
+
// 检查是否为例外用户(无视一切过滤器)
|
|
448
|
+
const guildId = session.guildId || '0'
|
|
449
|
+
const userId = session.userId || ''
|
|
450
|
+
const userKey = `${guildId}:${userId}`
|
|
451
|
+
const isExempt = config.exemptUsers?.some(entry => entry === userKey) ?? false
|
|
452
|
+
|
|
420
453
|
// 检查命令过滤(黑/白名单);仅使用配置提供的正则
|
|
421
454
|
const filterList = (config.commandList?.length ? config.commandList : config.blockedCommands) || []
|
|
422
455
|
const filterMode = config.commandFilterMode || 'blacklist'
|
|
423
|
-
if (isCommandBlocked(command, filterMode, filterList)) {
|
|
456
|
+
if (!isExempt && isCommandBlocked(command, filterMode, filterList)) {
|
|
424
457
|
return session.text('.blocked-command')
|
|
425
458
|
}
|
|
426
459
|
const sessionId = session.uid || session.channelId
|
|
427
460
|
const rootDir = path.resolve(ctx.baseDir, config.root)
|
|
428
461
|
const currentDir = sessionDirs.get(sessionId) || rootDir
|
|
429
462
|
// 验证 cd 命令
|
|
430
|
-
const cdValidation = validateCdCommand(command, currentDir, rootDir, config.restrictDirectory)
|
|
463
|
+
const cdValidation = validateCdCommand(command, currentDir, rootDir, !isExempt && config.restrictDirectory)
|
|
431
464
|
if (!cdValidation.valid) {
|
|
432
465
|
return session.text('.restricted-directory')
|
|
433
466
|
}
|
|
434
|
-
const pathValidation = validatePathAccess(command, currentDir, rootDir, config.restrictDirectory)
|
|
467
|
+
const pathValidation = validatePathAccess(command, currentDir, rootDir, !isExempt && config.restrictDirectory)
|
|
435
468
|
if (!pathValidation.valid) {
|
|
436
469
|
return session.text('.restricted-path')
|
|
437
470
|
}
|
|
@@ -459,7 +492,7 @@ export function apply(ctx: Context, config: Config) {
|
|
|
459
492
|
state.code = code
|
|
460
493
|
state.signal = signal
|
|
461
494
|
state.timeUsed = Date.now() - start
|
|
462
|
-
state.output = state.output.trim()
|
|
495
|
+
state.output = maskCurlOutput(command, state.output.trim())
|
|
463
496
|
// 更新当前目录(如果是 cd 命令且执行成功)
|
|
464
497
|
if (cdValidation.newDir && code === 0) {
|
|
465
498
|
sessionDirs.set(sessionId, cdValidation.newDir)
|