koishi-plugin-spawn-modified 1.2.7 → 1.2.8
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/config.d.ts +18 -0
- package/lib/config.js +19 -0
- package/lib/index.d.ts +3 -17
- package/lib/index.js +25 -282
- package/lib/logger.d.ts +11 -0
- package/lib/logger.js +19 -0
- package/lib/render.d.ts +2 -0
- package/lib/render.js +117 -0
- package/lib/utils.d.ts +20 -0
- package/lib/utils.js +206 -0
- package/package.json +1 -1
- package/src/config.ts +33 -0
- package/src/index.ts +30 -409
- package/src/logger.ts +27 -0
- package/src/render.ts +176 -0
- package/src/utils.ts +203 -0
- package/lib/debug-log.d.ts +0 -2
- package/lib/debug-log.js +0 -20
- package/src/debug-log.ts +0 -5
package/src/utils.ts
ADDED
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import os from 'os'
|
|
2
|
+
import path from 'path'
|
|
3
|
+
|
|
4
|
+
// 命令过滤:支持黑名单/白名单模式
|
|
5
|
+
export function buildRegex(entry: string): RegExp | null {
|
|
6
|
+
try {
|
|
7
|
+
return new RegExp(entry, 'i')
|
|
8
|
+
} catch (_) {
|
|
9
|
+
// 回退为逐字匹配,防止用户写了非法正则
|
|
10
|
+
const escaped = entry.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
11
|
+
try {
|
|
12
|
+
return new RegExp(escaped, 'i')
|
|
13
|
+
} catch (_) {
|
|
14
|
+
return null
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function isCommandBlocked(command: string, mode: 'blacklist' | 'whitelist', list: string[]): boolean {
|
|
20
|
+
if (!list?.length) return false
|
|
21
|
+
const trimmedCommand = command.trim()
|
|
22
|
+
const hit = list.some(entry => {
|
|
23
|
+
const regex = buildRegex(entry)
|
|
24
|
+
return regex ? regex.test(trimmedCommand) : false
|
|
25
|
+
})
|
|
26
|
+
return mode === 'blacklist' ? hit : !hit
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function stripQuotes(text: string): string {
|
|
30
|
+
return text.replace(/^['"]|['"]$/g, '')
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function tokenizeCommand(command: string): string[] {
|
|
34
|
+
const tokens: string[] = []
|
|
35
|
+
let current = ''
|
|
36
|
+
let quote: string | null = null
|
|
37
|
+
|
|
38
|
+
for (let i = 0; i < command.length; i++) {
|
|
39
|
+
const char = command[i]
|
|
40
|
+
|
|
41
|
+
if ((char === '"' || char === "'") && (quote === null || quote === char)) {
|
|
42
|
+
quote = quote ? null : char
|
|
43
|
+
continue
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (!quote && /\s/.test(char)) {
|
|
47
|
+
if (current) {
|
|
48
|
+
tokens.push(current)
|
|
49
|
+
current = ''
|
|
50
|
+
}
|
|
51
|
+
continue
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
current += char
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (current) tokens.push(current)
|
|
58
|
+
return tokens
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function isPathLike(token: string): boolean {
|
|
62
|
+
const trimmed = token.trim()
|
|
63
|
+
if (!trimmed) return false
|
|
64
|
+
if (/^[|&><]+$/.test(trimmed)) return false
|
|
65
|
+
if (/^-{1,2}[a-zA-Z0-9][\w-]*$/.test(trimmed)) return false
|
|
66
|
+
if (/^\$[A-Za-z_][A-Za-z0-9_]*$/.test(trimmed)) return false
|
|
67
|
+
|
|
68
|
+
const normalized = stripQuotes(trimmed)
|
|
69
|
+
|
|
70
|
+
return (
|
|
71
|
+
/^[A-Za-z]:[\\\/]/.test(normalized) ||
|
|
72
|
+
normalized.startsWith('/') ||
|
|
73
|
+
normalized.startsWith('~') ||
|
|
74
|
+
normalized.startsWith('..') ||
|
|
75
|
+
normalized.startsWith('./') ||
|
|
76
|
+
normalized.includes('/') ||
|
|
77
|
+
normalized.includes('\\')
|
|
78
|
+
)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function resolveCandidatePath(candidate: string, currentDir: string): string {
|
|
82
|
+
const cleaned = stripQuotes(candidate.trim())
|
|
83
|
+
const homeDir = os.homedir?.() || ''
|
|
84
|
+
|
|
85
|
+
if (cleaned.startsWith('~')) {
|
|
86
|
+
const withoutTilde = cleaned.slice(1).replace(/^[/\\]/, '')
|
|
87
|
+
const homeResolved = homeDir ? path.join(homeDir, withoutTilde) : cleaned
|
|
88
|
+
return path.resolve(homeResolved)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return path.resolve(currentDir, cleaned)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function extractPathCandidates(command: string): string[] {
|
|
95
|
+
const tokens = tokenizeCommand(command)
|
|
96
|
+
const candidates: string[] = []
|
|
97
|
+
|
|
98
|
+
for (const token of tokens) {
|
|
99
|
+
const normalized = stripQuotes(token)
|
|
100
|
+
if (isPathLike(normalized)) {
|
|
101
|
+
candidates.push(normalized)
|
|
102
|
+
continue
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const eqIndex = normalized.indexOf('=')
|
|
106
|
+
if (eqIndex > 0) {
|
|
107
|
+
const value = normalized.slice(eqIndex + 1)
|
|
108
|
+
if (isPathLike(value)) {
|
|
109
|
+
candidates.push(value)
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return candidates
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// 解析 cd 命令并验证路径
|
|
118
|
+
export function isWithinRoot(rootDir: string, targetPath: string): boolean {
|
|
119
|
+
const relative = path.relative(rootDir, targetPath)
|
|
120
|
+
return relative === '' || (!relative.startsWith('..') && !path.isAbsolute(relative))
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export function validatePathAccess(command: string, currentDir: string, rootDir: string, restrictDirectory: boolean): { valid: boolean; error?: string } {
|
|
124
|
+
if (!restrictDirectory) return { valid: true }
|
|
125
|
+
|
|
126
|
+
const normalizedRoot = path.resolve(rootDir)
|
|
127
|
+
const candidates = extractPathCandidates(command)
|
|
128
|
+
|
|
129
|
+
for (const candidate of candidates) {
|
|
130
|
+
const resolved = resolveCandidatePath(candidate, currentDir)
|
|
131
|
+
if (!isWithinRoot(normalizedRoot, resolved)) {
|
|
132
|
+
return { valid: false, error: 'restricted-path' }
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return { valid: true }
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export function validateCdCommand(command: string, currentDir: string, rootDir: string, restrictDirectory: boolean): { valid: boolean; newDir?: string; error?: string } {
|
|
140
|
+
if (!restrictDirectory) return { valid: true }
|
|
141
|
+
|
|
142
|
+
const normalizedRoot = path.resolve(rootDir)
|
|
143
|
+
const cdMatches: RegExpExecArray[] = []
|
|
144
|
+
const cdRegex = /\bcd\s+([^;&|\n]+)/gi
|
|
145
|
+
let m: RegExpExecArray | null
|
|
146
|
+
while ((m = cdRegex.exec(command)) !== null) {
|
|
147
|
+
cdMatches.push(m)
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (!cdMatches.length) return { valid: true }
|
|
151
|
+
|
|
152
|
+
// 若命令被链式运算符分隔且包含 cd,则要求所有 cd 目标都在指定 root 下,否则拒绝
|
|
153
|
+
for (const match of cdMatches) {
|
|
154
|
+
const target = match[1].trim().replace(/['"]/g, '')
|
|
155
|
+
const absolutePath = path.resolve(currentDir, target)
|
|
156
|
+
if (!isWithinRoot(normalizedRoot, absolutePath)) {
|
|
157
|
+
return { valid: false, error: 'restricted-directory' }
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// 仅当命令是单独的 cd 时才更新会话目录,避免链式命令切换目录后执行其他操作
|
|
162
|
+
const singleCdOnly = /^\s*cd\s+[^;&|\n]+\s*$/i.test(command)
|
|
163
|
+
if (singleCdOnly) {
|
|
164
|
+
const target = cdMatches[0][1].trim().replace(/['"]/g, '')
|
|
165
|
+
const absolutePath = path.resolve(currentDir, target)
|
|
166
|
+
return { valid: true, newDir: absolutePath }
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return { valid: true }
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export function maskCurlOutput(command: string, output: string): string {
|
|
173
|
+
if (!output) return output
|
|
174
|
+
if (!/\bcurl\b/i.test(command)) return output
|
|
175
|
+
|
|
176
|
+
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
|
|
177
|
+
return output.replace(ipv4Regex, (ip) => (isPrivateIpv4(ip) ? ip : '*.*.*.*'))
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export function isPrivateIpv4(ip: string): boolean {
|
|
181
|
+
const octets = ip.split('.').map(Number)
|
|
182
|
+
if (octets.length !== 4) return false
|
|
183
|
+
if (octets.some(octet => Number.isNaN(octet) || octet < 0 || octet > 255)) return false
|
|
184
|
+
|
|
185
|
+
const [a, b] = octets
|
|
186
|
+
|
|
187
|
+
if (a === 10) return true
|
|
188
|
+
if (a === 172 && b >= 16 && b <= 31) return true
|
|
189
|
+
if (a === 192 && b === 168) return true
|
|
190
|
+
if (a === 127) return true
|
|
191
|
+
if (a === 169 && b === 254) return true
|
|
192
|
+
|
|
193
|
+
return false
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
export function escapeHtml(text: string): string {
|
|
197
|
+
return text
|
|
198
|
+
.replace(/&/g, '&')
|
|
199
|
+
.replace(/</g, '<')
|
|
200
|
+
.replace(/>/g, '>')
|
|
201
|
+
.replace(/"/g, '"')
|
|
202
|
+
.replace(/'/g, ''')
|
|
203
|
+
}
|
package/lib/debug-log.d.ts
DELETED
package/lib/debug-log.js
DELETED
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) {
|
|
3
|
-
if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) {
|
|
4
|
-
if (ar || !(i in from)) {
|
|
5
|
-
if (!ar) ar = Array.prototype.slice.call(from, 0, i);
|
|
6
|
-
ar[i] = from[i];
|
|
7
|
-
}
|
|
8
|
-
}
|
|
9
|
-
return to.concat(ar || Array.prototype.slice.call(from));
|
|
10
|
-
};
|
|
11
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
-
exports.debugLog = debugLog;
|
|
13
|
-
function debugLog(ctx, tag) {
|
|
14
|
-
var _a;
|
|
15
|
-
var args = [];
|
|
16
|
-
for (var _i = 2; _i < arguments.length; _i++) {
|
|
17
|
-
args[_i - 2] = arguments[_i];
|
|
18
|
-
}
|
|
19
|
-
(_a = ctx.logger("spawn-debug")).info.apply(_a, __spreadArray(["[".concat(tag, "]")], args, false));
|
|
20
|
-
}
|