@lievclouds/dsh-cortex 0.5.4 → 1.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.
Potentially problematic release.
This version of @lievclouds/dsh-cortex might be problematic. Click here for more details.
- package/README.md +1 -1
- package/client.js +434 -412
- package/index.js +7 -0
- package/locale/en.json +6 -6
- package/locale/zh.json +6 -6
- package/package.json +115 -110
- package/src/cortex-ui/dsh/css-overrides.css +8 -0
- package/src/cortex-ui/renderer/App.tsx +39 -0
- package/src/cortex-ui/renderer/components/miniapp/FileTree.tsx +161 -0
- package/src/cortex-ui/renderer/components/miniapp/MiniappCard.tsx +263 -0
- package/src/cortex-ui/renderer/components/miniapp/MiniappDetail.tsx +1493 -0
- package/src/cortex-ui/renderer/components/retest/CurlImportDialog.tsx +158 -0
- package/src/cortex-ui/renderer/components/retest/RepeaterCollectionsDialog.tsx +327 -0
- package/src/cortex-ui/renderer/components/retest/RepeaterHistoryDialog.tsx +242 -0
- package/src/cortex-ui/renderer/components/retest/RepeaterPanel.tsx +291 -137
- package/src/cortex-ui/renderer/components/retest/YakitSendPanel.tsx +411 -0
- package/src/cortex-ui/renderer/i18n/messages/common.ts +2 -0
- package/src/cortex-ui/renderer/i18n/messages/fuzzer.ts +32 -0
- package/src/cortex-ui/renderer/i18n/messages/index.ts +5 -1
- package/src/cortex-ui/renderer/i18n/messages/miniapp.ts +392 -0
- package/src/cortex-ui/renderer/i18n/messages/retest.ts +144 -2
- package/src/cortex-ui/renderer/lib/curl.ts +635 -0
- package/src/cortex-ui/renderer/lib/miniappApi.ts +459 -0
- package/src/cortex-ui/renderer/lib/navigation.ts +13 -1
- package/src/cortex-ui/renderer/lib/repeaterRaw.ts +102 -0
- package/src/cortex-ui/renderer/lib/repeaterStoreApi.ts +163 -0
- package/src/cortex-ui/renderer/lib/retestResponse.ts +64 -17
- package/src/cortex-ui/renderer/pages/FuzzerPage.tsx +212 -0
- package/src/cortex-ui/renderer/pages/MiniappPage.tsx +664 -0
- package/src/host/cortex/index.js +42 -1
- package/src/host/cortex/repeater-store.js +411 -0
- package/src/host/cortex/repeater.js +15 -2
- package/src/host/cortex/tools.js +2 -0
- package/src/host/miniapp/analyze.js +1078 -0
- package/src/host/miniapp/assets-import.js +294 -0
- package/src/host/miniapp/detect.js +426 -0
- package/src/host/miniapp/format.js +263 -0
- package/src/host/miniapp/roots.js +360 -0
- package/src/host/miniapp/routes.js +304 -0
- package/src/host/miniapp/service.js +559 -0
- package/src/host/miniapp/store.js +756 -0
- package/src/host/miniapp/tools.js +335 -0
- package/src/host/miniapp/unpack.js +1010 -0
- package/src/host/miniapp/wxapkg.js +416 -0
|
@@ -0,0 +1,635 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* curl import/export helpers for the single Repeater.
|
|
3
|
+
*
|
|
4
|
+
* Everything here is pure (no network, no DOM); the dialog only calls
|
|
5
|
+
* `parseCurlCommand` and `buildCurlCommand`. The parser is intentionally
|
|
6
|
+
* "best effort": it covers the common bash/zsh and Windows cmd forms without
|
|
7
|
+
* pretending to be a shell. Options that cannot be represented in a raw HTTP
|
|
8
|
+
* request (proxies, certificates, output files, multipart uploads...) are
|
|
9
|
+
* ignored and reported via `ignored`.
|
|
10
|
+
*/
|
|
11
|
+
import {
|
|
12
|
+
serializeRawRequest,
|
|
13
|
+
type ParsedRawRequest
|
|
14
|
+
} from './repeaterRaw'
|
|
15
|
+
|
|
16
|
+
export type CurlParseErrorCode = 'empty' | 'no-url' | 'unterminated-quote'
|
|
17
|
+
|
|
18
|
+
export interface CurlParseFailure {
|
|
19
|
+
ok: false
|
|
20
|
+
code: CurlParseErrorCode
|
|
21
|
+
/** Raw token/option that triggered the failure, never sent to a toast. */
|
|
22
|
+
detail?: string
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface CurlParseSuccess {
|
|
26
|
+
ok: true
|
|
27
|
+
request: ParsedRawRequest
|
|
28
|
+
/** Raw editor text derived from the parsed request. */
|
|
29
|
+
raw: string
|
|
30
|
+
/** Recognized but unsupported options; kept for debugging/reporting only. */
|
|
31
|
+
ignored: string[]
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export type CurlParseResult = CurlParseSuccess | CurlParseFailure
|
|
35
|
+
|
|
36
|
+
type TokenizeResult =
|
|
37
|
+
| { ok: true; tokens: string[] }
|
|
38
|
+
| { ok: false; code: 'unterminated-quote' }
|
|
39
|
+
|
|
40
|
+
/** Options that take a separate value and are not part of the request we keep. */
|
|
41
|
+
const DISCARDED_VALUE_OPTIONS = new Set([
|
|
42
|
+
'-x',
|
|
43
|
+
'--proxy',
|
|
44
|
+
'--proxy-user',
|
|
45
|
+
'-m',
|
|
46
|
+
'--max-time',
|
|
47
|
+
'--connect-timeout',
|
|
48
|
+
'--retry',
|
|
49
|
+
'--retry-delay',
|
|
50
|
+
'-o',
|
|
51
|
+
'--output',
|
|
52
|
+
'--cacert',
|
|
53
|
+
'--capath',
|
|
54
|
+
'--cert',
|
|
55
|
+
'--key',
|
|
56
|
+
'--cert-type',
|
|
57
|
+
'--key-type',
|
|
58
|
+
'--pass',
|
|
59
|
+
'--resolve',
|
|
60
|
+
'--interface',
|
|
61
|
+
'--dns-servers',
|
|
62
|
+
'--limit-rate',
|
|
63
|
+
'--max-filesize',
|
|
64
|
+
'--range',
|
|
65
|
+
'-r',
|
|
66
|
+
'--write-out',
|
|
67
|
+
'-w',
|
|
68
|
+
'--upload-file',
|
|
69
|
+
'-T',
|
|
70
|
+
'--form',
|
|
71
|
+
'-F',
|
|
72
|
+
'--form-string',
|
|
73
|
+
'--dump-header',
|
|
74
|
+
'-D',
|
|
75
|
+
'--cookie-jar',
|
|
76
|
+
'-c',
|
|
77
|
+
'--oauth2-bearer',
|
|
78
|
+
'--connect-to',
|
|
79
|
+
'--local-port',
|
|
80
|
+
'--keepalive-time',
|
|
81
|
+
'--max-redirs',
|
|
82
|
+
'--proto',
|
|
83
|
+
'--proto-redir',
|
|
84
|
+
'--http-version',
|
|
85
|
+
'--tls-max'
|
|
86
|
+
])
|
|
87
|
+
|
|
88
|
+
/** Short options that consume a value: the attached form (`-XPOST`) is common. */
|
|
89
|
+
const SHORT_VALUE_OPTIONS = new Set([
|
|
90
|
+
'-X',
|
|
91
|
+
'-H',
|
|
92
|
+
'-d',
|
|
93
|
+
'-b',
|
|
94
|
+
'-A',
|
|
95
|
+
'-e',
|
|
96
|
+
'-u',
|
|
97
|
+
'-x',
|
|
98
|
+
'-m',
|
|
99
|
+
'-o',
|
|
100
|
+
'-w',
|
|
101
|
+
'-r',
|
|
102
|
+
'-T',
|
|
103
|
+
'-F',
|
|
104
|
+
'-D',
|
|
105
|
+
'-c'
|
|
106
|
+
])
|
|
107
|
+
|
|
108
|
+
/** Boolean short flags we can safely ignore (combined forms like `-sS` too). */
|
|
109
|
+
const SHORT_BOOLEAN_FLAGS = new Set('sSkLifgvNIqO462J1'.split(''))
|
|
110
|
+
|
|
111
|
+
/** Boolean long flags we can safely ignore. */
|
|
112
|
+
const LONG_BOOLEAN_FLAGS = new Set([
|
|
113
|
+
'--silent',
|
|
114
|
+
'--show-error',
|
|
115
|
+
'--insecure',
|
|
116
|
+
'--location',
|
|
117
|
+
'--location-trusted',
|
|
118
|
+
'--include',
|
|
119
|
+
'--verbose',
|
|
120
|
+
'--fail',
|
|
121
|
+
'--fail-with-body',
|
|
122
|
+
'--globoff',
|
|
123
|
+
'--compressed',
|
|
124
|
+
'--no-buffer',
|
|
125
|
+
'--ipv4',
|
|
126
|
+
'--ipv6',
|
|
127
|
+
'--http1.0',
|
|
128
|
+
'--http1.1',
|
|
129
|
+
'--http2',
|
|
130
|
+
'--http2-prior-knowledge',
|
|
131
|
+
'--http3',
|
|
132
|
+
'--tlsv1.0',
|
|
133
|
+
'--tlsv1.1',
|
|
134
|
+
'--tlsv1.2',
|
|
135
|
+
'--tlsv1.3',
|
|
136
|
+
'--disable',
|
|
137
|
+
'--path-as-is',
|
|
138
|
+
'--raw',
|
|
139
|
+
'--remote-name',
|
|
140
|
+
'--remote-header-name',
|
|
141
|
+
'--create-dirs',
|
|
142
|
+
'--no-keepalive',
|
|
143
|
+
'--no-progress-meter',
|
|
144
|
+
'--progress-bar',
|
|
145
|
+
'--trace-ascii',
|
|
146
|
+
'--trace-time',
|
|
147
|
+
'--ignore-content-length'
|
|
148
|
+
])
|
|
149
|
+
|
|
150
|
+
const BASE64_ALPHABET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'
|
|
151
|
+
|
|
152
|
+
/** Strip bash (`\`), Windows cmd (`^`) and PowerShell (`` ` ``) line continuations. */
|
|
153
|
+
function stripLineContinuations(text: string): string {
|
|
154
|
+
return text
|
|
155
|
+
.replace(/\\\r?\n/g, ' ')
|
|
156
|
+
.replace(/\^\r?\n/g, ' ')
|
|
157
|
+
.replace(/`\r?\n/g, ' ')
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function isWhitespace(char: string | undefined): boolean {
|
|
161
|
+
return char !== undefined && /\s/.test(char)
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function decodeAnsiEscape(
|
|
165
|
+
text: string,
|
|
166
|
+
slashIndex: number
|
|
167
|
+
): { value: string; index: number } {
|
|
168
|
+
const next = text[slashIndex + 1]
|
|
169
|
+
if (next === undefined) return { value: '\\', index: slashIndex }
|
|
170
|
+
switch (next) {
|
|
171
|
+
case 'n':
|
|
172
|
+
return { value: '\n', index: slashIndex + 1 }
|
|
173
|
+
case 'r':
|
|
174
|
+
return { value: '\r', index: slashIndex + 1 }
|
|
175
|
+
case 't':
|
|
176
|
+
return { value: '\t', index: slashIndex + 1 }
|
|
177
|
+
case '\\':
|
|
178
|
+
return { value: '\\', index: slashIndex + 1 }
|
|
179
|
+
case "'":
|
|
180
|
+
return { value: "'", index: slashIndex + 1 }
|
|
181
|
+
case '"':
|
|
182
|
+
return { value: '"', index: slashIndex + 1 }
|
|
183
|
+
default:
|
|
184
|
+
return { value: next, index: slashIndex + 1 }
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Split a curl command line into argv-like tokens.
|
|
190
|
+
*
|
|
191
|
+
* Supports POSIX single/double quotes, `$'...'` ANSI-C strings, backslash
|
|
192
|
+
* escapes and Windows cmd caret escapes. Backslashes inside paths (`C:\tmp`)
|
|
193
|
+
* stay literal unless they escape whitespace, quotes or another backslash.
|
|
194
|
+
*/
|
|
195
|
+
export function tokenizeCurl(input: string): TokenizeResult {
|
|
196
|
+
const text = stripLineContinuations(input)
|
|
197
|
+
const tokens: string[] = []
|
|
198
|
+
let current = ''
|
|
199
|
+
let quote: "'" | '"' | null = null
|
|
200
|
+
let ansiC = false
|
|
201
|
+
let started = false
|
|
202
|
+
|
|
203
|
+
const flush = (): void => {
|
|
204
|
+
if (!started) return
|
|
205
|
+
tokens.push(current)
|
|
206
|
+
current = ''
|
|
207
|
+
started = false
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
for (let index = 0; index < text.length; index += 1) {
|
|
211
|
+
const char = text[index] ?? ''
|
|
212
|
+
|
|
213
|
+
if (ansiC) {
|
|
214
|
+
if (char === "'") {
|
|
215
|
+
ansiC = false
|
|
216
|
+
} else if (char === '\\') {
|
|
217
|
+
const decoded = decodeAnsiEscape(text, index)
|
|
218
|
+
current += decoded.value
|
|
219
|
+
index = decoded.index
|
|
220
|
+
} else {
|
|
221
|
+
current += char
|
|
222
|
+
}
|
|
223
|
+
continue
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
if (quote === "'") {
|
|
227
|
+
if (char === "'") quote = null
|
|
228
|
+
else current += char
|
|
229
|
+
continue
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
if (quote === '"') {
|
|
233
|
+
if (char === '"') {
|
|
234
|
+
quote = null
|
|
235
|
+
} else if (char === '\\') {
|
|
236
|
+
const next = text[index + 1]
|
|
237
|
+
if (next === '"' || next === '\\' || next === '$' || next === '`') {
|
|
238
|
+
current += next
|
|
239
|
+
index += 1
|
|
240
|
+
} else {
|
|
241
|
+
current += '\\'
|
|
242
|
+
}
|
|
243
|
+
} else {
|
|
244
|
+
current += char
|
|
245
|
+
}
|
|
246
|
+
continue
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
if (char === "'") {
|
|
250
|
+
quote = "'"
|
|
251
|
+
started = true
|
|
252
|
+
continue
|
|
253
|
+
}
|
|
254
|
+
if (char === '"') {
|
|
255
|
+
quote = '"'
|
|
256
|
+
started = true
|
|
257
|
+
continue
|
|
258
|
+
}
|
|
259
|
+
if (char === '$' && (text[index + 1] === "'" || text[index + 1] === '"')) {
|
|
260
|
+
if (text[index + 1] === "'") ansiC = true
|
|
261
|
+
else quote = '"'
|
|
262
|
+
started = true
|
|
263
|
+
index += 1
|
|
264
|
+
continue
|
|
265
|
+
}
|
|
266
|
+
if (char === '\\') {
|
|
267
|
+
const next = text[index + 1]
|
|
268
|
+
if (next === undefined) {
|
|
269
|
+
current += '\\'
|
|
270
|
+
started = true
|
|
271
|
+
continue
|
|
272
|
+
}
|
|
273
|
+
if (next === ' ' || next === '\t' || next === '"' || next === "'" || next === '\\') {
|
|
274
|
+
current += next
|
|
275
|
+
index += 1
|
|
276
|
+
started = true
|
|
277
|
+
continue
|
|
278
|
+
}
|
|
279
|
+
// Windows paths keep their literal backslash.
|
|
280
|
+
current += '\\'
|
|
281
|
+
started = true
|
|
282
|
+
continue
|
|
283
|
+
}
|
|
284
|
+
if (char === '^') {
|
|
285
|
+
const next = text[index + 1]
|
|
286
|
+
if (next !== undefined && !isWhitespace(next)) {
|
|
287
|
+
current += next
|
|
288
|
+
index += 1
|
|
289
|
+
started = true
|
|
290
|
+
continue
|
|
291
|
+
}
|
|
292
|
+
current += '^'
|
|
293
|
+
started = true
|
|
294
|
+
continue
|
|
295
|
+
}
|
|
296
|
+
if (isWhitespace(char)) {
|
|
297
|
+
flush()
|
|
298
|
+
continue
|
|
299
|
+
}
|
|
300
|
+
current += char
|
|
301
|
+
started = true
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
if (quote !== null || ansiC) return { ok: false, code: 'unterminated-quote' }
|
|
305
|
+
flush()
|
|
306
|
+
return { ok: true, tokens }
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function baseName(token: string): string {
|
|
310
|
+
return token.replace(/^.*[\\/]/, '').toLowerCase()
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function isCurlExecutable(token: string): boolean {
|
|
314
|
+
return /^curl(?:\.exe)?$/.test(baseName(token))
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function looksLikeUrl(token: string): boolean {
|
|
318
|
+
return /^(?:[a-z][a-z0-9+.-]*:\/\/|\/\/|\/)/i.test(token) || token.includes('.') || token.includes(':')
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function shortValueOption(token: string): string | null {
|
|
322
|
+
if (!token.startsWith('-') || token.startsWith('--') || token.length < 2) return null
|
|
323
|
+
const short = token.slice(0, 2)
|
|
324
|
+
return SHORT_VALUE_OPTIONS.has(short) ? short : null
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
function isCombinedBooleanShortFlags(token: string): boolean {
|
|
328
|
+
if (!token.startsWith('-') || token.startsWith('--') || token.length <= 2) return false
|
|
329
|
+
return token
|
|
330
|
+
.slice(1)
|
|
331
|
+
.split('')
|
|
332
|
+
.every((flag) => SHORT_BOOLEAN_FLAGS.has(flag))
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
function setHeader(headers: Record<string, string>, name: string, value: string): void {
|
|
336
|
+
const trimmedName = name.trim()
|
|
337
|
+
if (trimmedName === '') return
|
|
338
|
+
const existing = Object.keys(headers).find(
|
|
339
|
+
(key) => key.toLowerCase() === trimmedName.toLowerCase()
|
|
340
|
+
)
|
|
341
|
+
if (existing === undefined) headers[trimmedName] = value.trim()
|
|
342
|
+
else headers[existing] = value.trim()
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
function hasHeader(headers: Record<string, string>, name: string): boolean {
|
|
346
|
+
return Object.keys(headers).some((key) => key.toLowerCase() === name.toLowerCase())
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
function applyHeaderLine(headers: Record<string, string>, line: string): void {
|
|
350
|
+
const colon = line.indexOf(':')
|
|
351
|
+
if (colon <= 0) return
|
|
352
|
+
setHeader(headers, line.slice(0, colon), line.slice(colon + 1))
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/** UTF-8 safe standard base64 (used for `-u user:pass`). */
|
|
356
|
+
export function toBase64(input: string): string {
|
|
357
|
+
const bytes = new TextEncoder().encode(input)
|
|
358
|
+
let output = ''
|
|
359
|
+
for (let index = 0; index < bytes.length; index += 3) {
|
|
360
|
+
const first = bytes[index] ?? 0
|
|
361
|
+
const second = bytes[index + 1]
|
|
362
|
+
const third = bytes[index + 2]
|
|
363
|
+
output += BASE64_ALPHABET[first >> 2]
|
|
364
|
+
output += BASE64_ALPHABET[((first & 0x03) << 4) | ((second ?? 0) >> 4)]
|
|
365
|
+
output += second === undefined ? '=' : BASE64_ALPHABET[((second & 0x0f) << 2) | ((third ?? 0) >> 6)]
|
|
366
|
+
output += third === undefined ? '=' : BASE64_ALPHABET[third & 0x3f]
|
|
367
|
+
}
|
|
368
|
+
return output
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* Best-effort conversion of a pasted curl command into a raw HTTP request.
|
|
373
|
+
*
|
|
374
|
+
* The returned `raw` text is what the Repeater editor should show; sending it
|
|
375
|
+
* still goes through `parseRawRequest` + `stripDerivedHeaders` + the retest
|
|
376
|
+
* proxy. No network access happens here.
|
|
377
|
+
*/
|
|
378
|
+
export function parseCurlCommand(input: string): CurlParseResult {
|
|
379
|
+
const tokenized = tokenizeCurl(input)
|
|
380
|
+
if (!tokenized.ok) return { ok: false, code: 'unterminated-quote' }
|
|
381
|
+
let tokens = tokenized.tokens.filter((token) => token !== '')
|
|
382
|
+
if (tokens.length === 0) return { ok: false, code: 'empty' }
|
|
383
|
+
|
|
384
|
+
let index = 0
|
|
385
|
+
const first = tokens[0] ?? ''
|
|
386
|
+
if (isCurlExecutable(first)) {
|
|
387
|
+
index = 1
|
|
388
|
+
} else if (!first.startsWith('-') && !looksLikeUrl(first)) {
|
|
389
|
+
// e.g. `proxychains curl ...`: drop a leading wrapper command.
|
|
390
|
+
index = 1
|
|
391
|
+
}
|
|
392
|
+
tokens = tokens.slice(index)
|
|
393
|
+
index = 0
|
|
394
|
+
|
|
395
|
+
const headers: Record<string, string> = {}
|
|
396
|
+
const specialHeaders: Record<string, string> = {}
|
|
397
|
+
const dataParts: string[] = []
|
|
398
|
+
const ignored: string[] = []
|
|
399
|
+
let method = ''
|
|
400
|
+
let url = ''
|
|
401
|
+
let headOnly = false
|
|
402
|
+
let getData = false
|
|
403
|
+
let basicAuth: string | null = null
|
|
404
|
+
let cookie: string | null = null
|
|
405
|
+
let jsonBody = false
|
|
406
|
+
let endOfOptions = false
|
|
407
|
+
|
|
408
|
+
const assignPositional = (token: string): void => {
|
|
409
|
+
if (url === '') {
|
|
410
|
+
url = token
|
|
411
|
+
return
|
|
412
|
+
}
|
|
413
|
+
ignored.push(token)
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
for (; index < tokens.length; index += 1) {
|
|
417
|
+
const token = tokens[index] ?? ''
|
|
418
|
+
if (token === '') continue
|
|
419
|
+
if (endOfOptions) {
|
|
420
|
+
assignPositional(token)
|
|
421
|
+
continue
|
|
422
|
+
}
|
|
423
|
+
if (token === '--') {
|
|
424
|
+
endOfOptions = true
|
|
425
|
+
continue
|
|
426
|
+
}
|
|
427
|
+
if (!token.startsWith('-') || token === '-') {
|
|
428
|
+
assignPositional(token)
|
|
429
|
+
continue
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
let option = token
|
|
433
|
+
let inlineValue: string | null = null
|
|
434
|
+
if (token.startsWith('--')) {
|
|
435
|
+
const equals = token.indexOf('=')
|
|
436
|
+
if (equals !== -1) {
|
|
437
|
+
option = token.slice(0, equals)
|
|
438
|
+
inlineValue = token.slice(equals + 1)
|
|
439
|
+
}
|
|
440
|
+
} else {
|
|
441
|
+
const valueOption = shortValueOption(token)
|
|
442
|
+
if (valueOption !== null) {
|
|
443
|
+
option = valueOption
|
|
444
|
+
if (token.length > 2) inlineValue = token.slice(2)
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
const takeValue = (): string | null => {
|
|
449
|
+
if (inlineValue !== null) {
|
|
450
|
+
const value = inlineValue
|
|
451
|
+
inlineValue = null
|
|
452
|
+
return value
|
|
453
|
+
}
|
|
454
|
+
const next = tokens[index + 1]
|
|
455
|
+
if (next === undefined) return null
|
|
456
|
+
index += 1
|
|
457
|
+
return next
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
switch (option) {
|
|
461
|
+
case '-X':
|
|
462
|
+
case '--request': {
|
|
463
|
+
const value = takeValue()
|
|
464
|
+
if (value !== null && /^[A-Za-z]+$/.test(value.trim())) {
|
|
465
|
+
method = value.trim().toUpperCase()
|
|
466
|
+
} else if (value !== null) {
|
|
467
|
+
ignored.push(option)
|
|
468
|
+
}
|
|
469
|
+
break
|
|
470
|
+
}
|
|
471
|
+
case '-H':
|
|
472
|
+
case '--header': {
|
|
473
|
+
const value = takeValue()
|
|
474
|
+
if (value !== null) applyHeaderLine(headers, value)
|
|
475
|
+
break
|
|
476
|
+
}
|
|
477
|
+
case '-d':
|
|
478
|
+
case '--data':
|
|
479
|
+
case '--data-raw':
|
|
480
|
+
case '--data-ascii':
|
|
481
|
+
case '--data-binary': {
|
|
482
|
+
const value = takeValue()
|
|
483
|
+
if (value === null) break
|
|
484
|
+
if (value.startsWith('@')) {
|
|
485
|
+
// File-backed bodies cannot be resolved from the renderer.
|
|
486
|
+
ignored.push(option)
|
|
487
|
+
break
|
|
488
|
+
}
|
|
489
|
+
dataParts.push(value)
|
|
490
|
+
break
|
|
491
|
+
}
|
|
492
|
+
case '--json': {
|
|
493
|
+
const value = takeValue()
|
|
494
|
+
if (value === null) break
|
|
495
|
+
dataParts.push(value)
|
|
496
|
+
jsonBody = true
|
|
497
|
+
break
|
|
498
|
+
}
|
|
499
|
+
case '-b':
|
|
500
|
+
case '--cookie': {
|
|
501
|
+
const value = takeValue()
|
|
502
|
+
if (value === null) break
|
|
503
|
+
if (value.startsWith('@')) {
|
|
504
|
+
ignored.push(option)
|
|
505
|
+
break
|
|
506
|
+
}
|
|
507
|
+
cookie = cookie === null ? value : `${cookie}; ${value}`
|
|
508
|
+
break
|
|
509
|
+
}
|
|
510
|
+
case '-A':
|
|
511
|
+
case '--user-agent': {
|
|
512
|
+
const value = takeValue()
|
|
513
|
+
if (value !== null) specialHeaders['User-Agent'] = value
|
|
514
|
+
break
|
|
515
|
+
}
|
|
516
|
+
case '-e':
|
|
517
|
+
case '--referer': {
|
|
518
|
+
const value = takeValue()
|
|
519
|
+
if (value !== null) specialHeaders.Referer = value
|
|
520
|
+
break
|
|
521
|
+
}
|
|
522
|
+
case '-u':
|
|
523
|
+
case '--user': {
|
|
524
|
+
const value = takeValue()
|
|
525
|
+
if (value !== null) basicAuth = value
|
|
526
|
+
break
|
|
527
|
+
}
|
|
528
|
+
case '--url': {
|
|
529
|
+
const value = takeValue()
|
|
530
|
+
if (value !== null && value.trim() !== '') url = value.trim()
|
|
531
|
+
break
|
|
532
|
+
}
|
|
533
|
+
case '-I':
|
|
534
|
+
case '--head':
|
|
535
|
+
headOnly = true
|
|
536
|
+
break
|
|
537
|
+
case '-G':
|
|
538
|
+
case '--get':
|
|
539
|
+
// `-G` moves -d payloads into the query string.
|
|
540
|
+
getData = true
|
|
541
|
+
break
|
|
542
|
+
default: {
|
|
543
|
+
if (isCombinedBooleanShortFlags(option)) {
|
|
544
|
+
if (option.includes('I')) headOnly = true
|
|
545
|
+
break
|
|
546
|
+
}
|
|
547
|
+
if (!option.startsWith('--') && option.length === 2 && SHORT_BOOLEAN_FLAGS.has(option[1] ?? '')) {
|
|
548
|
+
break
|
|
549
|
+
}
|
|
550
|
+
if (DISCARDED_VALUE_OPTIONS.has(option)) {
|
|
551
|
+
takeValue()
|
|
552
|
+
ignored.push(option)
|
|
553
|
+
break
|
|
554
|
+
}
|
|
555
|
+
if (LONG_BOOLEAN_FLAGS.has(option)) break
|
|
556
|
+
if (option.startsWith('--')) {
|
|
557
|
+
// Unknown long option: most curl long options take a value.
|
|
558
|
+
if (inlineValue === null) {
|
|
559
|
+
const next = tokens[index + 1]
|
|
560
|
+
if (next !== undefined && !next.startsWith('-')) index += 1
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
ignored.push(option)
|
|
564
|
+
break
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
if (url.trim() === '') return { ok: false, code: 'no-url' }
|
|
570
|
+
let normalizedUrl = url.trim().replace(/\s+/g, '%20')
|
|
571
|
+
let body = dataParts.join('&')
|
|
572
|
+
if (getData && body !== '') {
|
|
573
|
+
normalizedUrl = `${normalizedUrl}${normalizedUrl.includes('?') ? '&' : '?'}${body}`
|
|
574
|
+
body = ''
|
|
575
|
+
}
|
|
576
|
+
const finalMethod =
|
|
577
|
+
method !== ''
|
|
578
|
+
? method
|
|
579
|
+
: headOnly
|
|
580
|
+
? 'HEAD'
|
|
581
|
+
: body !== ''
|
|
582
|
+
? 'POST'
|
|
583
|
+
: 'GET'
|
|
584
|
+
|
|
585
|
+
if (jsonBody && !hasHeader(headers, 'content-type')) {
|
|
586
|
+
setHeader(headers, 'Content-Type', 'application/json')
|
|
587
|
+
}
|
|
588
|
+
if (basicAuth !== null && !hasHeader(headers, 'authorization')) {
|
|
589
|
+
setHeader(headers, 'Authorization', `Basic ${toBase64(basicAuth)}`)
|
|
590
|
+
}
|
|
591
|
+
if (cookie !== null && !hasHeader(headers, 'cookie')) {
|
|
592
|
+
setHeader(headers, 'Cookie', cookie)
|
|
593
|
+
}
|
|
594
|
+
for (const [name, value] of Object.entries(specialHeaders)) {
|
|
595
|
+
if (!hasHeader(headers, name)) setHeader(headers, name, value)
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
const request: ParsedRawRequest = {
|
|
599
|
+
method: finalMethod,
|
|
600
|
+
url: normalizedUrl,
|
|
601
|
+
headers,
|
|
602
|
+
body
|
|
603
|
+
}
|
|
604
|
+
return {
|
|
605
|
+
ok: true,
|
|
606
|
+
request,
|
|
607
|
+
raw: serializeRawRequest(request),
|
|
608
|
+
ignored
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
/** POSIX-shell single-quote escaping; safe for URLs, headers and bodies. */
|
|
613
|
+
export function shellQuote(value: string): string {
|
|
614
|
+
return `'${value.replace(/'/g, `'\\''`)}'`
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
/**
|
|
618
|
+
* Build a copyable curl command equivalent to the parsed request.
|
|
619
|
+
*
|
|
620
|
+
* GET without a body stays implicit (`curl <url>`); every other method (and a
|
|
621
|
+
* GET that carries a body) gets an explicit `-X` so the command stays exact.
|
|
622
|
+
*/
|
|
623
|
+
export function buildCurlCommand(request: ParsedRawRequest): string {
|
|
624
|
+
const method = request.method.trim().toUpperCase() || 'GET'
|
|
625
|
+
const body = request.body ?? ''
|
|
626
|
+
const parts: string[] = ['curl']
|
|
627
|
+
if (method !== 'GET' || body !== '') parts.push('-X', method)
|
|
628
|
+
for (const [name, value] of Object.entries(request.headers)) {
|
|
629
|
+
if (name.trim() === '') continue
|
|
630
|
+
parts.push('-H', shellQuote(`${name}: ${value}`))
|
|
631
|
+
}
|
|
632
|
+
if (body !== '') parts.push('--data-raw', shellQuote(body))
|
|
633
|
+
parts.push(shellQuote(request.url))
|
|
634
|
+
return parts.join(' ')
|
|
635
|
+
}
|