clever-tools 3.5.2 → 3.6.1
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/README.md +8 -101
- package/bin/clever.js +41 -32
- package/package.json +3 -2
- package/src/commands/addon.js +1 -1
- package/src/commands/drain.js +6 -3
- package/src/models/drain.js +24 -2
- package/vendors/README_VENDORS.md +18 -0
- package/vendors/curlconverter-parse.js +3709 -0
|
@@ -0,0 +1,3709 @@
|
|
|
1
|
+
// The MIT License (MIT)
|
|
2
|
+
//
|
|
3
|
+
// Copyright (c) 2014-2016 Nick Carneiro
|
|
4
|
+
//
|
|
5
|
+
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
// of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
// in the Software without restriction, including without limitation the rights
|
|
8
|
+
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
// copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
// furnished to do so, subject to the following conditions:
|
|
11
|
+
//
|
|
12
|
+
// The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
// copies or substantial portions of the Software.
|
|
14
|
+
//
|
|
15
|
+
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
// SOFTWARE.
|
|
22
|
+
|
|
23
|
+
'use strict';
|
|
24
|
+
|
|
25
|
+
var Parser = require('tree-sitter');
|
|
26
|
+
var Bash = require('@curlconverter/tree-sitter-bash');
|
|
27
|
+
|
|
28
|
+
class CCError extends Error {
|
|
29
|
+
}
|
|
30
|
+
const UTF8encoder = new TextEncoder();
|
|
31
|
+
// Note: !has() will lead to type errors
|
|
32
|
+
// TODO: replace with Object.hasOwn() once Node 16 is EOL'd on 2023-09-11
|
|
33
|
+
function has(obj, prop) {
|
|
34
|
+
return Object.prototype.hasOwnProperty.call(obj, prop);
|
|
35
|
+
}
|
|
36
|
+
function isInt(s) {
|
|
37
|
+
return /^\s*[+-]?\d+$/.test(s);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Words act like strings. They're lists of characters, except some
|
|
41
|
+
// characters can be shell variables or expressions.
|
|
42
|
+
// They're implemented like this:
|
|
43
|
+
// ["foobar", {type: "variable", value: "baz", text: "$baz"}, "qux"]
|
|
44
|
+
// Except for the empty string [""], there should be no empty strings in the array.
|
|
45
|
+
// TODO: Words should keep a list of operations that happened to them
|
|
46
|
+
// like .replace() so that we can generate code that also does that operation
|
|
47
|
+
// on the contents of the environment variable or the output of the command.
|
|
48
|
+
class Word {
|
|
49
|
+
constructor(tokens) {
|
|
50
|
+
this.valueOf = Word.toString;
|
|
51
|
+
if (typeof tokens === "string") {
|
|
52
|
+
tokens = [tokens];
|
|
53
|
+
}
|
|
54
|
+
if (tokens === undefined || tokens.length === 0) {
|
|
55
|
+
tokens = [""];
|
|
56
|
+
}
|
|
57
|
+
this.tokens = [];
|
|
58
|
+
for (const t of tokens) {
|
|
59
|
+
if (typeof t === "string") {
|
|
60
|
+
if (this.tokens.length > 0 &&
|
|
61
|
+
typeof this.tokens[this.tokens.length - 1] === "string") {
|
|
62
|
+
// If we have 2+ strings in a row, merge them
|
|
63
|
+
this.tokens[this.tokens.length - 1] += t;
|
|
64
|
+
}
|
|
65
|
+
else if (t) {
|
|
66
|
+
// skip empty strings
|
|
67
|
+
this.tokens.push(t);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
this.tokens.push(t);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
if (this.tokens.length === 0) {
|
|
75
|
+
this.tokens.push("");
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
get length() {
|
|
79
|
+
let len = 0;
|
|
80
|
+
for (const t of this.tokens) {
|
|
81
|
+
if (typeof t === "string") {
|
|
82
|
+
len += t.length;
|
|
83
|
+
}
|
|
84
|
+
else {
|
|
85
|
+
len += 1;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return len;
|
|
89
|
+
}
|
|
90
|
+
*[Symbol.iterator]() {
|
|
91
|
+
for (const t of this.tokens) {
|
|
92
|
+
if (typeof t === "string") {
|
|
93
|
+
for (const c of t) {
|
|
94
|
+
yield c;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
else {
|
|
98
|
+
yield t;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
// TODO: do we need this function?
|
|
103
|
+
get(index) {
|
|
104
|
+
let i = 0;
|
|
105
|
+
for (const t of this.tokens) {
|
|
106
|
+
if (typeof t === "string") {
|
|
107
|
+
if (i + t.length > index) {
|
|
108
|
+
return t[index - i];
|
|
109
|
+
}
|
|
110
|
+
i += t.length;
|
|
111
|
+
}
|
|
112
|
+
else {
|
|
113
|
+
if (i === index) {
|
|
114
|
+
return t;
|
|
115
|
+
}
|
|
116
|
+
i += 1;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
throw new CCError("Index out of bounds");
|
|
120
|
+
}
|
|
121
|
+
charAt(index = 0) {
|
|
122
|
+
try {
|
|
123
|
+
return this.get(index);
|
|
124
|
+
}
|
|
125
|
+
catch (_a) { }
|
|
126
|
+
return "";
|
|
127
|
+
}
|
|
128
|
+
indexOf(search, start) {
|
|
129
|
+
if (start === undefined) {
|
|
130
|
+
start = 0;
|
|
131
|
+
}
|
|
132
|
+
let i = 0;
|
|
133
|
+
for (const t of this.tokens) {
|
|
134
|
+
if (typeof t === "string") {
|
|
135
|
+
if (i + t.length > start) {
|
|
136
|
+
const index = t.indexOf(search, start - i);
|
|
137
|
+
if (index !== -1) {
|
|
138
|
+
return i + index;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
i += t.length;
|
|
142
|
+
}
|
|
143
|
+
else {
|
|
144
|
+
i += 1;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
return -1;
|
|
148
|
+
}
|
|
149
|
+
// Like indexOf() but accepts a string of characters and returns the index of the first one
|
|
150
|
+
// it finds
|
|
151
|
+
indexOfFirstChar(search) {
|
|
152
|
+
let i = 0;
|
|
153
|
+
for (const t of this.tokens) {
|
|
154
|
+
if (typeof t === "string") {
|
|
155
|
+
for (const c of t) {
|
|
156
|
+
if (search.includes(c)) {
|
|
157
|
+
return i;
|
|
158
|
+
}
|
|
159
|
+
i += 1;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
else {
|
|
163
|
+
i += 1;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
return -1;
|
|
167
|
+
}
|
|
168
|
+
removeFirstChar(c) {
|
|
169
|
+
if (this.length === 0) {
|
|
170
|
+
return new Word();
|
|
171
|
+
}
|
|
172
|
+
if (this.charAt(0) === c) {
|
|
173
|
+
return this.slice(1);
|
|
174
|
+
}
|
|
175
|
+
return this.copy();
|
|
176
|
+
}
|
|
177
|
+
copy() {
|
|
178
|
+
return new Word(this.tokens);
|
|
179
|
+
}
|
|
180
|
+
slice(indexStart, indexEnd) {
|
|
181
|
+
if (indexStart === undefined) {
|
|
182
|
+
indexStart = this.length;
|
|
183
|
+
}
|
|
184
|
+
if (indexEnd === undefined) {
|
|
185
|
+
indexEnd = this.length;
|
|
186
|
+
}
|
|
187
|
+
if (indexStart >= this.length) {
|
|
188
|
+
return new Word();
|
|
189
|
+
}
|
|
190
|
+
if (indexStart < 0) {
|
|
191
|
+
indexStart = Math.max(indexStart + this.length, 0);
|
|
192
|
+
}
|
|
193
|
+
if (indexEnd < 0) {
|
|
194
|
+
indexEnd = Math.max(indexEnd + this.length, 0);
|
|
195
|
+
}
|
|
196
|
+
if (indexEnd <= indexStart) {
|
|
197
|
+
return new Word();
|
|
198
|
+
}
|
|
199
|
+
const ret = [];
|
|
200
|
+
let i = 0;
|
|
201
|
+
for (const t of this.tokens) {
|
|
202
|
+
if (typeof t === "string") {
|
|
203
|
+
if (i + t.length > indexStart) {
|
|
204
|
+
if (i < indexEnd) {
|
|
205
|
+
ret.push(t.slice(Math.max(indexStart - i, 0), indexEnd - i));
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
i += t.length;
|
|
209
|
+
}
|
|
210
|
+
else {
|
|
211
|
+
if (i >= indexStart && i < indexEnd) {
|
|
212
|
+
ret.push(t);
|
|
213
|
+
}
|
|
214
|
+
i += 1;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
return new Word(ret);
|
|
218
|
+
}
|
|
219
|
+
// TODO: check
|
|
220
|
+
includes(search, start) {
|
|
221
|
+
if (start === undefined) {
|
|
222
|
+
start = 0;
|
|
223
|
+
}
|
|
224
|
+
let i = 0;
|
|
225
|
+
for (const t of this.tokens) {
|
|
226
|
+
if (typeof t === "string") {
|
|
227
|
+
if (i + t.length > start) {
|
|
228
|
+
if (t.includes(search, start - i)) {
|
|
229
|
+
return true;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
i += t.length;
|
|
233
|
+
}
|
|
234
|
+
else {
|
|
235
|
+
i += 1;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
return false;
|
|
239
|
+
}
|
|
240
|
+
test(search) {
|
|
241
|
+
for (const t of this.tokens) {
|
|
242
|
+
if (typeof t === "string") {
|
|
243
|
+
if (search.test(t)) {
|
|
244
|
+
return true;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
return false;
|
|
249
|
+
}
|
|
250
|
+
prepend(c) {
|
|
251
|
+
const ret = this.copy();
|
|
252
|
+
if (ret.tokens.length && typeof ret.tokens[0] === "string") {
|
|
253
|
+
ret.tokens[0] = c + ret.tokens[0];
|
|
254
|
+
}
|
|
255
|
+
else {
|
|
256
|
+
ret.tokens.unshift(c);
|
|
257
|
+
}
|
|
258
|
+
return ret;
|
|
259
|
+
}
|
|
260
|
+
append(c) {
|
|
261
|
+
const ret = this.copy();
|
|
262
|
+
if (ret.tokens.length &&
|
|
263
|
+
typeof ret.tokens[ret.tokens.length - 1] === "string") {
|
|
264
|
+
ret.tokens[ret.tokens.length - 1] += c;
|
|
265
|
+
}
|
|
266
|
+
else {
|
|
267
|
+
ret.tokens.push(c);
|
|
268
|
+
}
|
|
269
|
+
return ret;
|
|
270
|
+
}
|
|
271
|
+
// Merges two Words
|
|
272
|
+
add(other) {
|
|
273
|
+
return new Word([...this.tokens, ...other.tokens]);
|
|
274
|
+
}
|
|
275
|
+
// Returns the first match, searches each string independently
|
|
276
|
+
// TODO: improve this
|
|
277
|
+
match(regex) {
|
|
278
|
+
for (const t of this.tokens) {
|
|
279
|
+
if (typeof t === "string") {
|
|
280
|
+
const match = t.match(regex);
|
|
281
|
+
if (match) {
|
|
282
|
+
return match;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
return null;
|
|
287
|
+
}
|
|
288
|
+
search(regex) {
|
|
289
|
+
let offset = 0;
|
|
290
|
+
for (const t of this.tokens) {
|
|
291
|
+
if (typeof t === "string") {
|
|
292
|
+
const match = t.search(regex);
|
|
293
|
+
if (match !== -1) {
|
|
294
|
+
return offset + match;
|
|
295
|
+
}
|
|
296
|
+
offset += t.length;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
return -1;
|
|
300
|
+
}
|
|
301
|
+
// .replace() is called per-string, so it won't work through shell variables
|
|
302
|
+
replace(search, replacement) {
|
|
303
|
+
const ret = [];
|
|
304
|
+
for (const t of this.tokens) {
|
|
305
|
+
if (typeof t === "string") {
|
|
306
|
+
ret.push(t.replace(search, replacement));
|
|
307
|
+
}
|
|
308
|
+
else {
|
|
309
|
+
ret.push(t);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
return new Word(ret);
|
|
313
|
+
}
|
|
314
|
+
// splits correctly, not like String.split()
|
|
315
|
+
// The last entry can contain the separator if limit entries has been reached
|
|
316
|
+
split(separator, limit) {
|
|
317
|
+
const ret = [];
|
|
318
|
+
let i = 0;
|
|
319
|
+
let start = 0;
|
|
320
|
+
while (i < this.length) {
|
|
321
|
+
let match = true;
|
|
322
|
+
for (let j = 0; j < separator.length; j++) {
|
|
323
|
+
if (this.get(i + j) !== separator.charAt(j)) {
|
|
324
|
+
match = false;
|
|
325
|
+
break;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
if (match) {
|
|
329
|
+
ret.push(this.slice(start, i));
|
|
330
|
+
i += separator.length;
|
|
331
|
+
start = i;
|
|
332
|
+
if (limit !== undefined && ret.length === limit - 1) {
|
|
333
|
+
break;
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
else {
|
|
337
|
+
i += 1;
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
if (start <= this.length) {
|
|
341
|
+
ret.push(this.slice(start));
|
|
342
|
+
}
|
|
343
|
+
return ret;
|
|
344
|
+
}
|
|
345
|
+
toLowerCase() {
|
|
346
|
+
return new Word(this.tokens.map((t) => (typeof t === "string" ? t.toLowerCase() : t)));
|
|
347
|
+
}
|
|
348
|
+
toUpperCase() {
|
|
349
|
+
return new Word(this.tokens.map((t) => (typeof t === "string" ? t.toUpperCase() : t)));
|
|
350
|
+
}
|
|
351
|
+
trimStart() {
|
|
352
|
+
const ret = [];
|
|
353
|
+
let i, t;
|
|
354
|
+
for ([i, t] of this.tokens.entries()) {
|
|
355
|
+
if (typeof t === "string") {
|
|
356
|
+
if (i === 0) {
|
|
357
|
+
t = t.trimStart();
|
|
358
|
+
}
|
|
359
|
+
if (t) {
|
|
360
|
+
ret.push(t);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
else {
|
|
364
|
+
ret.push(t);
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
if (ret.length === 0) {
|
|
368
|
+
return new Word();
|
|
369
|
+
}
|
|
370
|
+
return new Word(ret);
|
|
371
|
+
}
|
|
372
|
+
trimEnd() {
|
|
373
|
+
const ret = [];
|
|
374
|
+
let i, t;
|
|
375
|
+
for ([i, t] of this.tokens.entries()) {
|
|
376
|
+
if (typeof t === "string") {
|
|
377
|
+
if (i === this.tokens.length - 1) {
|
|
378
|
+
t = t.trimEnd();
|
|
379
|
+
}
|
|
380
|
+
if (t) {
|
|
381
|
+
ret.push(t);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
else {
|
|
385
|
+
ret.push(t);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
if (ret.length === 0) {
|
|
389
|
+
return new Word();
|
|
390
|
+
}
|
|
391
|
+
return new Word(ret);
|
|
392
|
+
}
|
|
393
|
+
trim() {
|
|
394
|
+
const ret = [];
|
|
395
|
+
let i, t;
|
|
396
|
+
for ([i, t] of this.tokens.entries()) {
|
|
397
|
+
if (typeof t === "string") {
|
|
398
|
+
if (i === 0) {
|
|
399
|
+
t = t.trimStart();
|
|
400
|
+
}
|
|
401
|
+
if (i === this.tokens.length - 1) {
|
|
402
|
+
t = t.trimEnd();
|
|
403
|
+
}
|
|
404
|
+
if (t) {
|
|
405
|
+
ret.push(t);
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
else {
|
|
409
|
+
ret.push(t);
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
if (ret.length === 0) {
|
|
413
|
+
return new Word();
|
|
414
|
+
}
|
|
415
|
+
return new Word(ret);
|
|
416
|
+
}
|
|
417
|
+
isEmpty() {
|
|
418
|
+
if (this.tokens.length === 0) {
|
|
419
|
+
return true;
|
|
420
|
+
}
|
|
421
|
+
if (this.tokens.length === 1 && typeof this.tokens[0] === "string") {
|
|
422
|
+
return this.tokens[0].length === 0;
|
|
423
|
+
}
|
|
424
|
+
return false;
|
|
425
|
+
}
|
|
426
|
+
toBool() {
|
|
427
|
+
return !this.isEmpty();
|
|
428
|
+
}
|
|
429
|
+
// Returns true if .tokens contains no variables/commands
|
|
430
|
+
isString() {
|
|
431
|
+
for (const t of this.tokens) {
|
|
432
|
+
if (typeof t !== "string") {
|
|
433
|
+
return false;
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
return true;
|
|
437
|
+
}
|
|
438
|
+
firstShellToken() {
|
|
439
|
+
for (const t of this.tokens) {
|
|
440
|
+
if (typeof t !== "string") {
|
|
441
|
+
return t;
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
return null;
|
|
445
|
+
}
|
|
446
|
+
startsWith(prefix) {
|
|
447
|
+
if (this.tokens.length === 0) {
|
|
448
|
+
return false;
|
|
449
|
+
}
|
|
450
|
+
if (typeof this.tokens[0] === "string") {
|
|
451
|
+
return this.tokens[0].startsWith(prefix);
|
|
452
|
+
}
|
|
453
|
+
return false;
|
|
454
|
+
}
|
|
455
|
+
endsWith(suffix) {
|
|
456
|
+
if (this.tokens.length === 0) {
|
|
457
|
+
return false;
|
|
458
|
+
}
|
|
459
|
+
const lastToken = this.tokens[this.tokens.length - 1];
|
|
460
|
+
if (typeof lastToken === "string") {
|
|
461
|
+
return lastToken.endsWith(suffix);
|
|
462
|
+
}
|
|
463
|
+
return false;
|
|
464
|
+
}
|
|
465
|
+
// This destroys the information about the original tokenization
|
|
466
|
+
toString() {
|
|
467
|
+
return this.tokens
|
|
468
|
+
.map((t) => (typeof t === "string" ? t : t.text))
|
|
469
|
+
.join("");
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
function eq(it, other) {
|
|
473
|
+
if (it === undefined ||
|
|
474
|
+
it === null ||
|
|
475
|
+
other === undefined ||
|
|
476
|
+
other === null) {
|
|
477
|
+
return it === other;
|
|
478
|
+
}
|
|
479
|
+
if (typeof other === "string") {
|
|
480
|
+
return (it.tokens.length === 1 &&
|
|
481
|
+
typeof it.tokens[0] === "string" &&
|
|
482
|
+
it.tokens[0] === other);
|
|
483
|
+
}
|
|
484
|
+
return (it.tokens.length === other.tokens.length &&
|
|
485
|
+
it.tokens.every((itToken, i) => {
|
|
486
|
+
const otherToken = other.tokens[i];
|
|
487
|
+
if (typeof itToken === "string") {
|
|
488
|
+
return itToken === otherToken;
|
|
489
|
+
}
|
|
490
|
+
else if (typeof otherToken !== "string") {
|
|
491
|
+
return itToken.text === otherToken.text;
|
|
492
|
+
}
|
|
493
|
+
return false;
|
|
494
|
+
}));
|
|
495
|
+
}
|
|
496
|
+
function firstShellToken(word) {
|
|
497
|
+
if (typeof word === "string") {
|
|
498
|
+
return null;
|
|
499
|
+
}
|
|
500
|
+
return word.firstShellToken();
|
|
501
|
+
}
|
|
502
|
+
function mergeWords(...words) {
|
|
503
|
+
const ret = [];
|
|
504
|
+
for (const w of words) {
|
|
505
|
+
if (w instanceof Word) {
|
|
506
|
+
ret.push(...w.tokens);
|
|
507
|
+
}
|
|
508
|
+
else {
|
|
509
|
+
ret.push(w);
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
return new Word(ret);
|
|
513
|
+
}
|
|
514
|
+
function joinWords(words, joinChar) {
|
|
515
|
+
const ret = [];
|
|
516
|
+
for (const w of words) {
|
|
517
|
+
if (ret.length) {
|
|
518
|
+
ret.push(joinChar);
|
|
519
|
+
}
|
|
520
|
+
ret.push(...w.tokens);
|
|
521
|
+
}
|
|
522
|
+
return new Word(ret);
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
const parser = new Parser();
|
|
526
|
+
parser.setLanguage(Bash);
|
|
527
|
+
|
|
528
|
+
function warnf(global, warning) {
|
|
529
|
+
global.warnings.push(warning);
|
|
530
|
+
}
|
|
531
|
+
function underlineNode(node, curlCommand) {
|
|
532
|
+
// doesn't include leading whitespace
|
|
533
|
+
const command = node.tree.rootNode;
|
|
534
|
+
let startIndex = node.startIndex;
|
|
535
|
+
let endIndex = node.endIndex;
|
|
536
|
+
if (!curlCommand) {
|
|
537
|
+
curlCommand = command.text;
|
|
538
|
+
startIndex -= command.startIndex;
|
|
539
|
+
endIndex -= command.startIndex;
|
|
540
|
+
}
|
|
541
|
+
if (startIndex === endIndex) {
|
|
542
|
+
endIndex++;
|
|
543
|
+
}
|
|
544
|
+
// TODO: \r ?
|
|
545
|
+
let lineStart = startIndex;
|
|
546
|
+
if (startIndex > 0) {
|
|
547
|
+
// If it's -1 we're on the first line
|
|
548
|
+
lineStart = curlCommand.lastIndexOf("\n", startIndex - 1) + 1;
|
|
549
|
+
}
|
|
550
|
+
let underlineLength = endIndex - startIndex;
|
|
551
|
+
let lineEnd = curlCommand.indexOf("\n", startIndex);
|
|
552
|
+
if (lineEnd === -1) {
|
|
553
|
+
lineEnd = curlCommand.length;
|
|
554
|
+
}
|
|
555
|
+
else if (lineEnd < endIndex) {
|
|
556
|
+
// Add extra "^" past the end of a line to signal that the node continues
|
|
557
|
+
underlineLength = lineEnd - startIndex + 1;
|
|
558
|
+
}
|
|
559
|
+
const line = curlCommand.slice(lineStart, lineEnd);
|
|
560
|
+
const underline = " ".repeat(startIndex - lineStart) + "^".repeat(underlineLength);
|
|
561
|
+
return line + "\n" + underline;
|
|
562
|
+
}
|
|
563
|
+
function warnIfPartsIgnored(request, warnings, support) {
|
|
564
|
+
if (request.urls.length > 1 && !(support === null || support === void 0 ? void 0 : support.multipleUrls)) {
|
|
565
|
+
warnings.push([
|
|
566
|
+
"multiple-urls",
|
|
567
|
+
"found " +
|
|
568
|
+
request.urls.length +
|
|
569
|
+
" URLs, only the first one will be used: " +
|
|
570
|
+
request.urls
|
|
571
|
+
.map((u) => JSON.stringify(u.originalUrl.toString()))
|
|
572
|
+
.join(", "),
|
|
573
|
+
]);
|
|
574
|
+
}
|
|
575
|
+
if (request.dataReadsFile && !(support === null || support === void 0 ? void 0 : support.dataReadsFile)) {
|
|
576
|
+
warnings.push([
|
|
577
|
+
"unsafe-data",
|
|
578
|
+
// TODO: better wording. Could be "body:" too
|
|
579
|
+
"the generated data content is wrong, " +
|
|
580
|
+
// TODO: might not come from "@"
|
|
581
|
+
JSON.stringify("@" + request.dataReadsFile) +
|
|
582
|
+
" means read the file " +
|
|
583
|
+
JSON.stringify(request.dataReadsFile),
|
|
584
|
+
]);
|
|
585
|
+
}
|
|
586
|
+
if (request.urls[0].queryReadsFile && !(support === null || support === void 0 ? void 0 : support.queryReadsFile)) {
|
|
587
|
+
warnings.push([
|
|
588
|
+
"unsafe-query",
|
|
589
|
+
"the generated URL query string is wrong, " +
|
|
590
|
+
JSON.stringify("@" + request.urls[0].queryReadsFile) +
|
|
591
|
+
" means read the file " +
|
|
592
|
+
JSON.stringify(request.urls[0].queryReadsFile),
|
|
593
|
+
]);
|
|
594
|
+
}
|
|
595
|
+
if (request.cookieFiles && !(support === null || support === void 0 ? void 0 : support.cookieFiles)) {
|
|
596
|
+
warnings.push([
|
|
597
|
+
"cookie-files",
|
|
598
|
+
"passing a file for --cookie/-b is not supported: " +
|
|
599
|
+
request.cookieFiles.map((c) => JSON.stringify(c.toString())).join(", "),
|
|
600
|
+
]);
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
const BACKSLASHES = /\\./gs;
|
|
605
|
+
function removeBackslash(m) {
|
|
606
|
+
return m.charAt(1) === "\n" ? "" : m.charAt(1);
|
|
607
|
+
}
|
|
608
|
+
function removeBackslashes(str) {
|
|
609
|
+
return str.replace(BACKSLASHES, removeBackslash);
|
|
610
|
+
}
|
|
611
|
+
// https://www.gnu.org/software/bash/manual/bash.html#Double-Quotes
|
|
612
|
+
const DOUBLE_QUOTE_BACKSLASHES = /\\[\\$`"\n]/gs;
|
|
613
|
+
function removeDoubleQuoteBackslashes(str) {
|
|
614
|
+
return str.replace(DOUBLE_QUOTE_BACKSLASHES, removeBackslash);
|
|
615
|
+
}
|
|
616
|
+
// ANSI-C quoted strings look $'like this'.
|
|
617
|
+
// Not all shells have them but Bash does
|
|
618
|
+
// https://www.gnu.org/software/bash/manual/html_node/ANSI_002dC-Quoting.html
|
|
619
|
+
//
|
|
620
|
+
// https://git.savannah.gnu.org/cgit/bash.git/tree/lib/sh/strtrans.c
|
|
621
|
+
const ANSI_BACKSLASHES = /\\(\\|a|b|e|E|f|n|r|t|v|'|"|\?|[0-7]{1,3}|x[0-9A-Fa-f]{1,2}|u[0-9A-Fa-f]{1,4}|U[0-9A-Fa-f]{1,8}|c.)/gs;
|
|
622
|
+
function removeAnsiCBackslashes(str) {
|
|
623
|
+
function unescapeChar(m) {
|
|
624
|
+
switch (m.charAt(1)) {
|
|
625
|
+
case "\\":
|
|
626
|
+
return "\\";
|
|
627
|
+
case "a":
|
|
628
|
+
return "\x07";
|
|
629
|
+
case "b":
|
|
630
|
+
return "\b";
|
|
631
|
+
case "e":
|
|
632
|
+
case "E":
|
|
633
|
+
return "\x1B";
|
|
634
|
+
case "f":
|
|
635
|
+
return "\f";
|
|
636
|
+
case "n":
|
|
637
|
+
return "\n";
|
|
638
|
+
case "r":
|
|
639
|
+
return "\r";
|
|
640
|
+
case "t":
|
|
641
|
+
return "\t";
|
|
642
|
+
case "v":
|
|
643
|
+
return "\v";
|
|
644
|
+
case "'":
|
|
645
|
+
return "'";
|
|
646
|
+
case '"':
|
|
647
|
+
return '"';
|
|
648
|
+
case "?":
|
|
649
|
+
return "?";
|
|
650
|
+
case "c":
|
|
651
|
+
// Bash handles all characters by considering the first byte
|
|
652
|
+
// of its UTF-8 input and can produce invalid UTF-8, whereas
|
|
653
|
+
// JavaScript stores strings in UTF-16
|
|
654
|
+
if (m.codePointAt(2) > 127) {
|
|
655
|
+
throw new CCError('non-ASCII control character in ANSI-C quoted string: "\\u{' +
|
|
656
|
+
m.codePointAt(2).toString(16) +
|
|
657
|
+
'}"');
|
|
658
|
+
}
|
|
659
|
+
// If this produces a 0x00 (null) character, it will cause bash to
|
|
660
|
+
// terminate the string at that character, but we return the null
|
|
661
|
+
// character in the result.
|
|
662
|
+
return m[2] === "?"
|
|
663
|
+
? "\x7F"
|
|
664
|
+
: String.fromCodePoint(m[2].toUpperCase().codePointAt(0) & 0b00011111);
|
|
665
|
+
case "x":
|
|
666
|
+
case "u":
|
|
667
|
+
case "U":
|
|
668
|
+
// Hexadecimal character literal
|
|
669
|
+
// Unlike bash, this will error if the the code point is greater than 10FFFF
|
|
670
|
+
return String.fromCodePoint(parseInt(m.slice(2), 16));
|
|
671
|
+
case "0":
|
|
672
|
+
case "1":
|
|
673
|
+
case "2":
|
|
674
|
+
case "3":
|
|
675
|
+
case "4":
|
|
676
|
+
case "5":
|
|
677
|
+
case "6":
|
|
678
|
+
case "7":
|
|
679
|
+
// Octal character literal
|
|
680
|
+
return String.fromCodePoint(parseInt(m.slice(1), 8) % 256);
|
|
681
|
+
default:
|
|
682
|
+
// There must be a mis-match between ANSI_BACKSLASHES and the switch statement
|
|
683
|
+
throw new CCError("unhandled character in ANSI-C escape code: " + JSON.stringify(m));
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
return str.replace(ANSI_BACKSLASHES, unescapeChar);
|
|
687
|
+
}
|
|
688
|
+
function toTokens(node, curlCommand, warnings) {
|
|
689
|
+
let vals = [];
|
|
690
|
+
switch (node.type) {
|
|
691
|
+
case "word":
|
|
692
|
+
return [removeBackslashes(node.text)];
|
|
693
|
+
case "raw_string":
|
|
694
|
+
return [node.text.slice(1, -1)];
|
|
695
|
+
case "ansi_c_string":
|
|
696
|
+
return [removeAnsiCBackslashes(node.text.slice(2, -1))];
|
|
697
|
+
case "string":
|
|
698
|
+
case "translated_string": {
|
|
699
|
+
// TODO: MISSING quotes, for example
|
|
700
|
+
// curl "example.com
|
|
701
|
+
let prevEnd = node.type === "string" ? 1 : 2;
|
|
702
|
+
let res = "";
|
|
703
|
+
for (const child of node.namedChildren) {
|
|
704
|
+
res += removeDoubleQuoteBackslashes(node.text.slice(prevEnd, child.startIndex - node.startIndex));
|
|
705
|
+
// expansion, simple_expansion or command_substitution (or concat?)
|
|
706
|
+
const subVal = toTokens(child, curlCommand, warnings);
|
|
707
|
+
if (typeof subVal === "string") {
|
|
708
|
+
res += subVal;
|
|
709
|
+
}
|
|
710
|
+
else {
|
|
711
|
+
if (res) {
|
|
712
|
+
vals.push(res);
|
|
713
|
+
res = "";
|
|
714
|
+
}
|
|
715
|
+
vals = vals.concat(subVal);
|
|
716
|
+
}
|
|
717
|
+
prevEnd = child.endIndex - node.startIndex;
|
|
718
|
+
}
|
|
719
|
+
res += removeDoubleQuoteBackslashes(node.text.slice(prevEnd, -1));
|
|
720
|
+
if (res || vals.length === 0) {
|
|
721
|
+
vals.push(res);
|
|
722
|
+
}
|
|
723
|
+
return vals;
|
|
724
|
+
}
|
|
725
|
+
case "simple_expansion":
|
|
726
|
+
// TODO: handle variables downstream
|
|
727
|
+
// '$' + variable_name or special_variable_name
|
|
728
|
+
warnings.push([
|
|
729
|
+
"expansion",
|
|
730
|
+
"found environment variable\n" + underlineNode(node, curlCommand),
|
|
731
|
+
]);
|
|
732
|
+
if (node.firstNamedChild &&
|
|
733
|
+
node.firstNamedChild.type === "special_variable_name") {
|
|
734
|
+
// https://www.gnu.org/software/bash/manual/bash.html#Special-Parameters
|
|
735
|
+
// TODO: warning isn't printed
|
|
736
|
+
warnings.push([
|
|
737
|
+
"special_variable_name",
|
|
738
|
+
node.text +
|
|
739
|
+
" is a special Bash variable\n" +
|
|
740
|
+
underlineNode(node.firstNamedChild, curlCommand),
|
|
741
|
+
]);
|
|
742
|
+
}
|
|
743
|
+
return [
|
|
744
|
+
{
|
|
745
|
+
type: "variable",
|
|
746
|
+
value: node.text.slice(1),
|
|
747
|
+
text: node.text,
|
|
748
|
+
syntaxNode: node,
|
|
749
|
+
},
|
|
750
|
+
];
|
|
751
|
+
case "expansion":
|
|
752
|
+
// Expansions look ${like_this}
|
|
753
|
+
// https://www.gnu.org/software/bash/manual/bash.html#Shell-Parameter-Expansion
|
|
754
|
+
// TODO: MISSING }, for example
|
|
755
|
+
// curl example${com
|
|
756
|
+
warnings.push([
|
|
757
|
+
"expansion",
|
|
758
|
+
"found expansion expression\n" + underlineNode(node, curlCommand),
|
|
759
|
+
]);
|
|
760
|
+
// variable_name or subscript or no child
|
|
761
|
+
// TODO: handle substitutions
|
|
762
|
+
return [
|
|
763
|
+
{
|
|
764
|
+
type: "variable",
|
|
765
|
+
value: node.text.slice(2, -1),
|
|
766
|
+
text: node.text,
|
|
767
|
+
syntaxNode: node,
|
|
768
|
+
},
|
|
769
|
+
];
|
|
770
|
+
case "command_substitution":
|
|
771
|
+
// TODO: MISSING ), for example
|
|
772
|
+
// curl example$(com
|
|
773
|
+
warnings.push([
|
|
774
|
+
"expansion",
|
|
775
|
+
"found command substitution expression\n" +
|
|
776
|
+
underlineNode(node, curlCommand),
|
|
777
|
+
]);
|
|
778
|
+
return [
|
|
779
|
+
{
|
|
780
|
+
type: "command",
|
|
781
|
+
// TODO: further tokenize and pass an array of args
|
|
782
|
+
// to subprocess.run() or a command name + string args to C#
|
|
783
|
+
value: node.text.slice(node.text.startsWith("$(") ? 2 : 1, -1),
|
|
784
|
+
text: node.text,
|
|
785
|
+
syntaxNode: node,
|
|
786
|
+
},
|
|
787
|
+
];
|
|
788
|
+
case "concatenation": {
|
|
789
|
+
// item[]=1 turns into item=1 if we don't do this
|
|
790
|
+
// https://github.com/tree-sitter/tree-sitter-bash/issues/104
|
|
791
|
+
let prevEnd = 0;
|
|
792
|
+
let res = "";
|
|
793
|
+
for (const child of node.children) {
|
|
794
|
+
// TODO: removeBackslashes()?
|
|
795
|
+
// Can we get anything other than []{} characters here?
|
|
796
|
+
res += node.text.slice(prevEnd, child.startIndex - node.startIndex);
|
|
797
|
+
prevEnd = child.endIndex - node.startIndex;
|
|
798
|
+
const subVal = toTokens(child, curlCommand, warnings);
|
|
799
|
+
if (typeof subVal === "string") {
|
|
800
|
+
res += subVal;
|
|
801
|
+
}
|
|
802
|
+
else {
|
|
803
|
+
if (res) {
|
|
804
|
+
vals.push(res);
|
|
805
|
+
res = "";
|
|
806
|
+
}
|
|
807
|
+
vals = vals.concat(subVal);
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
res += node.text.slice(prevEnd);
|
|
811
|
+
if (res || vals.length === 0) {
|
|
812
|
+
vals.push(res);
|
|
813
|
+
}
|
|
814
|
+
return vals;
|
|
815
|
+
}
|
|
816
|
+
default:
|
|
817
|
+
throw new CCError("unexpected argument type " +
|
|
818
|
+
JSON.stringify(node.type) +
|
|
819
|
+
'. Must be one of "word", "string", "raw_string", "ansi_c_string", "expansion", "simple_expansion", "translated_string" or "concatenation"\n' +
|
|
820
|
+
underlineNode(node, curlCommand));
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
function toWord(node, curlCommand, warnings) {
|
|
824
|
+
return new Word(toTokens(node, curlCommand, warnings));
|
|
825
|
+
}
|
|
826
|
+
function warnAboutErrorNodes(ast, curlCommand, warnings) {
|
|
827
|
+
// TODO: get only named children?
|
|
828
|
+
const cursor = ast.walk();
|
|
829
|
+
cursor.gotoFirstChild();
|
|
830
|
+
while (cursor.gotoNextSibling()) {
|
|
831
|
+
if (cursor.nodeType === "ERROR") {
|
|
832
|
+
let currentNode = cursor.currentNode;
|
|
833
|
+
try {
|
|
834
|
+
// TreeCursor.currentNode is a property in Node but a function in the browser
|
|
835
|
+
// https://github.com/tree-sitter/tree-sitter/issues/2195
|
|
836
|
+
currentNode = cursor.currentNode();
|
|
837
|
+
}
|
|
838
|
+
catch (_a) { }
|
|
839
|
+
warnings.push([
|
|
840
|
+
"bash",
|
|
841
|
+
`Bash parsing error on line ${cursor.startPosition.row + 1}:\n` +
|
|
842
|
+
underlineNode(currentNode, curlCommand),
|
|
843
|
+
]);
|
|
844
|
+
break;
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
function warnAboutUselessBackslash(n, curlCommandLines, warnings) {
|
|
849
|
+
const lastCommandLine = curlCommandLines[n.endPosition.row];
|
|
850
|
+
const impromperBackslash = lastCommandLine.match(/\\\s+$/);
|
|
851
|
+
if (impromperBackslash &&
|
|
852
|
+
curlCommandLines.length > n.endPosition.row + 1 &&
|
|
853
|
+
impromperBackslash.index !== undefined) {
|
|
854
|
+
warnings.push([
|
|
855
|
+
"unescaped-newline",
|
|
856
|
+
"The trailling '\\' on line " +
|
|
857
|
+
(n.endPosition.row + 1) +
|
|
858
|
+
" is followed by whitespace, so it won't escape the newline after it:\n" +
|
|
859
|
+
// TODO: cut off line if it's very long?
|
|
860
|
+
lastCommandLine +
|
|
861
|
+
"\n" +
|
|
862
|
+
" ".repeat(impromperBackslash.index) +
|
|
863
|
+
"^".repeat(impromperBackslash[0].length),
|
|
864
|
+
]);
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
function extractRedirect(node, curlCommand, warnings) {
|
|
868
|
+
if (!node.childCount) {
|
|
869
|
+
throw new CCError('got empty "redirected_statement" AST node');
|
|
870
|
+
}
|
|
871
|
+
let stdin, stdinFile;
|
|
872
|
+
const [command, ...redirects] = node.namedChildren;
|
|
873
|
+
if (command.type !== "command") {
|
|
874
|
+
throw new CCError('got "redirected_statement" AST node whose first child is not a "command", got ' +
|
|
875
|
+
command.type +
|
|
876
|
+
" instead\n" +
|
|
877
|
+
underlineNode(command, curlCommand));
|
|
878
|
+
}
|
|
879
|
+
if (node.childCount < 2) {
|
|
880
|
+
throw new CCError('got "redirected_statement" AST node with only one child - no redirect');
|
|
881
|
+
}
|
|
882
|
+
if (redirects.length > 1) {
|
|
883
|
+
warnings.push([
|
|
884
|
+
"multiple-redirects",
|
|
885
|
+
// TODO: this is misleading because not all generators use the redirect
|
|
886
|
+
"found " +
|
|
887
|
+
redirects.length +
|
|
888
|
+
" redirect nodes. Only the first one will be used:\n" +
|
|
889
|
+
underlineNode(redirects[1], curlCommand),
|
|
890
|
+
]);
|
|
891
|
+
}
|
|
892
|
+
const redirect = redirects[0];
|
|
893
|
+
if (redirect.type === "file_redirect") {
|
|
894
|
+
stdinFile = toWord(redirect.namedChildren[0], curlCommand, warnings);
|
|
895
|
+
}
|
|
896
|
+
else if (redirect.type === "heredoc_redirect") {
|
|
897
|
+
// heredoc bodies are children of the parent program node
|
|
898
|
+
// https://github.com/tree-sitter/tree-sitter-bash/issues/118
|
|
899
|
+
if (redirect.namedChildCount < 1) {
|
|
900
|
+
throw new CCError('got "redirected_statement" AST node with heredoc but no heredoc start');
|
|
901
|
+
}
|
|
902
|
+
const heredocStart = redirect.namedChildren[0].text;
|
|
903
|
+
const heredocBody = node.nextNamedSibling;
|
|
904
|
+
if (!heredocBody) {
|
|
905
|
+
throw new CCError('got "redirected_statement" AST node with no heredoc body');
|
|
906
|
+
}
|
|
907
|
+
// TODO: herestrings and heredocs are different
|
|
908
|
+
if (heredocBody.type !== "heredoc_body") {
|
|
909
|
+
throw new CCError('got "redirected_statement" AST node with heredoc but no heredoc body, got ' +
|
|
910
|
+
heredocBody.type +
|
|
911
|
+
" instead");
|
|
912
|
+
}
|
|
913
|
+
// TODO: heredocs can do variable expansion and stuff
|
|
914
|
+
if (heredocStart.length) {
|
|
915
|
+
stdin = new Word(heredocBody.text.slice(0, -heredocStart.length));
|
|
916
|
+
}
|
|
917
|
+
else {
|
|
918
|
+
// this shouldn't happen
|
|
919
|
+
stdin = new Word(heredocBody.text);
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
else if (redirect.type === "herestring_redirect") {
|
|
923
|
+
if (redirect.namedChildCount < 1 || !redirect.firstNamedChild) {
|
|
924
|
+
throw new CCError('got "redirected_statement" AST node with empty herestring');
|
|
925
|
+
}
|
|
926
|
+
// TODO: this just converts bash code to text
|
|
927
|
+
stdin = new Word(redirect.firstNamedChild.text);
|
|
928
|
+
}
|
|
929
|
+
else {
|
|
930
|
+
throw new CCError('got "redirected_statement" AST node whose second child is not one of "file_redirect", "heredoc_redirect" or "herestring_redirect", got ' +
|
|
931
|
+
command.type +
|
|
932
|
+
" instead");
|
|
933
|
+
}
|
|
934
|
+
return [command, stdin, stdinFile];
|
|
935
|
+
}
|
|
936
|
+
function _findCurlInPipeline(node, curlCommand, warnings) {
|
|
937
|
+
let command, stdin, stdinFile;
|
|
938
|
+
for (const child of node.namedChildren) {
|
|
939
|
+
if (child.type === "command") {
|
|
940
|
+
const commandName = child.namedChildren[0];
|
|
941
|
+
if (commandName.type !== "command_name") {
|
|
942
|
+
throw new CCError('got "command" AST node whose first child is not a "command_name", got ' +
|
|
943
|
+
commandName.type +
|
|
944
|
+
" instead\n" +
|
|
945
|
+
underlineNode(commandName, curlCommand));
|
|
946
|
+
}
|
|
947
|
+
const commandNameWord = commandName.namedChildren[0];
|
|
948
|
+
if (commandNameWord.type !== "word") {
|
|
949
|
+
throw new CCError('got "command_name" AST node whose first child is not a "word", got ' +
|
|
950
|
+
commandNameWord.type +
|
|
951
|
+
" instead\n" +
|
|
952
|
+
underlineNode(commandNameWord, curlCommand));
|
|
953
|
+
}
|
|
954
|
+
if (commandNameWord.text === "curl") {
|
|
955
|
+
if (!command) {
|
|
956
|
+
command = child;
|
|
957
|
+
}
|
|
958
|
+
else {
|
|
959
|
+
warnings.push([
|
|
960
|
+
"multiple-curl-in-pipeline",
|
|
961
|
+
"found multiple curl commands in pipeline:\n" +
|
|
962
|
+
underlineNode(child, curlCommand),
|
|
963
|
+
]);
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
else if (child.type === "redirected_statement") {
|
|
968
|
+
const [redirCommand, redirStdin, redirStdinFile] = extractRedirect(child, curlCommand, warnings);
|
|
969
|
+
if (redirCommand.namedChildren[0].text === "curl") {
|
|
970
|
+
if (!command) {
|
|
971
|
+
[command, stdin, stdinFile] = [
|
|
972
|
+
redirCommand,
|
|
973
|
+
redirStdin,
|
|
974
|
+
redirStdinFile,
|
|
975
|
+
];
|
|
976
|
+
}
|
|
977
|
+
else {
|
|
978
|
+
warnings.push([
|
|
979
|
+
"multiple-curl-in-pipeline",
|
|
980
|
+
"found multiple curl commands in pipeline:\n" +
|
|
981
|
+
underlineNode(redirCommand, curlCommand),
|
|
982
|
+
]);
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
}
|
|
986
|
+
else if (child.type === "pipeline") {
|
|
987
|
+
// pipelines can be nested
|
|
988
|
+
// https://github.com/tree-sitter/tree-sitter-bash/issues/167
|
|
989
|
+
const [nestedCommand, nestedStdin, nestedStdinFile] = _findCurlInPipeline(child, curlCommand, warnings);
|
|
990
|
+
if (!nestedCommand) {
|
|
991
|
+
continue;
|
|
992
|
+
}
|
|
993
|
+
if (nestedCommand.namedChildren[0].text === "curl") {
|
|
994
|
+
if (!command) {
|
|
995
|
+
[command, stdin, stdinFile] = [
|
|
996
|
+
nestedCommand,
|
|
997
|
+
nestedStdin,
|
|
998
|
+
nestedStdinFile,
|
|
999
|
+
];
|
|
1000
|
+
}
|
|
1001
|
+
else {
|
|
1002
|
+
warnings.push([
|
|
1003
|
+
"multiple-curl-in-pipeline",
|
|
1004
|
+
"found multiple curl commands in pipeline:\n" +
|
|
1005
|
+
underlineNode(nestedCommand, curlCommand),
|
|
1006
|
+
]);
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
return [command, stdin, stdinFile];
|
|
1012
|
+
}
|
|
1013
|
+
// TODO: use pipeline input/output redirects,
|
|
1014
|
+
// i.e. add stdinCommand and stdout/stdoutFile/stdoutCommand
|
|
1015
|
+
function findCurlInPipeline(node, curlCommand, warnings) {
|
|
1016
|
+
const [command, stdin, stdinFile] = _findCurlInPipeline(node, curlCommand, warnings);
|
|
1017
|
+
if (!command) {
|
|
1018
|
+
throw new CCError("could not find curl command in pipeline\n" +
|
|
1019
|
+
underlineNode(node, curlCommand));
|
|
1020
|
+
}
|
|
1021
|
+
return [command, stdin, stdinFile];
|
|
1022
|
+
}
|
|
1023
|
+
// TODO: check entire AST for ERROR/MISSING nodes
|
|
1024
|
+
// TODO: get all command nodes
|
|
1025
|
+
function extractCommandNodes(ast, curlCommand, warnings) {
|
|
1026
|
+
// https://github.com/tree-sitter/tree-sitter-bash/blob/master/grammar.js
|
|
1027
|
+
// The AST must be in a nice format, i.e.
|
|
1028
|
+
// (program
|
|
1029
|
+
// (command
|
|
1030
|
+
// name: (command_name (word))
|
|
1031
|
+
// argument+: (
|
|
1032
|
+
// word |
|
|
1033
|
+
// "string" |
|
|
1034
|
+
// 'raw_string' |
|
|
1035
|
+
// $'ansi_c_string' |
|
|
1036
|
+
// $"translated_string" |
|
|
1037
|
+
// ${expansion} |
|
|
1038
|
+
// $simple_expansion |
|
|
1039
|
+
// concatenation)))
|
|
1040
|
+
// or
|
|
1041
|
+
// (program
|
|
1042
|
+
// (redirected_statement
|
|
1043
|
+
// body: (command, same as above)
|
|
1044
|
+
// redirect))
|
|
1045
|
+
// Shouldn't happen.
|
|
1046
|
+
if (ast.rootNode.type !== "program") {
|
|
1047
|
+
// TODO: better error message.
|
|
1048
|
+
throw new CCError(
|
|
1049
|
+
// TODO: expand "AST" acronym the first time it appears in an error message
|
|
1050
|
+
'expected a "program" top-level AST node, got ' +
|
|
1051
|
+
ast.rootNode.type +
|
|
1052
|
+
" instead");
|
|
1053
|
+
}
|
|
1054
|
+
if (ast.rootNode.namedChildCount < 1 || !ast.rootNode.namedChildren) {
|
|
1055
|
+
// TODO: better error message.
|
|
1056
|
+
throw new CCError('empty "program" node');
|
|
1057
|
+
}
|
|
1058
|
+
const curlCommandLines = curlCommand.split("\n");
|
|
1059
|
+
let sawComment = false;
|
|
1060
|
+
const commands = [];
|
|
1061
|
+
// Get top-level command and redirected_statement AST nodes, skipping comments
|
|
1062
|
+
for (const n of ast.rootNode.namedChildren) {
|
|
1063
|
+
switch (n.type) {
|
|
1064
|
+
case "comment":
|
|
1065
|
+
sawComment = true;
|
|
1066
|
+
continue;
|
|
1067
|
+
case "command":
|
|
1068
|
+
commands.push([n, undefined, undefined]);
|
|
1069
|
+
warnAboutUselessBackslash(n, curlCommandLines, warnings);
|
|
1070
|
+
break;
|
|
1071
|
+
case "redirected_statement":
|
|
1072
|
+
commands.push(extractRedirect(n, curlCommand, warnings));
|
|
1073
|
+
warnAboutUselessBackslash(n, curlCommandLines, warnings);
|
|
1074
|
+
break;
|
|
1075
|
+
case "pipeline":
|
|
1076
|
+
commands.push(findCurlInPipeline(n, curlCommand, warnings));
|
|
1077
|
+
warnAboutUselessBackslash(n, curlCommandLines, warnings);
|
|
1078
|
+
break;
|
|
1079
|
+
case "heredoc_body": // https://github.com/tree-sitter/tree-sitter-bash/issues/118
|
|
1080
|
+
continue;
|
|
1081
|
+
case "ERROR":
|
|
1082
|
+
throw new CCError(`Bash parsing error on line ${n.startPosition.row + 1}:\n` +
|
|
1083
|
+
underlineNode(n, curlCommand));
|
|
1084
|
+
default:
|
|
1085
|
+
// TODO: better error message.
|
|
1086
|
+
throw new CCError("found " +
|
|
1087
|
+
JSON.stringify(n.type) +
|
|
1088
|
+
' AST node, only "command", "pipeline" or "redirected_statement" are supported\n' +
|
|
1089
|
+
underlineNode(n, curlCommand));
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1092
|
+
if (!commands.length) {
|
|
1093
|
+
// NOTE: if you add more node types in the `for` loop above, this error needs to be updated.
|
|
1094
|
+
// We would probably need to keep track of the node types we've seen.
|
|
1095
|
+
throw new CCError('expected a "command" or "redirected_statement" AST node' +
|
|
1096
|
+
(sawComment ? ', only found "comment" nodes' : ""));
|
|
1097
|
+
}
|
|
1098
|
+
return commands;
|
|
1099
|
+
}
|
|
1100
|
+
function toNameAndArgv(command, curlCommand, warnings) {
|
|
1101
|
+
if (command.childCount < 1) {
|
|
1102
|
+
// TODO: better error message.
|
|
1103
|
+
throw new CCError('empty "command" node\n' + underlineNode(command, curlCommand));
|
|
1104
|
+
}
|
|
1105
|
+
// TODO: add childrenForFieldName to tree-sitter node/web bindings
|
|
1106
|
+
let commandNameLoc = 0;
|
|
1107
|
+
// TODO: parse variable_assignment nodes and replace variables in the command
|
|
1108
|
+
// TODO: support file_redirect
|
|
1109
|
+
for (const n of command.namedChildren) {
|
|
1110
|
+
if (n.type === "variable_assignment" || n.type === "file_redirect") {
|
|
1111
|
+
warnings.push([
|
|
1112
|
+
"command-preamble",
|
|
1113
|
+
"skipping " +
|
|
1114
|
+
JSON.stringify(n.type) +
|
|
1115
|
+
" expression\n" +
|
|
1116
|
+
underlineNode(n, curlCommand),
|
|
1117
|
+
]);
|
|
1118
|
+
commandNameLoc += 1;
|
|
1119
|
+
}
|
|
1120
|
+
else {
|
|
1121
|
+
// it must be the command name
|
|
1122
|
+
if (n.type !== "command_name") {
|
|
1123
|
+
throw new CCError('expected "command_name", "variable_assignment" or "file_redirect" AST node, found ' +
|
|
1124
|
+
n.type +
|
|
1125
|
+
" instead\n" +
|
|
1126
|
+
underlineNode(n, curlCommand));
|
|
1127
|
+
}
|
|
1128
|
+
break;
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1131
|
+
const [name, ...args] = command.namedChildren.slice(commandNameLoc);
|
|
1132
|
+
// Shouldn't happen
|
|
1133
|
+
if (name === undefined) {
|
|
1134
|
+
throw new CCError('found "command" AST node with no "command_name" child\n' +
|
|
1135
|
+
underlineNode(command, curlCommand));
|
|
1136
|
+
}
|
|
1137
|
+
return [name, args];
|
|
1138
|
+
}
|
|
1139
|
+
// Checks that name is "curl"
|
|
1140
|
+
function nameToWord(name, curlCommand, warnings) {
|
|
1141
|
+
if (name.childCount < 1 || !name.firstChild) {
|
|
1142
|
+
throw new CCError('found empty "command_name" AST node\n' + underlineNode(name, curlCommand));
|
|
1143
|
+
}
|
|
1144
|
+
else if (name.childCount > 1) {
|
|
1145
|
+
warnings.push([
|
|
1146
|
+
"extra-command_name-children",
|
|
1147
|
+
'expected "command_name" node to only have one child but it has ' +
|
|
1148
|
+
name.childCount,
|
|
1149
|
+
]);
|
|
1150
|
+
}
|
|
1151
|
+
const nameNode = name.firstChild;
|
|
1152
|
+
const nameWord = toWord(nameNode, curlCommand, warnings);
|
|
1153
|
+
const nameWordStr = nameWord.toString();
|
|
1154
|
+
const cmdNameShellToken = firstShellToken(nameWord);
|
|
1155
|
+
if (cmdNameShellToken) {
|
|
1156
|
+
// The most common reason for the command name to contain an expression
|
|
1157
|
+
// is probably users accidentally copying a $ from the shell prompt
|
|
1158
|
+
// without a space after it
|
|
1159
|
+
if (nameWordStr !== "$curl") {
|
|
1160
|
+
// TODO: or just assume it evaluates to "curl"?
|
|
1161
|
+
throw new CCError("expected command name to be a simple value but found a " +
|
|
1162
|
+
cmdNameShellToken.type +
|
|
1163
|
+
"\n" +
|
|
1164
|
+
underlineNode(cmdNameShellToken.syntaxNode, curlCommand));
|
|
1165
|
+
}
|
|
1166
|
+
}
|
|
1167
|
+
else if (nameWordStr.trim() !== "curl") {
|
|
1168
|
+
const c = nameWordStr.trim();
|
|
1169
|
+
if (!c) {
|
|
1170
|
+
throw new CCError("found command without a command_name\n" +
|
|
1171
|
+
underlineNode(nameNode, curlCommand));
|
|
1172
|
+
}
|
|
1173
|
+
throw new CCError('command should begin with "curl" but instead begins with ' +
|
|
1174
|
+
JSON.stringify(clip(c)) +
|
|
1175
|
+
"\n" +
|
|
1176
|
+
underlineNode(nameNode, curlCommand));
|
|
1177
|
+
}
|
|
1178
|
+
return nameWord;
|
|
1179
|
+
}
|
|
1180
|
+
function tokenize(curlCommand, warnings = []) {
|
|
1181
|
+
const ast = parser.parse(curlCommand);
|
|
1182
|
+
warnAboutErrorNodes(ast, curlCommand, warnings);
|
|
1183
|
+
// TODO: pass syntax nodes for each token downstream and use it to
|
|
1184
|
+
// highlight the problematic parts in warnings/errors so that it's clear
|
|
1185
|
+
// which command a warning/error is for
|
|
1186
|
+
const commandNodes = extractCommandNodes(ast, curlCommand, warnings);
|
|
1187
|
+
const commands = [];
|
|
1188
|
+
for (const [command, stdin, stdinFile] of commandNodes) {
|
|
1189
|
+
const [name, argv] = toNameAndArgv(command, curlCommand, warnings);
|
|
1190
|
+
commands.push([
|
|
1191
|
+
[
|
|
1192
|
+
nameToWord(name, curlCommand, warnings),
|
|
1193
|
+
...argv.map((arg) => toWord(arg, curlCommand, warnings)),
|
|
1194
|
+
],
|
|
1195
|
+
stdin,
|
|
1196
|
+
stdinFile,
|
|
1197
|
+
]);
|
|
1198
|
+
}
|
|
1199
|
+
return commands;
|
|
1200
|
+
}
|
|
1201
|
+
|
|
1202
|
+
const CURLAUTH_BASIC = 1 << 0;
|
|
1203
|
+
const CURLAUTH_DIGEST = 1 << 1;
|
|
1204
|
+
const CURLAUTH_NEGOTIATE = 1 << 2;
|
|
1205
|
+
const CURLAUTH_NTLM = 1 << 3;
|
|
1206
|
+
const CURLAUTH_DIGEST_IE = 1 << 4;
|
|
1207
|
+
const CURLAUTH_NTLM_WB = 1 << 5;
|
|
1208
|
+
const CURLAUTH_BEARER = 1 << 6;
|
|
1209
|
+
const CURLAUTH_AWS_SIGV4 = 1 << 7;
|
|
1210
|
+
const CURLAUTH_ANY = ~CURLAUTH_DIGEST_IE;
|
|
1211
|
+
// This is this function
|
|
1212
|
+
// https://github.com/curl/curl/blob/curl-7_86_0/lib/http.c#L455
|
|
1213
|
+
// which is not the correct function, since it works on the response.
|
|
1214
|
+
//
|
|
1215
|
+
// Curl also filters out auth schemes it doesn't support,
|
|
1216
|
+
// https://github.com/curl/curl/blob/curl-7_86_0/lib/setopt.c#L970
|
|
1217
|
+
// but we "support" all of them, so we don't need to do that.
|
|
1218
|
+
function pickAuth(mask) {
|
|
1219
|
+
if (mask === CURLAUTH_ANY) {
|
|
1220
|
+
return "basic";
|
|
1221
|
+
}
|
|
1222
|
+
const auths = [
|
|
1223
|
+
[CURLAUTH_NEGOTIATE, "negotiate"],
|
|
1224
|
+
[CURLAUTH_BEARER, "bearer"],
|
|
1225
|
+
[CURLAUTH_DIGEST, "digest"],
|
|
1226
|
+
[CURLAUTH_NTLM, "ntlm"],
|
|
1227
|
+
[CURLAUTH_NTLM_WB, "ntlm-wb"],
|
|
1228
|
+
[CURLAUTH_BASIC, "basic"],
|
|
1229
|
+
// This check happens outside this function because we obviously
|
|
1230
|
+
// don't need to to specify --no-basic to use aws-sigv4
|
|
1231
|
+
// https://github.com/curl/curl/blob/curl-7_86_0/lib/setopt.c#L678-L679
|
|
1232
|
+
[CURLAUTH_AWS_SIGV4, "aws-sigv4"],
|
|
1233
|
+
];
|
|
1234
|
+
for (const [auth, authName] of auths) {
|
|
1235
|
+
if (mask & auth) {
|
|
1236
|
+
return authName;
|
|
1237
|
+
}
|
|
1238
|
+
}
|
|
1239
|
+
return "none";
|
|
1240
|
+
}
|
|
1241
|
+
|
|
1242
|
+
// prettier-ignore
|
|
1243
|
+
const curlLongOpts = {
|
|
1244
|
+
// BEGIN EXTRACTED OPTIONS
|
|
1245
|
+
"url": { type: "string", name: "url" },
|
|
1246
|
+
"dns-ipv4-addr": { type: "string", name: "dns-ipv4-addr" },
|
|
1247
|
+
"dns-ipv6-addr": { type: "string", name: "dns-ipv6-addr" },
|
|
1248
|
+
"random-file": { type: "string", name: "random-file" },
|
|
1249
|
+
"egd-file": { type: "string", name: "egd-file" },
|
|
1250
|
+
"oauth2-bearer": { type: "string", name: "oauth2-bearer" },
|
|
1251
|
+
"connect-timeout": { type: "string", name: "connect-timeout" },
|
|
1252
|
+
"doh-url": { type: "string", name: "doh-url" },
|
|
1253
|
+
"ciphers": { type: "string", name: "ciphers" },
|
|
1254
|
+
"dns-interface": { type: "string", name: "dns-interface" },
|
|
1255
|
+
"disable-epsv": { type: "bool", name: "disable-epsv" },
|
|
1256
|
+
"no-disable-epsv": { type: "bool", name: "disable-epsv", expand: false },
|
|
1257
|
+
"disallow-username-in-url": { type: "bool", name: "disallow-username-in-url" },
|
|
1258
|
+
"no-disallow-username-in-url": { type: "bool", name: "disallow-username-in-url", expand: false },
|
|
1259
|
+
"epsv": { type: "bool", name: "epsv" },
|
|
1260
|
+
"no-epsv": { type: "bool", name: "epsv", expand: false },
|
|
1261
|
+
"dns-servers": { type: "string", name: "dns-servers" },
|
|
1262
|
+
"trace": { type: "string", name: "trace" },
|
|
1263
|
+
"npn": { type: "bool", name: "npn" },
|
|
1264
|
+
"no-npn": { type: "bool", name: "npn", expand: false },
|
|
1265
|
+
"trace-ascii": { type: "string", name: "trace-ascii" },
|
|
1266
|
+
"alpn": { type: "bool", name: "alpn" },
|
|
1267
|
+
"no-alpn": { type: "bool", name: "alpn", expand: false },
|
|
1268
|
+
"limit-rate": { type: "string", name: "limit-rate" },
|
|
1269
|
+
"rate": { type: "string", name: "rate" },
|
|
1270
|
+
"compressed": { type: "bool", name: "compressed" },
|
|
1271
|
+
"no-compressed": { type: "bool", name: "compressed", expand: false },
|
|
1272
|
+
"tr-encoding": { type: "bool", name: "tr-encoding" },
|
|
1273
|
+
"no-tr-encoding": { type: "bool", name: "tr-encoding", expand: false },
|
|
1274
|
+
"digest": { type: "bool", name: "digest" },
|
|
1275
|
+
"no-digest": { type: "bool", name: "digest", expand: false },
|
|
1276
|
+
"negotiate": { type: "bool", name: "negotiate" },
|
|
1277
|
+
"no-negotiate": { type: "bool", name: "negotiate", expand: false },
|
|
1278
|
+
"ntlm": { type: "bool", name: "ntlm" },
|
|
1279
|
+
"no-ntlm": { type: "bool", name: "ntlm", expand: false },
|
|
1280
|
+
"ntlm-wb": { type: "bool", name: "ntlm-wb" },
|
|
1281
|
+
"no-ntlm-wb": { type: "bool", name: "ntlm-wb", expand: false },
|
|
1282
|
+
"basic": { type: "bool", name: "basic" },
|
|
1283
|
+
"no-basic": { type: "bool", name: "basic", expand: false },
|
|
1284
|
+
"anyauth": { type: "bool", name: "anyauth" },
|
|
1285
|
+
"no-anyauth": { type: "bool", name: "anyauth", expand: false },
|
|
1286
|
+
"wdebug": { type: "bool", name: "wdebug" },
|
|
1287
|
+
"no-wdebug": { type: "bool", name: "wdebug", expand: false },
|
|
1288
|
+
"ftp-create-dirs": { type: "bool", name: "ftp-create-dirs" },
|
|
1289
|
+
"no-ftp-create-dirs": { type: "bool", name: "ftp-create-dirs", expand: false },
|
|
1290
|
+
"create-dirs": { type: "bool", name: "create-dirs" },
|
|
1291
|
+
"no-create-dirs": { type: "bool", name: "create-dirs", expand: false },
|
|
1292
|
+
"create-file-mode": { type: "string", name: "create-file-mode" },
|
|
1293
|
+
"max-redirs": { type: "string", name: "max-redirs" },
|
|
1294
|
+
"proxy-ntlm": { type: "bool", name: "proxy-ntlm" },
|
|
1295
|
+
"no-proxy-ntlm": { type: "bool", name: "proxy-ntlm", expand: false },
|
|
1296
|
+
"crlf": { type: "bool", name: "crlf" },
|
|
1297
|
+
"no-crlf": { type: "bool", name: "crlf", expand: false },
|
|
1298
|
+
"stderr": { type: "string", name: "stderr" },
|
|
1299
|
+
"aws-sigv4": { type: "string", name: "aws-sigv4" },
|
|
1300
|
+
"interface": { type: "string", name: "interface" },
|
|
1301
|
+
"krb": { type: "string", name: "krb" },
|
|
1302
|
+
"krb4": { type: "string", name: "krb" },
|
|
1303
|
+
"haproxy-protocol": { type: "bool", name: "haproxy-protocol" },
|
|
1304
|
+
"no-haproxy-protocol": { type: "bool", name: "haproxy-protocol", expand: false },
|
|
1305
|
+
"haproxy-clientip": { type: "string", name: "haproxy-clientip" },
|
|
1306
|
+
"max-filesize": { type: "string", name: "max-filesize" },
|
|
1307
|
+
"disable-eprt": { type: "bool", name: "disable-eprt" },
|
|
1308
|
+
"no-disable-eprt": { type: "bool", name: "disable-eprt", expand: false },
|
|
1309
|
+
"eprt": { type: "bool", name: "eprt" },
|
|
1310
|
+
"no-eprt": { type: "bool", name: "eprt", expand: false },
|
|
1311
|
+
"xattr": { type: "bool", name: "xattr" },
|
|
1312
|
+
"no-xattr": { type: "bool", name: "xattr", expand: false },
|
|
1313
|
+
"ftp-ssl": { type: "bool", name: "ssl" },
|
|
1314
|
+
"no-ftp-ssl": { type: "bool", name: "ssl", expand: false },
|
|
1315
|
+
"ssl": { type: "bool", name: "ssl" },
|
|
1316
|
+
"no-ssl": { type: "bool", name: "ssl", expand: false },
|
|
1317
|
+
"ftp-pasv": { type: "bool", name: "ftp-pasv" },
|
|
1318
|
+
"no-ftp-pasv": { type: "bool", name: "ftp-pasv", expand: false },
|
|
1319
|
+
"socks5": { type: "string", name: "socks5" },
|
|
1320
|
+
"tcp-nodelay": { type: "bool", name: "tcp-nodelay" },
|
|
1321
|
+
"no-tcp-nodelay": { type: "bool", name: "tcp-nodelay", expand: false },
|
|
1322
|
+
"proxy-digest": { type: "bool", name: "proxy-digest" },
|
|
1323
|
+
"no-proxy-digest": { type: "bool", name: "proxy-digest", expand: false },
|
|
1324
|
+
"proxy-basic": { type: "bool", name: "proxy-basic" },
|
|
1325
|
+
"no-proxy-basic": { type: "bool", name: "proxy-basic", expand: false },
|
|
1326
|
+
"retry": { type: "string", name: "retry" },
|
|
1327
|
+
"retry-connrefused": { type: "bool", name: "retry-connrefused" },
|
|
1328
|
+
"no-retry-connrefused": { type: "bool", name: "retry-connrefused", expand: false },
|
|
1329
|
+
"retry-delay": { type: "string", name: "retry-delay" },
|
|
1330
|
+
"retry-max-time": { type: "string", name: "retry-max-time" },
|
|
1331
|
+
"proxy-negotiate": { type: "bool", name: "proxy-negotiate" },
|
|
1332
|
+
"no-proxy-negotiate": { type: "bool", name: "proxy-negotiate", expand: false },
|
|
1333
|
+
"form-escape": { type: "bool", name: "form-escape" },
|
|
1334
|
+
"no-form-escape": { type: "bool", name: "form-escape", expand: false },
|
|
1335
|
+
"ftp-account": { type: "string", name: "ftp-account" },
|
|
1336
|
+
"proxy-anyauth": { type: "bool", name: "proxy-anyauth" },
|
|
1337
|
+
"no-proxy-anyauth": { type: "bool", name: "proxy-anyauth", expand: false },
|
|
1338
|
+
"trace-time": { type: "bool", name: "trace-time" },
|
|
1339
|
+
"no-trace-time": { type: "bool", name: "trace-time", expand: false },
|
|
1340
|
+
"ignore-content-length": { type: "bool", name: "ignore-content-length" },
|
|
1341
|
+
"no-ignore-content-length": { type: "bool", name: "ignore-content-length", expand: false },
|
|
1342
|
+
"ftp-skip-pasv-ip": { type: "bool", name: "ftp-skip-pasv-ip" },
|
|
1343
|
+
"no-ftp-skip-pasv-ip": { type: "bool", name: "ftp-skip-pasv-ip", expand: false },
|
|
1344
|
+
"ftp-method": { type: "string", name: "ftp-method" },
|
|
1345
|
+
"local-port": { type: "string", name: "local-port" },
|
|
1346
|
+
"socks4": { type: "string", name: "socks4" },
|
|
1347
|
+
"socks4a": { type: "string", name: "socks4a" },
|
|
1348
|
+
"ftp-alternative-to-user": { type: "string", name: "ftp-alternative-to-user" },
|
|
1349
|
+
"ftp-ssl-reqd": { type: "bool", name: "ssl-reqd" },
|
|
1350
|
+
"no-ftp-ssl-reqd": { type: "bool", name: "ssl-reqd", expand: false },
|
|
1351
|
+
"ssl-reqd": { type: "bool", name: "ssl-reqd" },
|
|
1352
|
+
"no-ssl-reqd": { type: "bool", name: "ssl-reqd", expand: false },
|
|
1353
|
+
"sessionid": { type: "bool", name: "sessionid" },
|
|
1354
|
+
"no-sessionid": { type: "bool", name: "sessionid", expand: false },
|
|
1355
|
+
"ftp-ssl-control": { type: "bool", name: "ftp-ssl-control" },
|
|
1356
|
+
"no-ftp-ssl-control": { type: "bool", name: "ftp-ssl-control", expand: false },
|
|
1357
|
+
"ftp-ssl-ccc": { type: "bool", name: "ftp-ssl-ccc" },
|
|
1358
|
+
"no-ftp-ssl-ccc": { type: "bool", name: "ftp-ssl-ccc", expand: false },
|
|
1359
|
+
"ftp-ssl-ccc-mode": { type: "string", name: "ftp-ssl-ccc-mode" },
|
|
1360
|
+
"libcurl": { type: "string", name: "libcurl" },
|
|
1361
|
+
"raw": { type: "bool", name: "raw" },
|
|
1362
|
+
"no-raw": { type: "bool", name: "raw", expand: false },
|
|
1363
|
+
"post301": { type: "bool", name: "post301" },
|
|
1364
|
+
"no-post301": { type: "bool", name: "post301", expand: false },
|
|
1365
|
+
"keepalive": { type: "bool", name: "keepalive" },
|
|
1366
|
+
"no-keepalive": { type: "bool", name: "keepalive", expand: false },
|
|
1367
|
+
"socks5-hostname": { type: "string", name: "socks5-hostname" },
|
|
1368
|
+
"keepalive-time": { type: "string", name: "keepalive-time" },
|
|
1369
|
+
"post302": { type: "bool", name: "post302" },
|
|
1370
|
+
"no-post302": { type: "bool", name: "post302", expand: false },
|
|
1371
|
+
"noproxy": { type: "string", name: "noproxy" },
|
|
1372
|
+
"socks5-gssapi-nec": { type: "bool", name: "socks5-gssapi-nec" },
|
|
1373
|
+
"no-socks5-gssapi-nec": { type: "bool", name: "socks5-gssapi-nec", expand: false },
|
|
1374
|
+
"proxy1.0": { type: "string", name: "proxy1.0" },
|
|
1375
|
+
"tftp-blksize": { type: "string", name: "tftp-blksize" },
|
|
1376
|
+
"mail-from": { type: "string", name: "mail-from" },
|
|
1377
|
+
"mail-rcpt": { type: "string", name: "mail-rcpt" },
|
|
1378
|
+
"ftp-pret": { type: "bool", name: "ftp-pret" },
|
|
1379
|
+
"no-ftp-pret": { type: "bool", name: "ftp-pret", expand: false },
|
|
1380
|
+
"proto": { type: "string", name: "proto" },
|
|
1381
|
+
"proto-redir": { type: "string", name: "proto-redir" },
|
|
1382
|
+
"resolve": { type: "string", name: "resolve" },
|
|
1383
|
+
"delegation": { type: "string", name: "delegation" },
|
|
1384
|
+
"mail-auth": { type: "string", name: "mail-auth" },
|
|
1385
|
+
"post303": { type: "bool", name: "post303" },
|
|
1386
|
+
"no-post303": { type: "bool", name: "post303", expand: false },
|
|
1387
|
+
"metalink": { type: "bool", name: "metalink" },
|
|
1388
|
+
"no-metalink": { type: "bool", name: "metalink", expand: false },
|
|
1389
|
+
"sasl-authzid": { type: "string", name: "sasl-authzid" },
|
|
1390
|
+
"sasl-ir": { type: "bool", name: "sasl-ir" },
|
|
1391
|
+
"no-sasl-ir": { type: "bool", name: "sasl-ir", expand: false },
|
|
1392
|
+
"test-event": { type: "bool", name: "test-event" },
|
|
1393
|
+
"no-test-event": { type: "bool", name: "test-event", expand: false },
|
|
1394
|
+
"unix-socket": { type: "string", name: "unix-socket" },
|
|
1395
|
+
"path-as-is": { type: "bool", name: "path-as-is" },
|
|
1396
|
+
"no-path-as-is": { type: "bool", name: "path-as-is", expand: false },
|
|
1397
|
+
"socks5-gssapi-service": { type: "string", name: "proxy-service-name" },
|
|
1398
|
+
"proxy-service-name": { type: "string", name: "proxy-service-name" },
|
|
1399
|
+
"service-name": { type: "string", name: "service-name" },
|
|
1400
|
+
"proto-default": { type: "string", name: "proto-default" },
|
|
1401
|
+
"expect100-timeout": { type: "string", name: "expect100-timeout" },
|
|
1402
|
+
"tftp-no-options": { type: "bool", name: "tftp-no-options" },
|
|
1403
|
+
"no-tftp-no-options": { type: "bool", name: "tftp-no-options", expand: false },
|
|
1404
|
+
"connect-to": { type: "string", name: "connect-to" },
|
|
1405
|
+
"abstract-unix-socket": { type: "string", name: "abstract-unix-socket" },
|
|
1406
|
+
"tls-max": { type: "string", name: "tls-max" },
|
|
1407
|
+
"suppress-connect-headers": { type: "bool", name: "suppress-connect-headers" },
|
|
1408
|
+
"no-suppress-connect-headers": { type: "bool", name: "suppress-connect-headers", expand: false },
|
|
1409
|
+
"compressed-ssh": { type: "bool", name: "compressed-ssh" },
|
|
1410
|
+
"no-compressed-ssh": { type: "bool", name: "compressed-ssh", expand: false },
|
|
1411
|
+
"happy-eyeballs-timeout-ms": { type: "string", name: "happy-eyeballs-timeout-ms" },
|
|
1412
|
+
"retry-all-errors": { type: "bool", name: "retry-all-errors" },
|
|
1413
|
+
"no-retry-all-errors": { type: "bool", name: "retry-all-errors", expand: false },
|
|
1414
|
+
"trace-ids": { type: "bool", name: "trace-ids" },
|
|
1415
|
+
"no-trace-ids": { type: "bool", name: "trace-ids", expand: false },
|
|
1416
|
+
"http1.0": { type: "bool", name: "http1.0" },
|
|
1417
|
+
"http1.1": { type: "bool", name: "http1.1" },
|
|
1418
|
+
"http2": { type: "bool", name: "http2" },
|
|
1419
|
+
"http2-prior-knowledge": { type: "bool", name: "http2-prior-knowledge" },
|
|
1420
|
+
"http3": { type: "bool", name: "http3" },
|
|
1421
|
+
"http3-only": { type: "bool", name: "http3-only" },
|
|
1422
|
+
"http0.9": { type: "bool", name: "http0.9" },
|
|
1423
|
+
"no-http0.9": { type: "bool", name: "http0.9", expand: false },
|
|
1424
|
+
"proxy-http2": { type: "bool", name: "proxy-http2" },
|
|
1425
|
+
"no-proxy-http2": { type: "bool", name: "proxy-http2", expand: false },
|
|
1426
|
+
"tlsv1": { type: "bool", name: "tlsv1" },
|
|
1427
|
+
"tlsv1.0": { type: "bool", name: "tlsv1.0" },
|
|
1428
|
+
"tlsv1.1": { type: "bool", name: "tlsv1.1" },
|
|
1429
|
+
"tlsv1.2": { type: "bool", name: "tlsv1.2" },
|
|
1430
|
+
"tlsv1.3": { type: "bool", name: "tlsv1.3" },
|
|
1431
|
+
"tls13-ciphers": { type: "string", name: "tls13-ciphers" },
|
|
1432
|
+
"proxy-tls13-ciphers": { type: "string", name: "proxy-tls13-ciphers" },
|
|
1433
|
+
"sslv2": { type: "bool", name: "sslv2" },
|
|
1434
|
+
"sslv3": { type: "bool", name: "sslv3" },
|
|
1435
|
+
"ipv4": { type: "bool", name: "ipv4" },
|
|
1436
|
+
"ipv6": { type: "bool", name: "ipv6" },
|
|
1437
|
+
"append": { type: "bool", name: "append" },
|
|
1438
|
+
"no-append": { type: "bool", name: "append", expand: false },
|
|
1439
|
+
"user-agent": { type: "string", name: "user-agent" },
|
|
1440
|
+
"cookie": { type: "string", name: "cookie" },
|
|
1441
|
+
"alt-svc": { type: "string", name: "alt-svc" },
|
|
1442
|
+
"hsts": { type: "string", name: "hsts" },
|
|
1443
|
+
"use-ascii": { type: "bool", name: "use-ascii" },
|
|
1444
|
+
"no-use-ascii": { type: "bool", name: "use-ascii", expand: false },
|
|
1445
|
+
"cookie-jar": { type: "string", name: "cookie-jar" },
|
|
1446
|
+
"continue-at": { type: "string", name: "continue-at" },
|
|
1447
|
+
"data": { type: "string", name: "data" },
|
|
1448
|
+
"data-raw": { type: "string", name: "data-raw" },
|
|
1449
|
+
"data-ascii": { type: "string", name: "data-ascii" },
|
|
1450
|
+
"data-binary": { type: "string", name: "data-binary" },
|
|
1451
|
+
"data-urlencode": { type: "string", name: "data-urlencode" },
|
|
1452
|
+
"json": { type: "string", name: "json" },
|
|
1453
|
+
"url-query": { type: "string", name: "url-query" },
|
|
1454
|
+
"dump-header": { type: "string", name: "dump-header" },
|
|
1455
|
+
"referer": { type: "string", name: "referer" },
|
|
1456
|
+
"cert": { type: "string", name: "cert" },
|
|
1457
|
+
"cacert": { type: "string", name: "cacert" },
|
|
1458
|
+
"cert-type": { type: "string", name: "cert-type" },
|
|
1459
|
+
"key": { type: "string", name: "key" },
|
|
1460
|
+
"key-type": { type: "string", name: "key-type" },
|
|
1461
|
+
"pass": { type: "string", name: "pass" },
|
|
1462
|
+
"engine": { type: "string", name: "engine" },
|
|
1463
|
+
"ca-native": { type: "bool", name: "ca-native" },
|
|
1464
|
+
"no-ca-native": { type: "bool", name: "ca-native", expand: false },
|
|
1465
|
+
"proxy-ca-native": { type: "bool", name: "proxy-ca-native" },
|
|
1466
|
+
"no-proxy-ca-native": { type: "bool", name: "proxy-ca-native", expand: false },
|
|
1467
|
+
"capath": { type: "string", name: "capath" },
|
|
1468
|
+
"pubkey": { type: "string", name: "pubkey" },
|
|
1469
|
+
"hostpubmd5": { type: "string", name: "hostpubmd5" },
|
|
1470
|
+
"hostpubsha256": { type: "string", name: "hostpubsha256" },
|
|
1471
|
+
"crlfile": { type: "string", name: "crlfile" },
|
|
1472
|
+
"tlsuser": { type: "string", name: "tlsuser" },
|
|
1473
|
+
"tlspassword": { type: "string", name: "tlspassword" },
|
|
1474
|
+
"tlsauthtype": { type: "string", name: "tlsauthtype" },
|
|
1475
|
+
"ssl-allow-beast": { type: "bool", name: "ssl-allow-beast" },
|
|
1476
|
+
"no-ssl-allow-beast": { type: "bool", name: "ssl-allow-beast", expand: false },
|
|
1477
|
+
"ssl-auto-client-cert": { type: "bool", name: "ssl-auto-client-cert" },
|
|
1478
|
+
"no-ssl-auto-client-cert": { type: "bool", name: "ssl-auto-client-cert", expand: false },
|
|
1479
|
+
"proxy-ssl-auto-client-cert": { type: "bool", name: "proxy-ssl-auto-client-cert" },
|
|
1480
|
+
"no-proxy-ssl-auto-client-cert": { type: "bool", name: "proxy-ssl-auto-client-cert", expand: false },
|
|
1481
|
+
"pinnedpubkey": { type: "string", name: "pinnedpubkey" },
|
|
1482
|
+
"proxy-pinnedpubkey": { type: "string", name: "proxy-pinnedpubkey" },
|
|
1483
|
+
"cert-status": { type: "bool", name: "cert-status" },
|
|
1484
|
+
"no-cert-status": { type: "bool", name: "cert-status", expand: false },
|
|
1485
|
+
"doh-cert-status": { type: "bool", name: "doh-cert-status" },
|
|
1486
|
+
"no-doh-cert-status": { type: "bool", name: "doh-cert-status", expand: false },
|
|
1487
|
+
"false-start": { type: "bool", name: "false-start" },
|
|
1488
|
+
"no-false-start": { type: "bool", name: "false-start", expand: false },
|
|
1489
|
+
"ssl-no-revoke": { type: "bool", name: "ssl-no-revoke" },
|
|
1490
|
+
"no-ssl-no-revoke": { type: "bool", name: "ssl-no-revoke", expand: false },
|
|
1491
|
+
"ssl-revoke-best-effort": { type: "bool", name: "ssl-revoke-best-effort" },
|
|
1492
|
+
"no-ssl-revoke-best-effort": { type: "bool", name: "ssl-revoke-best-effort", expand: false },
|
|
1493
|
+
"tcp-fastopen": { type: "bool", name: "tcp-fastopen" },
|
|
1494
|
+
"no-tcp-fastopen": { type: "bool", name: "tcp-fastopen", expand: false },
|
|
1495
|
+
"proxy-tlsuser": { type: "string", name: "proxy-tlsuser" },
|
|
1496
|
+
"proxy-tlspassword": { type: "string", name: "proxy-tlspassword" },
|
|
1497
|
+
"proxy-tlsauthtype": { type: "string", name: "proxy-tlsauthtype" },
|
|
1498
|
+
"proxy-cert": { type: "string", name: "proxy-cert" },
|
|
1499
|
+
"proxy-cert-type": { type: "string", name: "proxy-cert-type" },
|
|
1500
|
+
"proxy-key": { type: "string", name: "proxy-key" },
|
|
1501
|
+
"proxy-key-type": { type: "string", name: "proxy-key-type" },
|
|
1502
|
+
"proxy-pass": { type: "string", name: "proxy-pass" },
|
|
1503
|
+
"proxy-ciphers": { type: "string", name: "proxy-ciphers" },
|
|
1504
|
+
"proxy-crlfile": { type: "string", name: "proxy-crlfile" },
|
|
1505
|
+
"proxy-ssl-allow-beast": { type: "bool", name: "proxy-ssl-allow-beast" },
|
|
1506
|
+
"no-proxy-ssl-allow-beast": { type: "bool", name: "proxy-ssl-allow-beast", expand: false },
|
|
1507
|
+
"login-options": { type: "string", name: "login-options" },
|
|
1508
|
+
"proxy-cacert": { type: "string", name: "proxy-cacert" },
|
|
1509
|
+
"proxy-capath": { type: "string", name: "proxy-capath" },
|
|
1510
|
+
"proxy-insecure": { type: "bool", name: "proxy-insecure" },
|
|
1511
|
+
"no-proxy-insecure": { type: "bool", name: "proxy-insecure", expand: false },
|
|
1512
|
+
"proxy-tlsv1": { type: "bool", name: "proxy-tlsv1" },
|
|
1513
|
+
"socks5-basic": { type: "bool", name: "socks5-basic" },
|
|
1514
|
+
"no-socks5-basic": { type: "bool", name: "socks5-basic", expand: false },
|
|
1515
|
+
"socks5-gssapi": { type: "bool", name: "socks5-gssapi" },
|
|
1516
|
+
"no-socks5-gssapi": { type: "bool", name: "socks5-gssapi", expand: false },
|
|
1517
|
+
"etag-save": { type: "string", name: "etag-save" },
|
|
1518
|
+
"etag-compare": { type: "string", name: "etag-compare" },
|
|
1519
|
+
"curves": { type: "string", name: "curves" },
|
|
1520
|
+
"fail": { type: "bool", name: "fail" },
|
|
1521
|
+
"no-fail": { type: "bool", name: "fail", expand: false },
|
|
1522
|
+
"fail-early": { type: "bool", name: "fail-early" },
|
|
1523
|
+
"no-fail-early": { type: "bool", name: "fail-early", expand: false },
|
|
1524
|
+
"styled-output": { type: "bool", name: "styled-output" },
|
|
1525
|
+
"no-styled-output": { type: "bool", name: "styled-output", expand: false },
|
|
1526
|
+
"mail-rcpt-allowfails": { type: "bool", name: "mail-rcpt-allowfails" },
|
|
1527
|
+
"no-mail-rcpt-allowfails": { type: "bool", name: "mail-rcpt-allowfails", expand: false },
|
|
1528
|
+
"fail-with-body": { type: "bool", name: "fail-with-body" },
|
|
1529
|
+
"no-fail-with-body": { type: "bool", name: "fail-with-body", expand: false },
|
|
1530
|
+
"remove-on-error": { type: "bool", name: "remove-on-error" },
|
|
1531
|
+
"no-remove-on-error": { type: "bool", name: "remove-on-error", expand: false },
|
|
1532
|
+
"form": { type: "string", name: "form" },
|
|
1533
|
+
"form-string": { type: "string", name: "form-string" },
|
|
1534
|
+
"globoff": { type: "bool", name: "globoff" },
|
|
1535
|
+
"no-globoff": { type: "bool", name: "globoff", expand: false },
|
|
1536
|
+
"get": { type: "bool", name: "get" },
|
|
1537
|
+
"no-get": { type: "bool", name: "get", expand: false },
|
|
1538
|
+
"request-target": { type: "string", name: "request-target" },
|
|
1539
|
+
"help": { type: "bool", name: "help" },
|
|
1540
|
+
"no-help": { type: "bool", name: "help", expand: false },
|
|
1541
|
+
"header": { type: "string", name: "header" },
|
|
1542
|
+
"proxy-header": { type: "string", name: "proxy-header" },
|
|
1543
|
+
"include": { type: "bool", name: "include" },
|
|
1544
|
+
"no-include": { type: "bool", name: "include", expand: false },
|
|
1545
|
+
"head": { type: "bool", name: "head" },
|
|
1546
|
+
"no-head": { type: "bool", name: "head", expand: false },
|
|
1547
|
+
"junk-session-cookies": { type: "bool", name: "junk-session-cookies" },
|
|
1548
|
+
"no-junk-session-cookies": { type: "bool", name: "junk-session-cookies", expand: false },
|
|
1549
|
+
"remote-header-name": { type: "bool", name: "remote-header-name" },
|
|
1550
|
+
"no-remote-header-name": { type: "bool", name: "remote-header-name", expand: false },
|
|
1551
|
+
"insecure": { type: "bool", name: "insecure" },
|
|
1552
|
+
"no-insecure": { type: "bool", name: "insecure", expand: false },
|
|
1553
|
+
"doh-insecure": { type: "bool", name: "doh-insecure" },
|
|
1554
|
+
"no-doh-insecure": { type: "bool", name: "doh-insecure", expand: false },
|
|
1555
|
+
"config": { type: "string", name: "config" },
|
|
1556
|
+
"list-only": { type: "bool", name: "list-only" },
|
|
1557
|
+
"no-list-only": { type: "bool", name: "list-only", expand: false },
|
|
1558
|
+
"location": { type: "bool", name: "location" },
|
|
1559
|
+
"no-location": { type: "bool", name: "location", expand: false },
|
|
1560
|
+
"location-trusted": { type: "bool", name: "location-trusted" },
|
|
1561
|
+
"no-location-trusted": { type: "bool", name: "location-trusted", expand: false },
|
|
1562
|
+
"max-time": { type: "string", name: "max-time" },
|
|
1563
|
+
"manual": { type: "bool", name: "manual" },
|
|
1564
|
+
"no-manual": { type: "bool", name: "manual", expand: false },
|
|
1565
|
+
"netrc": { type: "bool", name: "netrc" },
|
|
1566
|
+
"no-netrc": { type: "bool", name: "netrc", expand: false },
|
|
1567
|
+
"netrc-optional": { type: "bool", name: "netrc-optional" },
|
|
1568
|
+
"no-netrc-optional": { type: "bool", name: "netrc-optional", expand: false },
|
|
1569
|
+
"netrc-file": { type: "string", name: "netrc-file" },
|
|
1570
|
+
"buffer": { type: "bool", name: "buffer" },
|
|
1571
|
+
"no-buffer": { type: "bool", name: "buffer", expand: false },
|
|
1572
|
+
"output": { type: "string", name: "output" },
|
|
1573
|
+
"remote-name": { type: "bool", name: "remote-name" },
|
|
1574
|
+
"no-remote-name": { type: "bool", name: "remote-name", expand: false },
|
|
1575
|
+
"remote-name-all": { type: "bool", name: "remote-name-all" },
|
|
1576
|
+
"no-remote-name-all": { type: "bool", name: "remote-name-all", expand: false },
|
|
1577
|
+
"output-dir": { type: "string", name: "output-dir" },
|
|
1578
|
+
"clobber": { type: "bool", name: "clobber" },
|
|
1579
|
+
"no-clobber": { type: "bool", name: "clobber", expand: false },
|
|
1580
|
+
"proxytunnel": { type: "bool", name: "proxytunnel" },
|
|
1581
|
+
"no-proxytunnel": { type: "bool", name: "proxytunnel", expand: false },
|
|
1582
|
+
"ftp-port": { type: "string", name: "ftp-port" },
|
|
1583
|
+
"disable": { type: "bool", name: "disable" },
|
|
1584
|
+
"no-disable": { type: "bool", name: "disable", expand: false },
|
|
1585
|
+
"quote": { type: "string", name: "quote" },
|
|
1586
|
+
"range": { type: "string", name: "range" },
|
|
1587
|
+
"remote-time": { type: "bool", name: "remote-time" },
|
|
1588
|
+
"no-remote-time": { type: "bool", name: "remote-time", expand: false },
|
|
1589
|
+
"silent": { type: "bool", name: "silent" },
|
|
1590
|
+
"no-silent": { type: "bool", name: "silent", expand: false },
|
|
1591
|
+
"show-error": { type: "bool", name: "show-error" },
|
|
1592
|
+
"no-show-error": { type: "bool", name: "show-error", expand: false },
|
|
1593
|
+
"telnet-option": { type: "string", name: "telnet-option" },
|
|
1594
|
+
"upload-file": { type: "string", name: "upload-file" },
|
|
1595
|
+
"user": { type: "string", name: "user" },
|
|
1596
|
+
"proxy-user": { type: "string", name: "proxy-user" },
|
|
1597
|
+
"verbose": { type: "bool", name: "verbose" },
|
|
1598
|
+
"no-verbose": { type: "bool", name: "verbose", expand: false },
|
|
1599
|
+
"version": { type: "bool", name: "version" },
|
|
1600
|
+
"no-version": { type: "bool", name: "version", expand: false },
|
|
1601
|
+
"write-out": { type: "string", name: "write-out" },
|
|
1602
|
+
"proxy": { type: "string", name: "proxy" },
|
|
1603
|
+
"preproxy": { type: "string", name: "preproxy" },
|
|
1604
|
+
"request": { type: "string", name: "request" },
|
|
1605
|
+
"speed-limit": { type: "string", name: "speed-limit" },
|
|
1606
|
+
"speed-time": { type: "string", name: "speed-time" },
|
|
1607
|
+
"time-cond": { type: "string", name: "time-cond" },
|
|
1608
|
+
"parallel": { type: "bool", name: "parallel" },
|
|
1609
|
+
"no-parallel": { type: "bool", name: "parallel", expand: false },
|
|
1610
|
+
"parallel-max": { type: "string", name: "parallel-max" },
|
|
1611
|
+
"parallel-immediate": { type: "bool", name: "parallel-immediate" },
|
|
1612
|
+
"no-parallel-immediate": { type: "bool", name: "parallel-immediate", expand: false },
|
|
1613
|
+
"progress-bar": { type: "bool", name: "progress-bar" },
|
|
1614
|
+
"no-progress-bar": { type: "bool", name: "progress-bar", expand: false },
|
|
1615
|
+
"progress-meter": { type: "bool", name: "progress-meter" },
|
|
1616
|
+
"no-progress-meter": { type: "bool", name: "progress-meter", expand: false },
|
|
1617
|
+
"next": { type: "bool", name: "next" },
|
|
1618
|
+
// END EXTRACTED OPTIONS
|
|
1619
|
+
// These are options that curl used to have.
|
|
1620
|
+
// Those that don't conflict with the current options are supported by curlconverter.
|
|
1621
|
+
// TODO: curl's --long-options can be shortened.
|
|
1622
|
+
// For example if curl used to only have a single option, "--blah" then
|
|
1623
|
+
// "--bla" "--bl" and "--b" all used to be valid options as well. If later
|
|
1624
|
+
// "--blaz" was added, suddenly those 3 shortened options are removed (because
|
|
1625
|
+
// they are now ambiguous).
|
|
1626
|
+
// https://github.com/curlconverter/curlconverter/pull/280#issuecomment-931241328
|
|
1627
|
+
port: { type: "string", name: "port", removed: "7.3" },
|
|
1628
|
+
// These are now shoretened forms of --upload-file and --continue-at
|
|
1629
|
+
//upload: { type: "bool", name: "upload", removed: "7.7" },
|
|
1630
|
+
//continue: { type: "bool", name: "continue", removed: "7.9" },
|
|
1631
|
+
"ftp-ascii": { type: "bool", name: "use-ascii", removed: "7.10.7" },
|
|
1632
|
+
"3p-url": { type: "string", name: "3p-url", removed: "7.16.0" },
|
|
1633
|
+
"3p-user": { type: "string", name: "3p-user", removed: "7.16.0" },
|
|
1634
|
+
"3p-quote": { type: "string", name: "3p-quote", removed: "7.16.0" },
|
|
1635
|
+
"http2.0": { type: "bool", name: "http2", removed: "7.36.0" },
|
|
1636
|
+
"no-http2.0": { type: "bool", name: "http2", removed: "7.36.0" },
|
|
1637
|
+
"telnet-options": { type: "string", name: "telnet-option", removed: "7.49.0" },
|
|
1638
|
+
"http-request": { type: "string", name: "request", removed: "7.49.0" },
|
|
1639
|
+
// --socks is now an ambiguous shortening of --socks4, --socks5 and a bunch more
|
|
1640
|
+
//socks: { type: "string", name: "socks5", removed: "7.49.0" },
|
|
1641
|
+
"capath ": { type: "string", name: "capath", removed: "7.49.0" },
|
|
1642
|
+
ftpport: { type: "string", name: "ftp-port", removed: "7.49.0" },
|
|
1643
|
+
environment: { type: "bool", name: "environment", removed: "7.54.1" },
|
|
1644
|
+
// These never had any effect
|
|
1645
|
+
"no-tlsv1": { type: "bool", name: "tlsv1", removed: "7.54.1" },
|
|
1646
|
+
"no-tlsv1.2": { type: "bool", name: "tlsv1.2", removed: "7.54.1" },
|
|
1647
|
+
"no-http2-prior-knowledge": { type: "bool", name: "http2-prior-knowledge", removed: "7.54.1" },
|
|
1648
|
+
"no-ipv6": { type: "bool", name: "ipv6", removed: "7.54.1" },
|
|
1649
|
+
"no-ipv4": { type: "bool", name: "ipv4", removed: "7.54.1" },
|
|
1650
|
+
"no-sslv2": { type: "bool", name: "sslv2", removed: "7.54.1" },
|
|
1651
|
+
"no-tlsv1.0": { type: "bool", name: "tlsv1.0", removed: "7.54.1" },
|
|
1652
|
+
"no-tlsv1.1": { type: "bool", name: "tlsv1.1", removed: "7.54.1" },
|
|
1653
|
+
"no-sslv3": { type: "bool", name: "sslv3", removed: "7.54.1" },
|
|
1654
|
+
"no-http1.0": { type: "bool", name: "http1.0", removed: "7.54.1" },
|
|
1655
|
+
"no-next": { type: "bool", name: "next", removed: "7.54.1" },
|
|
1656
|
+
"no-tlsv1.3": { type: "bool", name: "tlsv1.3", removed: "7.54.1" },
|
|
1657
|
+
"no-environment": { type: "bool", name: "environment", removed: "7.54.1" },
|
|
1658
|
+
"no-http1.1": { type: "bool", name: "http1.1", removed: "7.54.1" },
|
|
1659
|
+
"no-proxy-tlsv1": { type: "bool", name: "proxy-tlsv1", removed: "7.54.1" },
|
|
1660
|
+
"no-http2": { type: "bool", name: "http2", removed: "7.54.1" },
|
|
1661
|
+
};
|
|
1662
|
+
// curl lets you not type the full argument as long as it's unambiguous.
|
|
1663
|
+
// So --sil instead of --silent is okay, --s is not.
|
|
1664
|
+
const curlLongOptsShortened = {};
|
|
1665
|
+
for (const [opt, val] of Object.entries(curlLongOpts)) {
|
|
1666
|
+
const expand = "expand" in val ? val.expand : true;
|
|
1667
|
+
const removed = "removed" in val ? val.removed : false;
|
|
1668
|
+
if (expand && !removed) {
|
|
1669
|
+
for (let i = 1; i < opt.length; i++) {
|
|
1670
|
+
const shortenedOpt = opt.slice(0, i);
|
|
1671
|
+
if (!Object.prototype.hasOwnProperty.call(curlLongOptsShortened, shortenedOpt)) {
|
|
1672
|
+
if (!Object.prototype.hasOwnProperty.call(curlLongOpts, shortenedOpt)) {
|
|
1673
|
+
curlLongOptsShortened[shortenedOpt] = val;
|
|
1674
|
+
}
|
|
1675
|
+
}
|
|
1676
|
+
else {
|
|
1677
|
+
// If more than one option shortens to this, it's ambiguous
|
|
1678
|
+
curlLongOptsShortened[shortenedOpt] = null;
|
|
1679
|
+
}
|
|
1680
|
+
}
|
|
1681
|
+
}
|
|
1682
|
+
}
|
|
1683
|
+
// Arguments which are supported by all generators, because they're
|
|
1684
|
+
// easy to implement or because they're handled by upstream code and
|
|
1685
|
+
// affect something that's easy to implement.
|
|
1686
|
+
const COMMON_SUPPORTED_ARGS = [
|
|
1687
|
+
"url",
|
|
1688
|
+
"proto-default",
|
|
1689
|
+
// Method
|
|
1690
|
+
"request",
|
|
1691
|
+
"get",
|
|
1692
|
+
"head",
|
|
1693
|
+
"no-head",
|
|
1694
|
+
// Headers
|
|
1695
|
+
"header",
|
|
1696
|
+
"user-agent",
|
|
1697
|
+
"referer",
|
|
1698
|
+
"range",
|
|
1699
|
+
"time-cond",
|
|
1700
|
+
"cookie",
|
|
1701
|
+
"oauth2-bearer",
|
|
1702
|
+
// Basic Auth
|
|
1703
|
+
"user",
|
|
1704
|
+
"basic",
|
|
1705
|
+
"no-basic",
|
|
1706
|
+
// Data
|
|
1707
|
+
"data",
|
|
1708
|
+
"data-raw",
|
|
1709
|
+
"data-ascii",
|
|
1710
|
+
"data-binary",
|
|
1711
|
+
"data-urlencode",
|
|
1712
|
+
"json",
|
|
1713
|
+
"url-query",
|
|
1714
|
+
// Trivial support for globoff means controlling whether or not
|
|
1715
|
+
// backslash-escaped [] {} will have the backslash removed.
|
|
1716
|
+
"globoff",
|
|
1717
|
+
// curl will exit if it finds auth credentials in the URL with this option,
|
|
1718
|
+
// we remove it from the URL and emit a warning instead.
|
|
1719
|
+
"disallow-username-in-url",
|
|
1720
|
+
// TODO: --compressed is already the default for some runtimes, in
|
|
1721
|
+
// which case we might have to only warn that --no-compressed isn't supported.
|
|
1722
|
+
];
|
|
1723
|
+
function toBoolean(opt) {
|
|
1724
|
+
if (opt.startsWith("no-disable-")) {
|
|
1725
|
+
return true;
|
|
1726
|
+
}
|
|
1727
|
+
if (opt.startsWith("disable-") || opt.startsWith("no-")) {
|
|
1728
|
+
return false;
|
|
1729
|
+
}
|
|
1730
|
+
return true;
|
|
1731
|
+
}
|
|
1732
|
+
// prettier-ignore
|
|
1733
|
+
const curlShortOpts = {
|
|
1734
|
+
// BEGIN EXTRACTED SHORT OPTIONS
|
|
1735
|
+
"0": "http1.0",
|
|
1736
|
+
"1": "tlsv1",
|
|
1737
|
+
"2": "sslv2",
|
|
1738
|
+
"3": "sslv3",
|
|
1739
|
+
"4": "ipv4",
|
|
1740
|
+
"6": "ipv6",
|
|
1741
|
+
"a": "append",
|
|
1742
|
+
"A": "user-agent",
|
|
1743
|
+
"b": "cookie",
|
|
1744
|
+
"B": "use-ascii",
|
|
1745
|
+
"c": "cookie-jar",
|
|
1746
|
+
"C": "continue-at",
|
|
1747
|
+
"d": "data",
|
|
1748
|
+
"D": "dump-header",
|
|
1749
|
+
"e": "referer",
|
|
1750
|
+
"E": "cert",
|
|
1751
|
+
"f": "fail",
|
|
1752
|
+
"F": "form",
|
|
1753
|
+
"g": "globoff",
|
|
1754
|
+
"G": "get",
|
|
1755
|
+
"h": "help",
|
|
1756
|
+
"H": "header",
|
|
1757
|
+
"i": "include",
|
|
1758
|
+
"I": "head",
|
|
1759
|
+
"j": "junk-session-cookies",
|
|
1760
|
+
"J": "remote-header-name",
|
|
1761
|
+
"k": "insecure",
|
|
1762
|
+
"K": "config",
|
|
1763
|
+
"l": "list-only",
|
|
1764
|
+
"L": "location",
|
|
1765
|
+
"m": "max-time",
|
|
1766
|
+
"M": "manual",
|
|
1767
|
+
"n": "netrc",
|
|
1768
|
+
"N": "no-buffer",
|
|
1769
|
+
"o": "output",
|
|
1770
|
+
"O": "remote-name",
|
|
1771
|
+
"p": "proxytunnel",
|
|
1772
|
+
"P": "ftp-port",
|
|
1773
|
+
"q": "disable",
|
|
1774
|
+
"Q": "quote",
|
|
1775
|
+
"r": "range",
|
|
1776
|
+
"R": "remote-time",
|
|
1777
|
+
"s": "silent",
|
|
1778
|
+
"S": "show-error",
|
|
1779
|
+
"t": "telnet-option",
|
|
1780
|
+
"T": "upload-file",
|
|
1781
|
+
"u": "user",
|
|
1782
|
+
"U": "proxy-user",
|
|
1783
|
+
"v": "verbose",
|
|
1784
|
+
"V": "version",
|
|
1785
|
+
"w": "write-out",
|
|
1786
|
+
"x": "proxy",
|
|
1787
|
+
"X": "request",
|
|
1788
|
+
"Y": "speed-limit",
|
|
1789
|
+
"y": "speed-time",
|
|
1790
|
+
"z": "time-cond",
|
|
1791
|
+
"Z": "parallel",
|
|
1792
|
+
"#": "progress-bar",
|
|
1793
|
+
":": "next",
|
|
1794
|
+
// END EXTRACTED SHORT OPTIONS
|
|
1795
|
+
};
|
|
1796
|
+
const changedShortOpts = {
|
|
1797
|
+
p: "used to be short for --port <port> (a since-deleted flag) until curl 7.3",
|
|
1798
|
+
// TODO: some of these might be renamed options
|
|
1799
|
+
t: "used to be short for --upload (a since-deleted boolean flag) until curl 7.7",
|
|
1800
|
+
c: "used to be short for --continue (a since-deleted boolean flag) until curl 7.9",
|
|
1801
|
+
// TODO: did -@ actually work?
|
|
1802
|
+
"@": "used to be short for --create-dirs until curl 7.10.7",
|
|
1803
|
+
Z: "used to be short for --max-redirs <num> until curl 7.10.7",
|
|
1804
|
+
9: "used to be short for --crlf until curl 7.10.8",
|
|
1805
|
+
8: "used to be short for --stderr <file> until curl 7.10.8",
|
|
1806
|
+
7: "used to be short for --interface <name> until curl 7.10.8",
|
|
1807
|
+
6: "used to be short for --krb <level> (which itself used to be --krb4 <level>) until curl 7.10.8",
|
|
1808
|
+
// TODO: did these short options ever actually work?
|
|
1809
|
+
5: "used to be another way to specify the url until curl 7.10.8",
|
|
1810
|
+
"*": "used to be another way to specify the url until curl 7.49.0",
|
|
1811
|
+
"~": "used to be short for --xattr until curl 7.49.0",
|
|
1812
|
+
};
|
|
1813
|
+
// type Satisfies<T, U extends T> = void;
|
|
1814
|
+
// type AssertSubsetKeys = Satisfies<
|
|
1815
|
+
// keyof typeof curlLongOpts | "authtype" | "authArgs",
|
|
1816
|
+
// keyof OperationConfig
|
|
1817
|
+
// >;
|
|
1818
|
+
// These options can be specified more than once, they
|
|
1819
|
+
// are always returned as a list.
|
|
1820
|
+
// For all other options, if you specify it more than once
|
|
1821
|
+
// curl will use the last one.
|
|
1822
|
+
const canBeList = new Set([
|
|
1823
|
+
"authArgs",
|
|
1824
|
+
"connect-to",
|
|
1825
|
+
"cookie",
|
|
1826
|
+
"data",
|
|
1827
|
+
"form",
|
|
1828
|
+
"header",
|
|
1829
|
+
"hsts",
|
|
1830
|
+
"mail-rcpt",
|
|
1831
|
+
"output",
|
|
1832
|
+
"proxy-header",
|
|
1833
|
+
"quote",
|
|
1834
|
+
"resolve",
|
|
1835
|
+
"telnet-option",
|
|
1836
|
+
"upload-file",
|
|
1837
|
+
"url-query",
|
|
1838
|
+
"url",
|
|
1839
|
+
]);
|
|
1840
|
+
function checkSupported(global, lookup, longArg, supportedOpts) {
|
|
1841
|
+
if (supportedOpts && !supportedOpts.has(longArg.name)) {
|
|
1842
|
+
// TODO: better message. include generator name?
|
|
1843
|
+
warnf(global, [
|
|
1844
|
+
longArg.name,
|
|
1845
|
+
lookup +
|
|
1846
|
+
" is not a supported option" +
|
|
1847
|
+
(longArg.removed ? ", it was removed in curl " + longArg.removed : ""),
|
|
1848
|
+
]);
|
|
1849
|
+
}
|
|
1850
|
+
}
|
|
1851
|
+
function pushProp(obj, prop, value) {
|
|
1852
|
+
if (!Object.prototype.hasOwnProperty.call(obj, prop)) {
|
|
1853
|
+
obj[prop] = [];
|
|
1854
|
+
}
|
|
1855
|
+
obj[prop].push(value);
|
|
1856
|
+
return obj;
|
|
1857
|
+
}
|
|
1858
|
+
function pushArgValue(global, config, argName, value) {
|
|
1859
|
+
// Note: cli.ts assumes that the property names on OperationConfig
|
|
1860
|
+
// are the same as the passed in argument in an error message, so
|
|
1861
|
+
// if you do something like
|
|
1862
|
+
// echo curl example.com | curlconverter - --data-raw foo
|
|
1863
|
+
// The error message will say
|
|
1864
|
+
// "if you pass --stdin or -, you can't also pass --data"
|
|
1865
|
+
// instead of "--data-raw".
|
|
1866
|
+
switch (argName) {
|
|
1867
|
+
case "data":
|
|
1868
|
+
case "data-ascii":
|
|
1869
|
+
return pushProp(config, "data", ["data", value]);
|
|
1870
|
+
case "data-binary":
|
|
1871
|
+
return pushProp(config, "data", [
|
|
1872
|
+
// Unless it's a file, --data-binary works the same as --data
|
|
1873
|
+
value.startsWith("@") ? "binary" : "data",
|
|
1874
|
+
value,
|
|
1875
|
+
]);
|
|
1876
|
+
case "data-raw":
|
|
1877
|
+
return pushProp(config, "data", [
|
|
1878
|
+
// Unless it's a file, --data-raw works the same as --data
|
|
1879
|
+
value.startsWith("@") ? "raw" : "data",
|
|
1880
|
+
value,
|
|
1881
|
+
]);
|
|
1882
|
+
case "data-urlencode":
|
|
1883
|
+
return pushProp(config, "data", ["urlencode", value]);
|
|
1884
|
+
case "json":
|
|
1885
|
+
config.json = true;
|
|
1886
|
+
return pushProp(config, "data", ["json", value]);
|
|
1887
|
+
case "url-query":
|
|
1888
|
+
if (value.startsWith("+")) {
|
|
1889
|
+
return pushProp(config, "url-query", ["raw", value.slice(1)]);
|
|
1890
|
+
}
|
|
1891
|
+
return pushProp(config, "url-query", ["urlencode", value]);
|
|
1892
|
+
case "form":
|
|
1893
|
+
return pushProp(config, "form", { value, type: "form" });
|
|
1894
|
+
case "form-string":
|
|
1895
|
+
return pushProp(config, "form", { value, type: "string" });
|
|
1896
|
+
case "aws-sigv4":
|
|
1897
|
+
pushProp(config, "authArgs", [argName, true]); // error reporting
|
|
1898
|
+
config.authtype |= CURLAUTH_AWS_SIGV4;
|
|
1899
|
+
break;
|
|
1900
|
+
case "oauth2-bearer":
|
|
1901
|
+
pushProp(config, "authArgs", [argName, true]); // error reporting
|
|
1902
|
+
config.authtype |= CURLAUTH_BEARER;
|
|
1903
|
+
break;
|
|
1904
|
+
case "unix-socket":
|
|
1905
|
+
case "abstract-unix-socket":
|
|
1906
|
+
// Ignore distinction
|
|
1907
|
+
// TODO: this makes the error message wrong
|
|
1908
|
+
// TODO: what's the difference?
|
|
1909
|
+
pushProp(config, "unix-socket", value);
|
|
1910
|
+
break;
|
|
1911
|
+
case "trace":
|
|
1912
|
+
case "trace-ascii":
|
|
1913
|
+
case "stderr":
|
|
1914
|
+
case "libcurl":
|
|
1915
|
+
case "config":
|
|
1916
|
+
case "parallel-max":
|
|
1917
|
+
global[argName] = value;
|
|
1918
|
+
break;
|
|
1919
|
+
case "language": // --language is a curlconverter specific option
|
|
1920
|
+
global[argName] = value.toString();
|
|
1921
|
+
return;
|
|
1922
|
+
}
|
|
1923
|
+
return pushProp(config, argName, value);
|
|
1924
|
+
}
|
|
1925
|
+
// Might create a new config
|
|
1926
|
+
function setArgValue(global, config, argName, toggle) {
|
|
1927
|
+
switch (argName) {
|
|
1928
|
+
case "digest":
|
|
1929
|
+
pushProp(config, "authArgs", [argName, toggle]); // error reporting
|
|
1930
|
+
if (toggle) {
|
|
1931
|
+
config.authtype |= CURLAUTH_DIGEST;
|
|
1932
|
+
}
|
|
1933
|
+
else {
|
|
1934
|
+
config.authtype &= ~CURLAUTH_DIGEST;
|
|
1935
|
+
}
|
|
1936
|
+
break;
|
|
1937
|
+
case "negotiate":
|
|
1938
|
+
pushProp(config, "authArgs", [argName, toggle]); // error reporting
|
|
1939
|
+
if (toggle) {
|
|
1940
|
+
config.authtype |= CURLAUTH_NEGOTIATE;
|
|
1941
|
+
}
|
|
1942
|
+
else {
|
|
1943
|
+
config.authtype &= ~CURLAUTH_NEGOTIATE;
|
|
1944
|
+
}
|
|
1945
|
+
break;
|
|
1946
|
+
case "ntlm":
|
|
1947
|
+
pushProp(config, "authArgs", [argName, toggle]); // error reporting
|
|
1948
|
+
if (toggle) {
|
|
1949
|
+
config.authtype |= CURLAUTH_NTLM;
|
|
1950
|
+
}
|
|
1951
|
+
else {
|
|
1952
|
+
config.authtype &= ~CURLAUTH_NTLM;
|
|
1953
|
+
}
|
|
1954
|
+
break;
|
|
1955
|
+
case "ntlm-wb":
|
|
1956
|
+
pushProp(config, "authArgs", [argName, toggle]); // error reporting
|
|
1957
|
+
if (toggle) {
|
|
1958
|
+
config.authtype |= CURLAUTH_NTLM_WB;
|
|
1959
|
+
}
|
|
1960
|
+
else {
|
|
1961
|
+
config.authtype &= ~CURLAUTH_NTLM_WB;
|
|
1962
|
+
}
|
|
1963
|
+
break;
|
|
1964
|
+
case "basic":
|
|
1965
|
+
pushProp(config, "authArgs", [argName, toggle]); // error reporting
|
|
1966
|
+
if (toggle) {
|
|
1967
|
+
config.authtype |= CURLAUTH_BASIC;
|
|
1968
|
+
}
|
|
1969
|
+
else {
|
|
1970
|
+
config.authtype &= ~CURLAUTH_BASIC;
|
|
1971
|
+
}
|
|
1972
|
+
break;
|
|
1973
|
+
case "anyauth":
|
|
1974
|
+
pushProp(config, "authArgs", [argName, toggle]); // error reporting
|
|
1975
|
+
if (toggle) {
|
|
1976
|
+
config.authtype = CURLAUTH_ANY;
|
|
1977
|
+
}
|
|
1978
|
+
break;
|
|
1979
|
+
case "location":
|
|
1980
|
+
config["location"] = toggle;
|
|
1981
|
+
break;
|
|
1982
|
+
case "location-trusted":
|
|
1983
|
+
config["location"] = toggle;
|
|
1984
|
+
config["location-trusted"] = toggle;
|
|
1985
|
+
break;
|
|
1986
|
+
case "verbose":
|
|
1987
|
+
case "version":
|
|
1988
|
+
case "trace-time":
|
|
1989
|
+
case "test-event":
|
|
1990
|
+
case "progress-bar":
|
|
1991
|
+
case "progress-meter":
|
|
1992
|
+
case "fail-early":
|
|
1993
|
+
case "styled-output":
|
|
1994
|
+
case "help":
|
|
1995
|
+
case "silent":
|
|
1996
|
+
case "show-error":
|
|
1997
|
+
case "parallel":
|
|
1998
|
+
case "parallel-immediate":
|
|
1999
|
+
case "stdin": // --stdin or - is a curlconverter specific option
|
|
2000
|
+
global[argName] = toggle;
|
|
2001
|
+
break;
|
|
2002
|
+
case "next":
|
|
2003
|
+
// curl ignores --next if the last url node doesn't have a url
|
|
2004
|
+
if (toggle &&
|
|
2005
|
+
config.url &&
|
|
2006
|
+
config.url.length > 0 &&
|
|
2007
|
+
config.url.length >= (config["upload-file"] || []).length &&
|
|
2008
|
+
config.url.length >= (config.output || []).length) {
|
|
2009
|
+
config = { authtype: CURLAUTH_BASIC };
|
|
2010
|
+
global.configs.push(config);
|
|
2011
|
+
}
|
|
2012
|
+
break;
|
|
2013
|
+
default:
|
|
2014
|
+
config[argName] = toggle;
|
|
2015
|
+
}
|
|
2016
|
+
return config;
|
|
2017
|
+
}
|
|
2018
|
+
function parseArgs(args, longOpts = curlLongOpts, shortenedLongOpts = curlLongOptsShortened, shortOpts = curlShortOpts, supportedOpts, warnings = []) {
|
|
2019
|
+
let config = { authtype: CURLAUTH_BASIC };
|
|
2020
|
+
const global = { configs: [config], warnings };
|
|
2021
|
+
for (let i = 1, stillflags = true; i < args.length; i++) {
|
|
2022
|
+
const arg = args[i];
|
|
2023
|
+
if (stillflags && arg.startsWith("-")) {
|
|
2024
|
+
if (eq(arg, "--")) {
|
|
2025
|
+
/* This indicates the end of the flags and thus enables the
|
|
2026
|
+
following (URL) argument to start with -. */
|
|
2027
|
+
stillflags = false;
|
|
2028
|
+
}
|
|
2029
|
+
else if (arg.startsWith("--")) {
|
|
2030
|
+
const shellToken = firstShellToken(arg);
|
|
2031
|
+
if (shellToken) {
|
|
2032
|
+
// TODO: if there's any text after the "--" or after the variable
|
|
2033
|
+
// we could narrow it down.
|
|
2034
|
+
throw new CCError("this " +
|
|
2035
|
+
shellToken.type +
|
|
2036
|
+
" could " +
|
|
2037
|
+
(shellToken.type === "command" ? "return" : "be") +
|
|
2038
|
+
" anything\n" +
|
|
2039
|
+
underlineNode(shellToken.syntaxNode));
|
|
2040
|
+
}
|
|
2041
|
+
const argStr = arg.toString();
|
|
2042
|
+
const lookup = argStr.slice(2);
|
|
2043
|
+
let longArg = shortenedLongOpts[lookup];
|
|
2044
|
+
if (typeof longArg === "undefined") {
|
|
2045
|
+
longArg = longOpts[lookup];
|
|
2046
|
+
}
|
|
2047
|
+
if (longArg === null) {
|
|
2048
|
+
throw new CCError("option " + argStr + ": is ambiguous");
|
|
2049
|
+
}
|
|
2050
|
+
if (typeof longArg === "undefined") {
|
|
2051
|
+
// TODO: extract a list of deleted arguments to check here
|
|
2052
|
+
throw new CCError("option " + argStr + ": is unknown");
|
|
2053
|
+
}
|
|
2054
|
+
if (longArg.type === "string") {
|
|
2055
|
+
i++;
|
|
2056
|
+
if (i >= args.length) {
|
|
2057
|
+
throw new CCError("option " + argStr + ": requires parameter");
|
|
2058
|
+
}
|
|
2059
|
+
pushArgValue(global, config, longArg.name, args[i]);
|
|
2060
|
+
}
|
|
2061
|
+
else {
|
|
2062
|
+
config = setArgValue(global, config, longArg.name, toBoolean(argStr.slice(2))); // TODO: all shortened args work correctly?
|
|
2063
|
+
}
|
|
2064
|
+
checkSupported(global, argStr, longArg, supportedOpts);
|
|
2065
|
+
}
|
|
2066
|
+
else {
|
|
2067
|
+
// Short option. These can look like
|
|
2068
|
+
// -X POST -> {request: 'POST'}
|
|
2069
|
+
// or
|
|
2070
|
+
// -XPOST -> {request: 'POST'}
|
|
2071
|
+
// or multiple options
|
|
2072
|
+
// -ABCX POST -> {A: true, B: true, C: true, request: 'POST'}
|
|
2073
|
+
// or multiple options and a value for the last one
|
|
2074
|
+
// -ABCXPOST -> {A: true, B: true, C: true, request: 'POST'}
|
|
2075
|
+
// "-" passed to curl as an argument raises an error,
|
|
2076
|
+
// curlconverter's command line uses it to read from stdin
|
|
2077
|
+
if (arg.length === 1) {
|
|
2078
|
+
if (Object.prototype.hasOwnProperty.call(shortOpts, "")) {
|
|
2079
|
+
const shortFor = shortOpts[""];
|
|
2080
|
+
const longArg = longOpts[shortFor];
|
|
2081
|
+
if (longArg === null) {
|
|
2082
|
+
throw new CCError("option -: is unknown");
|
|
2083
|
+
}
|
|
2084
|
+
config = setArgValue(global, config, longArg.name, toBoolean(shortFor));
|
|
2085
|
+
}
|
|
2086
|
+
else {
|
|
2087
|
+
throw new CCError("option -: is unknown");
|
|
2088
|
+
}
|
|
2089
|
+
}
|
|
2090
|
+
for (let j = 1; j < arg.length; j++) {
|
|
2091
|
+
const jthChar = arg.get(j);
|
|
2092
|
+
if (typeof jthChar !== "string") {
|
|
2093
|
+
// A bash variable in the middle of a short option
|
|
2094
|
+
throw new CCError("this " +
|
|
2095
|
+
jthChar.type +
|
|
2096
|
+
" could " +
|
|
2097
|
+
(jthChar.type === "command" ? "return" : "be") +
|
|
2098
|
+
" anything\n" +
|
|
2099
|
+
underlineNode(jthChar.syntaxNode));
|
|
2100
|
+
}
|
|
2101
|
+
if (!has(shortOpts, jthChar)) {
|
|
2102
|
+
if (has(changedShortOpts, jthChar)) {
|
|
2103
|
+
throw new CCError("option " + arg + ": " + changedShortOpts[jthChar]);
|
|
2104
|
+
}
|
|
2105
|
+
// TODO: there are a few deleted short options we could report
|
|
2106
|
+
throw new CCError("option " + arg + ": is unknown");
|
|
2107
|
+
}
|
|
2108
|
+
const lookup = jthChar;
|
|
2109
|
+
const shortFor = shortOpts[lookup];
|
|
2110
|
+
const longArg = longOpts[shortFor];
|
|
2111
|
+
if (longArg === null) {
|
|
2112
|
+
// This could happen if curlShortOpts points to a renamed option or has a typo
|
|
2113
|
+
throw new CCError("ambiguous short option -" + jthChar);
|
|
2114
|
+
}
|
|
2115
|
+
if (longArg.type === "string") {
|
|
2116
|
+
let val;
|
|
2117
|
+
if (j + 1 < arg.length) {
|
|
2118
|
+
// treat -XPOST as -X POST
|
|
2119
|
+
val = arg.slice(j + 1);
|
|
2120
|
+
j = arg.length;
|
|
2121
|
+
}
|
|
2122
|
+
else if (i + 1 < args.length) {
|
|
2123
|
+
i++;
|
|
2124
|
+
val = args[i];
|
|
2125
|
+
}
|
|
2126
|
+
else {
|
|
2127
|
+
throw new CCError("option " + arg.toString() + ": requires parameter");
|
|
2128
|
+
}
|
|
2129
|
+
pushArgValue(global, config, longArg.name, val);
|
|
2130
|
+
}
|
|
2131
|
+
else {
|
|
2132
|
+
// Use shortFor because -N is short for --no-buffer
|
|
2133
|
+
// and we want to end up with {buffer: false}
|
|
2134
|
+
config = setArgValue(global, config, longArg.name, toBoolean(shortFor));
|
|
2135
|
+
}
|
|
2136
|
+
if (lookup) {
|
|
2137
|
+
checkSupported(global, "-" + lookup, longArg, supportedOpts);
|
|
2138
|
+
}
|
|
2139
|
+
}
|
|
2140
|
+
}
|
|
2141
|
+
}
|
|
2142
|
+
else {
|
|
2143
|
+
if (typeof arg !== "string" &&
|
|
2144
|
+
arg.tokens.length &&
|
|
2145
|
+
typeof arg.tokens[0] !== "string") {
|
|
2146
|
+
const isOrBeginsWith = arg.tokens.length === 1 ? "is" : "begins with";
|
|
2147
|
+
warnings.push([
|
|
2148
|
+
"ambiguous argument",
|
|
2149
|
+
"argument " +
|
|
2150
|
+
isOrBeginsWith +
|
|
2151
|
+
" a " +
|
|
2152
|
+
arg.tokens[0].type +
|
|
2153
|
+
", assuming it's a URL\n" +
|
|
2154
|
+
underlineNode(arg.tokens[0].syntaxNode),
|
|
2155
|
+
]);
|
|
2156
|
+
}
|
|
2157
|
+
pushArgValue(global, config, "url", arg);
|
|
2158
|
+
}
|
|
2159
|
+
}
|
|
2160
|
+
for (const cfg of global.configs) {
|
|
2161
|
+
for (const [arg, values] of Object.entries(cfg)) {
|
|
2162
|
+
if (Array.isArray(values) && !canBeList.has(arg)) {
|
|
2163
|
+
cfg[arg] = values[values.length - 1];
|
|
2164
|
+
}
|
|
2165
|
+
}
|
|
2166
|
+
}
|
|
2167
|
+
return global;
|
|
2168
|
+
}
|
|
2169
|
+
|
|
2170
|
+
// https://en.wikipedia.org/wiki/List_of_HTTP_header_fields#Standard_request_fields
|
|
2171
|
+
// and then searched for "#" in the RFCs that define each header
|
|
2172
|
+
const COMMA_SEPARATED = new Set([
|
|
2173
|
+
"A-IM",
|
|
2174
|
+
"Accept",
|
|
2175
|
+
"Accept-Charset",
|
|
2176
|
+
// "Accept-Datetime",
|
|
2177
|
+
"Accept-Encoding",
|
|
2178
|
+
"Accept-Language",
|
|
2179
|
+
// "Access-Control-Request-Method",
|
|
2180
|
+
"Access-Control-Request-Headers",
|
|
2181
|
+
// TODO: auth-scheme [ 1*SP ( token68 / #auth-param ) ]
|
|
2182
|
+
// "Authorization",
|
|
2183
|
+
"Cache-Control",
|
|
2184
|
+
"Connection",
|
|
2185
|
+
"Content-Encoding",
|
|
2186
|
+
// "Content-Length",
|
|
2187
|
+
// "Content-MD5",
|
|
2188
|
+
// "Content-Type", // semicolon
|
|
2189
|
+
// "Cookie", // semicolon
|
|
2190
|
+
// "Date",
|
|
2191
|
+
"Expect",
|
|
2192
|
+
"Forwarded",
|
|
2193
|
+
// "From",
|
|
2194
|
+
// "Host",
|
|
2195
|
+
// "HTTP2-Settings",
|
|
2196
|
+
"If-Match",
|
|
2197
|
+
// "If-Modified-Since",
|
|
2198
|
+
"If-None-Match",
|
|
2199
|
+
// "If-Range",
|
|
2200
|
+
// "If-Unmodified-Since",
|
|
2201
|
+
// "Max-Forwards",
|
|
2202
|
+
// "Origin",
|
|
2203
|
+
// "Pragma",
|
|
2204
|
+
// "Prefer", // semicolon
|
|
2205
|
+
// "Proxy-Authorization",
|
|
2206
|
+
"Range",
|
|
2207
|
+
// "Referer",
|
|
2208
|
+
"TE",
|
|
2209
|
+
"Trailer",
|
|
2210
|
+
"Transfer-Encoding",
|
|
2211
|
+
// "User-Agent",
|
|
2212
|
+
"Upgrade",
|
|
2213
|
+
"Via",
|
|
2214
|
+
"Warning",
|
|
2215
|
+
].map((h) => h.toLowerCase()));
|
|
2216
|
+
const SEMICOLON_SEPARATED = new Set(["Content-Type", "Cookie", "Prefer"].map((h) => h.toLowerCase()));
|
|
2217
|
+
class Headers {
|
|
2218
|
+
constructor(headerArgs, warnings = []) {
|
|
2219
|
+
let headers = [];
|
|
2220
|
+
if (headerArgs) {
|
|
2221
|
+
for (const header of headerArgs) {
|
|
2222
|
+
if (header.startsWith("@")) {
|
|
2223
|
+
warnings.push([
|
|
2224
|
+
"header-file",
|
|
2225
|
+
"passing a file for --header/-H is not supported: " +
|
|
2226
|
+
JSON.stringify(header.toString()),
|
|
2227
|
+
]);
|
|
2228
|
+
continue;
|
|
2229
|
+
}
|
|
2230
|
+
if (header.includes(":")) {
|
|
2231
|
+
const [name, value] = header.split(":", 2);
|
|
2232
|
+
const nameToken = firstShellToken(name);
|
|
2233
|
+
if (nameToken) {
|
|
2234
|
+
warnings.push([
|
|
2235
|
+
"header-expression",
|
|
2236
|
+
"ignoring " +
|
|
2237
|
+
nameToken.type +
|
|
2238
|
+
" in header name\n" +
|
|
2239
|
+
underlineNode(nameToken.syntaxNode),
|
|
2240
|
+
]);
|
|
2241
|
+
}
|
|
2242
|
+
// TODO: whitespace-only headers are treated incosistently.
|
|
2243
|
+
// curl -H 'Hosts: ' example.com sends the header
|
|
2244
|
+
// curl -H 'User-Agent: ' example.com doesn't
|
|
2245
|
+
const hasValue = value && value.trim().toBool();
|
|
2246
|
+
const headerValue = hasValue ? value.removeFirstChar(" ") : null;
|
|
2247
|
+
headers.push([name, headerValue]);
|
|
2248
|
+
}
|
|
2249
|
+
else if (header.includes(";")) {
|
|
2250
|
+
const [name] = header.split(";", 2);
|
|
2251
|
+
headers.push([name, new Word()]);
|
|
2252
|
+
}
|
|
2253
|
+
else ;
|
|
2254
|
+
}
|
|
2255
|
+
}
|
|
2256
|
+
this.lowercase =
|
|
2257
|
+
headers.length > 0 && headers.every((h) => eq(h[0], h[0].toLowerCase()));
|
|
2258
|
+
// Handle repeated headers
|
|
2259
|
+
// For Cookie and Accept, merge the values using ';' and ',' respectively
|
|
2260
|
+
// For other headers, warn about the repeated header
|
|
2261
|
+
const uniqueHeaders = {};
|
|
2262
|
+
for (const [name, value] of headers) {
|
|
2263
|
+
// TODO: something better, at least warn that variable is ignored
|
|
2264
|
+
const lowerName = name.toLowerCase().toString();
|
|
2265
|
+
if (!uniqueHeaders[lowerName]) {
|
|
2266
|
+
uniqueHeaders[lowerName] = [];
|
|
2267
|
+
}
|
|
2268
|
+
uniqueHeaders[lowerName].push([name, value]);
|
|
2269
|
+
}
|
|
2270
|
+
headers = [];
|
|
2271
|
+
for (const [lowerName, repeatedHeaders] of Object.entries(uniqueHeaders)) {
|
|
2272
|
+
if (repeatedHeaders.length === 1) {
|
|
2273
|
+
headers.push(repeatedHeaders[0]);
|
|
2274
|
+
continue;
|
|
2275
|
+
}
|
|
2276
|
+
// If they're all null, just use the first one
|
|
2277
|
+
if (repeatedHeaders.every((h) => h[1] === null)) {
|
|
2278
|
+
const lastRepeat = repeatedHeaders[repeatedHeaders.length - 1];
|
|
2279
|
+
// Warn users if some are capitalized differently
|
|
2280
|
+
if (new Set(repeatedHeaders.map((h) => h[0])).size > 1) {
|
|
2281
|
+
warnings.push([
|
|
2282
|
+
"repeated-header",
|
|
2283
|
+
`"${lastRepeat[0]}" header unset ${repeatedHeaders.length} times`,
|
|
2284
|
+
]);
|
|
2285
|
+
}
|
|
2286
|
+
headers.push(lastRepeat);
|
|
2287
|
+
continue;
|
|
2288
|
+
}
|
|
2289
|
+
// Otherwise there's at least one non-null value, so we can ignore the nulls
|
|
2290
|
+
// TODO: if the values of the repeated headers are the same, just use the first one
|
|
2291
|
+
// 'content-type': 'application/json; application/json',
|
|
2292
|
+
// doesn't really make sense
|
|
2293
|
+
const nonEmptyHeaders = repeatedHeaders.filter((h) => h[1] !== null);
|
|
2294
|
+
if (nonEmptyHeaders.length === 1) {
|
|
2295
|
+
headers.push(nonEmptyHeaders[0]);
|
|
2296
|
+
continue;
|
|
2297
|
+
}
|
|
2298
|
+
let mergeChar = "";
|
|
2299
|
+
if (COMMA_SEPARATED.has(lowerName)) {
|
|
2300
|
+
mergeChar = ", ";
|
|
2301
|
+
}
|
|
2302
|
+
else if (SEMICOLON_SEPARATED.has(lowerName)) {
|
|
2303
|
+
mergeChar = "; ";
|
|
2304
|
+
}
|
|
2305
|
+
if (mergeChar) {
|
|
2306
|
+
const merged = joinWords(nonEmptyHeaders.map((h) => h[1]), mergeChar);
|
|
2307
|
+
warnings.push([
|
|
2308
|
+
"repeated-header",
|
|
2309
|
+
`merged ${nonEmptyHeaders.length} "${nonEmptyHeaders[nonEmptyHeaders.length - 1][0]}" headers together with "${mergeChar.trim()}"`,
|
|
2310
|
+
]);
|
|
2311
|
+
headers.push([nonEmptyHeaders[0][0], merged]);
|
|
2312
|
+
continue;
|
|
2313
|
+
}
|
|
2314
|
+
warnings.push([
|
|
2315
|
+
"repeated-header",
|
|
2316
|
+
`found ${nonEmptyHeaders.length} "${nonEmptyHeaders[nonEmptyHeaders.length - 1][0]}" headers, only the last one will be sent`,
|
|
2317
|
+
]);
|
|
2318
|
+
headers = headers.concat(nonEmptyHeaders);
|
|
2319
|
+
}
|
|
2320
|
+
this.headers = headers;
|
|
2321
|
+
}
|
|
2322
|
+
get length() {
|
|
2323
|
+
return this.headers.length;
|
|
2324
|
+
}
|
|
2325
|
+
*[Symbol.iterator]() {
|
|
2326
|
+
for (const h of this.headers) {
|
|
2327
|
+
yield h;
|
|
2328
|
+
}
|
|
2329
|
+
}
|
|
2330
|
+
// Gets the first header, matching case-insensitively
|
|
2331
|
+
get(header) {
|
|
2332
|
+
const lookup = header.toLowerCase();
|
|
2333
|
+
for (const [h, v] of this.headers) {
|
|
2334
|
+
if (h.toLowerCase().toString() === lookup) {
|
|
2335
|
+
return v;
|
|
2336
|
+
}
|
|
2337
|
+
}
|
|
2338
|
+
return undefined;
|
|
2339
|
+
}
|
|
2340
|
+
getContentType() {
|
|
2341
|
+
const contentTypeHeader = this.get("content-type");
|
|
2342
|
+
if (!contentTypeHeader) {
|
|
2343
|
+
return contentTypeHeader;
|
|
2344
|
+
}
|
|
2345
|
+
return contentTypeHeader.split(";")[0].trim().toString();
|
|
2346
|
+
}
|
|
2347
|
+
has(header) {
|
|
2348
|
+
const lookup = header.toLowerCase();
|
|
2349
|
+
for (const h of this.headers) {
|
|
2350
|
+
if (eq(h[0].toLowerCase(), lookup)) {
|
|
2351
|
+
return true;
|
|
2352
|
+
}
|
|
2353
|
+
}
|
|
2354
|
+
return false;
|
|
2355
|
+
}
|
|
2356
|
+
// Doesn't overwrite existing headers
|
|
2357
|
+
setIfMissing(header, value) {
|
|
2358
|
+
if (this.has(header)) {
|
|
2359
|
+
return false;
|
|
2360
|
+
}
|
|
2361
|
+
if (this.lowercase) {
|
|
2362
|
+
header = header.toLowerCase();
|
|
2363
|
+
}
|
|
2364
|
+
const k = typeof header === "string" ? new Word(header) : header;
|
|
2365
|
+
const v = typeof value === "string" ? new Word(value) : value;
|
|
2366
|
+
this.headers.push([k, v]);
|
|
2367
|
+
return true;
|
|
2368
|
+
}
|
|
2369
|
+
prependIfMissing(header, value) {
|
|
2370
|
+
if (this.has(header)) {
|
|
2371
|
+
return false;
|
|
2372
|
+
}
|
|
2373
|
+
if (this.lowercase) {
|
|
2374
|
+
header = header.toLowerCase();
|
|
2375
|
+
}
|
|
2376
|
+
const k = typeof header === "string" ? new Word(header) : header;
|
|
2377
|
+
const v = typeof value === "string" ? new Word(value) : value;
|
|
2378
|
+
this.headers.unshift([k, v]);
|
|
2379
|
+
return true;
|
|
2380
|
+
}
|
|
2381
|
+
set(header, value) {
|
|
2382
|
+
if (this.lowercase) {
|
|
2383
|
+
header = header.toLowerCase();
|
|
2384
|
+
}
|
|
2385
|
+
const k = typeof header === "string" ? new Word(header) : header;
|
|
2386
|
+
const v = typeof value === "string" ? new Word(value) : value;
|
|
2387
|
+
// keep it in the same place if we overwrite
|
|
2388
|
+
const searchHeader = k.toLowerCase().toString();
|
|
2389
|
+
for (let i = 0; i < this.headers.length; i++) {
|
|
2390
|
+
if (eq(this.headers[i][0].toLowerCase(), searchHeader)) {
|
|
2391
|
+
this.headers[i][1] = v;
|
|
2392
|
+
return;
|
|
2393
|
+
}
|
|
2394
|
+
}
|
|
2395
|
+
this.headers.push([k, v]);
|
|
2396
|
+
}
|
|
2397
|
+
delete(header) {
|
|
2398
|
+
const lookup = header.toLowerCase();
|
|
2399
|
+
for (let i = this.headers.length - 1; i >= 0; i--) {
|
|
2400
|
+
if (this.headers[i][0].toLowerCase().toString() === lookup) {
|
|
2401
|
+
this.headers.splice(i, 1);
|
|
2402
|
+
}
|
|
2403
|
+
}
|
|
2404
|
+
}
|
|
2405
|
+
// TODO: doesn't this skip the next element after deleting?
|
|
2406
|
+
clearNulls() {
|
|
2407
|
+
for (let i = this.headers.length - 1; i >= 0; i--) {
|
|
2408
|
+
if (this.headers[i][1] === null) {
|
|
2409
|
+
this.headers.splice(i, 1);
|
|
2410
|
+
}
|
|
2411
|
+
}
|
|
2412
|
+
}
|
|
2413
|
+
// TODO: shouldn't be used
|
|
2414
|
+
count(header) {
|
|
2415
|
+
let count = 0;
|
|
2416
|
+
const lookup = header.toLowerCase();
|
|
2417
|
+
for (const h of this.headers || []) {
|
|
2418
|
+
if (h[0].toLowerCase().toString() === lookup) {
|
|
2419
|
+
count += 1;
|
|
2420
|
+
}
|
|
2421
|
+
}
|
|
2422
|
+
return count;
|
|
2423
|
+
}
|
|
2424
|
+
toBool() {
|
|
2425
|
+
return this.headers.length > 0 && this.headers.some((h) => h[1] !== null);
|
|
2426
|
+
}
|
|
2427
|
+
}
|
|
2428
|
+
function parseCookiesStrict(cookieString) {
|
|
2429
|
+
const cookies = [];
|
|
2430
|
+
for (let cookie of cookieString.split(";")) {
|
|
2431
|
+
cookie = cookie.replace(/^ /, "");
|
|
2432
|
+
const [name, value] = cookie.split("=", 2);
|
|
2433
|
+
if (value === undefined) {
|
|
2434
|
+
return null;
|
|
2435
|
+
}
|
|
2436
|
+
cookies.push([name, value]);
|
|
2437
|
+
}
|
|
2438
|
+
if (new Set(cookies.map((c) => c[0])).size !== cookies.length) {
|
|
2439
|
+
return null;
|
|
2440
|
+
}
|
|
2441
|
+
return cookies;
|
|
2442
|
+
}
|
|
2443
|
+
function parseCookies(cookieString) {
|
|
2444
|
+
const cookies = [];
|
|
2445
|
+
for (let cookie of cookieString.split(";")) {
|
|
2446
|
+
cookie = cookie.trim();
|
|
2447
|
+
if (!cookie) {
|
|
2448
|
+
continue;
|
|
2449
|
+
}
|
|
2450
|
+
const [name, value] = cookie.split("=", 2);
|
|
2451
|
+
cookies.push([name.trim(), (value || "").trim()]);
|
|
2452
|
+
}
|
|
2453
|
+
if (new Set(cookies.map((c) => c[0])).size !== cookies.length) {
|
|
2454
|
+
return null;
|
|
2455
|
+
}
|
|
2456
|
+
return cookies;
|
|
2457
|
+
}
|
|
2458
|
+
|
|
2459
|
+
// https://github.com/curl/curl/blob/curl-7_88_1/src/tool_urlglob.c#L327
|
|
2460
|
+
const MAX_IP6LEN = 128;
|
|
2461
|
+
function isIpv6(glob) {
|
|
2462
|
+
if (glob.length > MAX_IP6LEN) {
|
|
2463
|
+
return false;
|
|
2464
|
+
}
|
|
2465
|
+
// TODO: curl tries to parse the glob as a hostname.
|
|
2466
|
+
return !glob.includes("-");
|
|
2467
|
+
}
|
|
2468
|
+
function warnAboutGlobs(global, url) {
|
|
2469
|
+
// Find any glob expressions in the URL and underline them
|
|
2470
|
+
let prev = "";
|
|
2471
|
+
for (let i = 0; i < url.length; i++) {
|
|
2472
|
+
const cur = url[i];
|
|
2473
|
+
if (cur === "[" && prev !== "\\") {
|
|
2474
|
+
let j = i + 1;
|
|
2475
|
+
while (j < url.length && url[j] !== "]") {
|
|
2476
|
+
j++;
|
|
2477
|
+
}
|
|
2478
|
+
if (j < url.length && url[j] === "]") {
|
|
2479
|
+
const glob = url.slice(i, j + 1);
|
|
2480
|
+
// could be ipv6 address
|
|
2481
|
+
if (!isIpv6(glob)) {
|
|
2482
|
+
warnf(global, [
|
|
2483
|
+
"glob-in-url",
|
|
2484
|
+
`globs in the URL are not supported:\n` +
|
|
2485
|
+
`${url}\n` +
|
|
2486
|
+
" ".repeat(i) +
|
|
2487
|
+
"^".repeat(glob.length),
|
|
2488
|
+
]);
|
|
2489
|
+
}
|
|
2490
|
+
prev = "";
|
|
2491
|
+
}
|
|
2492
|
+
else {
|
|
2493
|
+
// No closing bracket
|
|
2494
|
+
warnf(global, [
|
|
2495
|
+
"unbalanced-glob",
|
|
2496
|
+
"bracket doesn't have a closing bracket:\n" +
|
|
2497
|
+
`${url}\n` +
|
|
2498
|
+
`${" ".repeat(i)}^`,
|
|
2499
|
+
]);
|
|
2500
|
+
return; // malformed URL, stop looking for globs
|
|
2501
|
+
}
|
|
2502
|
+
}
|
|
2503
|
+
else if (cur === "{" && prev !== "\\") {
|
|
2504
|
+
let j = i + 1;
|
|
2505
|
+
while (j < url.length && url[j] !== "}") {
|
|
2506
|
+
j++;
|
|
2507
|
+
}
|
|
2508
|
+
if (j < url.length && url[j] === "}") {
|
|
2509
|
+
const glob = url.slice(i, j + 1);
|
|
2510
|
+
warnf(global, [
|
|
2511
|
+
"glob-in-url",
|
|
2512
|
+
`globs in the URL are not supported:\n` +
|
|
2513
|
+
`${url}\n` +
|
|
2514
|
+
" ".repeat(i) +
|
|
2515
|
+
"^".repeat(glob.length),
|
|
2516
|
+
]);
|
|
2517
|
+
prev = "";
|
|
2518
|
+
}
|
|
2519
|
+
else {
|
|
2520
|
+
// No closing bracket
|
|
2521
|
+
warnf(global, [
|
|
2522
|
+
"unbalanced-glob",
|
|
2523
|
+
"bracket doesn't have a closing bracket:\n" +
|
|
2524
|
+
`${url}\n` +
|
|
2525
|
+
`${" ".repeat(i)}^`,
|
|
2526
|
+
]);
|
|
2527
|
+
return; // malformed URL, stop looking for globs
|
|
2528
|
+
}
|
|
2529
|
+
}
|
|
2530
|
+
prev = cur;
|
|
2531
|
+
}
|
|
2532
|
+
}
|
|
2533
|
+
function parseurl(global, config, url) {
|
|
2534
|
+
var _a;
|
|
2535
|
+
// This is curl's parseurl()
|
|
2536
|
+
// https://github.com/curl/curl/blob/curl-7_85_0/lib/urlapi.c#L1144
|
|
2537
|
+
// Except we want to accept all URLs.
|
|
2538
|
+
// curl further validates URLs in curl_url_get()
|
|
2539
|
+
// https://github.com/curl/curl/blob/curl-7_86_0/lib/urlapi.c#L1374
|
|
2540
|
+
const u = {
|
|
2541
|
+
scheme: new Word(),
|
|
2542
|
+
host: new Word(),
|
|
2543
|
+
port: new Word(),
|
|
2544
|
+
path: new Word(),
|
|
2545
|
+
query: new Word(),
|
|
2546
|
+
fragment: new Word(), // with leading '#'
|
|
2547
|
+
};
|
|
2548
|
+
// Remove url glob escapes
|
|
2549
|
+
// https://github.com/curl/curl/blob/curl-7_87_0/src/tool_urlglob.c#L395-L398
|
|
2550
|
+
if (!config.globoff) {
|
|
2551
|
+
if (url.isString()) {
|
|
2552
|
+
warnAboutGlobs(global, url.toString());
|
|
2553
|
+
}
|
|
2554
|
+
url = url.replace(/\\([[\]{}])/g, "$1");
|
|
2555
|
+
}
|
|
2556
|
+
// Prepend "http"/"https" if the scheme is missing.
|
|
2557
|
+
// RFC 3986 3.1 says
|
|
2558
|
+
// scheme = ALPHA *( ALPHA / DIGIT / "+" / "-" / "." )
|
|
2559
|
+
// but curl will accept a digit/plus/minus/dot in the first character
|
|
2560
|
+
// curl will also accept a url with one / like http:/localhost
|
|
2561
|
+
// https://github.com/curl/curl/blob/curl-7_85_0/lib/urlapi.c#L960
|
|
2562
|
+
let schemeMatch = null;
|
|
2563
|
+
if (url.tokens.length && typeof url.tokens[0] === "string") {
|
|
2564
|
+
schemeMatch = url.tokens[0].match(/^([a-zA-Z0-9+-.]*):\/\/*/);
|
|
2565
|
+
}
|
|
2566
|
+
if (schemeMatch) {
|
|
2567
|
+
const [schemeAndSlashes, scheme] = schemeMatch;
|
|
2568
|
+
u.scheme = new Word(scheme.toLowerCase());
|
|
2569
|
+
url = url.slice(schemeAndSlashes.length);
|
|
2570
|
+
}
|
|
2571
|
+
else {
|
|
2572
|
+
// curl defaults to https://
|
|
2573
|
+
// we don't because most libraries won't downgrade to http
|
|
2574
|
+
// if you ask for https, unlike curl.
|
|
2575
|
+
// TODO: handle file:// scheme
|
|
2576
|
+
u.scheme = (_a = config["proto-default"]) !== null && _a !== void 0 ? _a : new Word("http");
|
|
2577
|
+
}
|
|
2578
|
+
if (!eq(u.scheme, "http") && !eq(u.scheme, "https")) {
|
|
2579
|
+
warnf(global, ["bad-scheme", `Protocol "${u.scheme}" not supported`]);
|
|
2580
|
+
}
|
|
2581
|
+
// https://github.com/curl/curl/blob/curl-7_85_0/lib/urlapi.c#L992
|
|
2582
|
+
const hostMatch = url.indexOfFirstChar("/?#");
|
|
2583
|
+
if (hostMatch !== -1) {
|
|
2584
|
+
u.host = url.slice(0, hostMatch);
|
|
2585
|
+
// TODO: u.path might end up empty if indexOfFirstChar found ?#
|
|
2586
|
+
u.path = url.slice(hostMatch); // keep leading '/' in .path
|
|
2587
|
+
// https://github.com/curl/curl/blob/curl-7_85_0/lib/urlapi.c#L1024
|
|
2588
|
+
const fragmentIndex = u.path.indexOf("#");
|
|
2589
|
+
const queryIndex = u.path.indexOf("?");
|
|
2590
|
+
if (fragmentIndex !== -1) {
|
|
2591
|
+
u.fragment = u.path.slice(fragmentIndex);
|
|
2592
|
+
if (queryIndex !== -1 && queryIndex < fragmentIndex) {
|
|
2593
|
+
u.query = u.path.slice(queryIndex, fragmentIndex);
|
|
2594
|
+
u.path = u.path.slice(0, queryIndex);
|
|
2595
|
+
}
|
|
2596
|
+
else {
|
|
2597
|
+
u.path = u.path.slice(0, fragmentIndex);
|
|
2598
|
+
}
|
|
2599
|
+
}
|
|
2600
|
+
else if (queryIndex !== -1) {
|
|
2601
|
+
u.query = u.path.slice(queryIndex);
|
|
2602
|
+
u.path = u.path.slice(0, queryIndex);
|
|
2603
|
+
}
|
|
2604
|
+
}
|
|
2605
|
+
else {
|
|
2606
|
+
u.host = url;
|
|
2607
|
+
}
|
|
2608
|
+
// parse username:password@hostname
|
|
2609
|
+
// https://github.com/curl/curl/blob/curl-7_85_0/lib/urlapi.c#L1083
|
|
2610
|
+
// https://github.com/curl/curl/blob/curl-7_85_0/lib/urlapi.c#L460
|
|
2611
|
+
// https://github.com/curl/curl/blob/curl-7_85_0/lib/url.c#L2827
|
|
2612
|
+
const authMatch = u.host.indexOf("@");
|
|
2613
|
+
if (authMatch !== -1) {
|
|
2614
|
+
const auth = u.host.slice(0, authMatch);
|
|
2615
|
+
u.host = u.host.slice(authMatch + 1); // throw away '@'
|
|
2616
|
+
if (!config["disallow-username-in-url"]) {
|
|
2617
|
+
u.auth = auth;
|
|
2618
|
+
if (auth.includes(":")) {
|
|
2619
|
+
[u.user, u.password] = auth.split(":", 2);
|
|
2620
|
+
}
|
|
2621
|
+
else {
|
|
2622
|
+
u.user = auth;
|
|
2623
|
+
u.password = new Word(); // if there's no ':', curl will append it
|
|
2624
|
+
}
|
|
2625
|
+
}
|
|
2626
|
+
else {
|
|
2627
|
+
// Curl will exit if this is the case, but we just remove it from the URL
|
|
2628
|
+
warnf(global, [
|
|
2629
|
+
"login-denied",
|
|
2630
|
+
`Found auth in URL but --disallow-username-in-url was passed: ${auth.toString()}`,
|
|
2631
|
+
]);
|
|
2632
|
+
}
|
|
2633
|
+
}
|
|
2634
|
+
// TODO: need to extract port first
|
|
2635
|
+
// hostname_check()
|
|
2636
|
+
// https://github.com/curl/curl/blob/curl-7_86_0/lib/urlapi.c#L572
|
|
2637
|
+
// if (!u.host) {
|
|
2638
|
+
// warnf(global, [
|
|
2639
|
+
// "no-host",
|
|
2640
|
+
// "Found empty host in URL: " + JSON.stringify(url),
|
|
2641
|
+
// ]);
|
|
2642
|
+
// } else if (u.host.startsWith("[")) {
|
|
2643
|
+
// if (!u.host.endsWith("]")) {
|
|
2644
|
+
// warnf(global, [
|
|
2645
|
+
// "bad-host",
|
|
2646
|
+
// "Found invalid IPv6 address in URL: " + JSON.stringify(url),
|
|
2647
|
+
// ]);
|
|
2648
|
+
// } else {
|
|
2649
|
+
// const firstWeirdCharacter = u.host.match(/[^0123456789abcdefABCDEF:.]/);
|
|
2650
|
+
// // %zone_id
|
|
2651
|
+
// if (firstWeirdCharacter && firstWeirdCharacter[0] !== "%") {
|
|
2652
|
+
// warnf(global, [
|
|
2653
|
+
// "bad-host",
|
|
2654
|
+
// "Found invalid IPv6 address in URL: " + JSON.stringify(url),
|
|
2655
|
+
// ]);
|
|
2656
|
+
// }
|
|
2657
|
+
// }
|
|
2658
|
+
// } else {
|
|
2659
|
+
// const firstInvalidCharacter = u.host.match(
|
|
2660
|
+
// /[\r\n\t/:#?!@{}[\]\\$'"^`*<>=;,]/
|
|
2661
|
+
// );
|
|
2662
|
+
// if (firstInvalidCharacter) {
|
|
2663
|
+
// warnf(global, [
|
|
2664
|
+
// "bad-host",
|
|
2665
|
+
// "Found invalid character " +
|
|
2666
|
+
// JSON.stringify(firstInvalidCharacter[0]) +
|
|
2667
|
+
// " in URL: " +
|
|
2668
|
+
// JSON.stringify(url),
|
|
2669
|
+
// ]);
|
|
2670
|
+
// }
|
|
2671
|
+
// }
|
|
2672
|
+
return u;
|
|
2673
|
+
}
|
|
2674
|
+
|
|
2675
|
+
// Match Python's urllib.parse.quote() behavior
|
|
2676
|
+
// https://github.com/python/cpython/blob/3.11/Lib/urllib/parse.py#L826
|
|
2677
|
+
// curl and Python let you send non-ASCII characters by encoding each UTF-8 byte.
|
|
2678
|
+
// TODO: ignore hex case?
|
|
2679
|
+
function _percentEncode(s) {
|
|
2680
|
+
return [...UTF8encoder.encode(s)]
|
|
2681
|
+
.map((b) => {
|
|
2682
|
+
if (
|
|
2683
|
+
// A-Z
|
|
2684
|
+
(b >= 0x41 && b <= 0x5a) ||
|
|
2685
|
+
// a-z
|
|
2686
|
+
(b >= 0x61 && b <= 0x7a) ||
|
|
2687
|
+
// 0-9
|
|
2688
|
+
(b >= 0x30 && b <= 0x39) ||
|
|
2689
|
+
// -._~
|
|
2690
|
+
b === 0x2d ||
|
|
2691
|
+
b === 0x2e ||
|
|
2692
|
+
b === 0x5f ||
|
|
2693
|
+
b === 0x7e) {
|
|
2694
|
+
return String.fromCharCode(b);
|
|
2695
|
+
}
|
|
2696
|
+
return "%" + b.toString(16).toUpperCase().padStart(2, "0");
|
|
2697
|
+
})
|
|
2698
|
+
.join("");
|
|
2699
|
+
}
|
|
2700
|
+
function percentEncode(s) {
|
|
2701
|
+
const newTokens = [];
|
|
2702
|
+
for (const token of s.tokens) {
|
|
2703
|
+
if (typeof token === "string") {
|
|
2704
|
+
newTokens.push(_percentEncode(token));
|
|
2705
|
+
}
|
|
2706
|
+
else {
|
|
2707
|
+
newTokens.push(token);
|
|
2708
|
+
}
|
|
2709
|
+
}
|
|
2710
|
+
return new Word(newTokens);
|
|
2711
|
+
}
|
|
2712
|
+
function percentEncodePlus(s) {
|
|
2713
|
+
const newTokens = [];
|
|
2714
|
+
for (const token of s.tokens) {
|
|
2715
|
+
if (typeof token === "string") {
|
|
2716
|
+
newTokens.push(_percentEncode(token).replace(/%20/g, "+"));
|
|
2717
|
+
}
|
|
2718
|
+
else {
|
|
2719
|
+
newTokens.push(token);
|
|
2720
|
+
}
|
|
2721
|
+
}
|
|
2722
|
+
return new Word(newTokens);
|
|
2723
|
+
}
|
|
2724
|
+
// Reimplements decodeURIComponent but ignores variables/commands
|
|
2725
|
+
function wordDecodeURIComponent(s) {
|
|
2726
|
+
const newTokens = [];
|
|
2727
|
+
for (const token of s.tokens) {
|
|
2728
|
+
if (typeof token === "string") {
|
|
2729
|
+
newTokens.push(decodeURIComponent(token));
|
|
2730
|
+
}
|
|
2731
|
+
else {
|
|
2732
|
+
newTokens.push(token);
|
|
2733
|
+
}
|
|
2734
|
+
}
|
|
2735
|
+
return new Word(newTokens);
|
|
2736
|
+
}
|
|
2737
|
+
// if url is 'example.com?' the s is ''
|
|
2738
|
+
// if url is 'example.com' the s is null
|
|
2739
|
+
function parseQueryString(s) {
|
|
2740
|
+
if (!s || s.isEmpty()) {
|
|
2741
|
+
return [null, null];
|
|
2742
|
+
}
|
|
2743
|
+
const asList = [];
|
|
2744
|
+
for (const param of s.split("&")) {
|
|
2745
|
+
// Most software libraries don't let you distinguish between a=&b= and a&b,
|
|
2746
|
+
// so if we get an `a&b`-type query string, don't bother.
|
|
2747
|
+
if (!param.includes("=")) {
|
|
2748
|
+
return [null, null];
|
|
2749
|
+
}
|
|
2750
|
+
const [key, val] = param.split("=", 2);
|
|
2751
|
+
let decodedKey;
|
|
2752
|
+
let decodedVal;
|
|
2753
|
+
try {
|
|
2754
|
+
// https://url.spec.whatwg.org/#urlencoded-parsing
|
|
2755
|
+
// recommends replacing + with space before decoding.
|
|
2756
|
+
decodedKey = wordDecodeURIComponent(key.replace(/\+/g, " "));
|
|
2757
|
+
decodedVal = wordDecodeURIComponent(val.replace(/\+/g, " "));
|
|
2758
|
+
}
|
|
2759
|
+
catch (e) {
|
|
2760
|
+
if (e instanceof URIError) {
|
|
2761
|
+
// Query string contains invalid percent encoded characters,
|
|
2762
|
+
// we cannot properly convert it.
|
|
2763
|
+
return [null, null];
|
|
2764
|
+
}
|
|
2765
|
+
throw e;
|
|
2766
|
+
}
|
|
2767
|
+
// If the query string doesn't round-trip, we cannot properly convert it.
|
|
2768
|
+
// TODO: this is a bit Python-specific, ideally we would check how each runtime/library
|
|
2769
|
+
// percent-encodes query strings. For example, a %27 character in the input query
|
|
2770
|
+
// string will be decoded to a ' but won't be re-encoded into a %27 by encodeURIComponent
|
|
2771
|
+
const roundTripKey = percentEncode(decodedKey);
|
|
2772
|
+
const roundTripVal = percentEncode(decodedVal);
|
|
2773
|
+
// If the original data used %20 instead of + (what requests will send), that's close enough
|
|
2774
|
+
if ((!eq(roundTripKey, key) && !eq(roundTripKey.replace(/%20/g, "+"), key)) ||
|
|
2775
|
+
(!eq(roundTripVal, val) && !eq(roundTripVal.replace(/%20/g, "+"), val))) {
|
|
2776
|
+
return [null, null];
|
|
2777
|
+
}
|
|
2778
|
+
asList.push([decodedKey, decodedVal]);
|
|
2779
|
+
}
|
|
2780
|
+
// Group keys
|
|
2781
|
+
const keyWords = {};
|
|
2782
|
+
const uniqueKeys = {};
|
|
2783
|
+
let prevKey = null;
|
|
2784
|
+
for (const [key, val] of asList) {
|
|
2785
|
+
const keyStr = key.toString(); // TODO: do this better
|
|
2786
|
+
if (prevKey === keyStr) {
|
|
2787
|
+
uniqueKeys[keyStr].push(val);
|
|
2788
|
+
}
|
|
2789
|
+
else if (!Object.prototype.hasOwnProperty.call(uniqueKeys, keyStr)) {
|
|
2790
|
+
uniqueKeys[keyStr] = [val];
|
|
2791
|
+
keyWords[keyStr] = key;
|
|
2792
|
+
}
|
|
2793
|
+
else {
|
|
2794
|
+
// If there's a repeated key with a different key between
|
|
2795
|
+
// one of its repetitions, there is no way to represent
|
|
2796
|
+
// this query string as a dictionary.
|
|
2797
|
+
return [asList, null];
|
|
2798
|
+
}
|
|
2799
|
+
prevKey = keyStr;
|
|
2800
|
+
}
|
|
2801
|
+
// Convert lists with 1 element to the element
|
|
2802
|
+
const asDict = [];
|
|
2803
|
+
for (const [keyStr, val] of Object.entries(uniqueKeys)) {
|
|
2804
|
+
asDict.push([keyWords[keyStr], val.length === 1 ? val[0] : val]);
|
|
2805
|
+
}
|
|
2806
|
+
return [asList, asDict];
|
|
2807
|
+
}
|
|
2808
|
+
|
|
2809
|
+
function parseDetails(formParam, p, ptr, supported, warnings) {
|
|
2810
|
+
while (ptr < p.length && p.charAt(ptr) === ";") {
|
|
2811
|
+
ptr += 1;
|
|
2812
|
+
while (ptr < p.length && isSpace(p.charAt(ptr))) {
|
|
2813
|
+
ptr += 1;
|
|
2814
|
+
}
|
|
2815
|
+
if (ptr >= p.length) {
|
|
2816
|
+
return formParam;
|
|
2817
|
+
}
|
|
2818
|
+
const value = p.slice(ptr);
|
|
2819
|
+
if (value.startsWith("type=")) {
|
|
2820
|
+
// TODO: the syntax for type= is more complicated
|
|
2821
|
+
[formParam.contentType, ptr] = getParamWord(p, ptr + 5, warnings);
|
|
2822
|
+
}
|
|
2823
|
+
else if (value.startsWith("filename=")) {
|
|
2824
|
+
const [filename, filenameEnd] = getParamWord(p, ptr + 9, warnings);
|
|
2825
|
+
ptr = filenameEnd;
|
|
2826
|
+
if (supported.filename) {
|
|
2827
|
+
formParam.filename = filename;
|
|
2828
|
+
}
|
|
2829
|
+
else {
|
|
2830
|
+
warnings.push([
|
|
2831
|
+
"unsupported-form-detail",
|
|
2832
|
+
"Field file name not allowed here: " + filename.toString(),
|
|
2833
|
+
]);
|
|
2834
|
+
}
|
|
2835
|
+
}
|
|
2836
|
+
else if (value.startsWith("encoder=")) {
|
|
2837
|
+
const [encoder, encoderEnd] = getParamWord(p, ptr + 8, warnings);
|
|
2838
|
+
ptr = encoderEnd;
|
|
2839
|
+
if (supported.encoder) {
|
|
2840
|
+
formParam.encoder = encoder;
|
|
2841
|
+
}
|
|
2842
|
+
else {
|
|
2843
|
+
warnings.push([
|
|
2844
|
+
"unsupported-form-detail",
|
|
2845
|
+
"Field encoder not allowed here: " + encoder.toString(),
|
|
2846
|
+
]);
|
|
2847
|
+
}
|
|
2848
|
+
}
|
|
2849
|
+
else if (value.startsWith("headers=")) {
|
|
2850
|
+
// TODO: more complicated because of header files
|
|
2851
|
+
const [headers, headersEnd] = getParamWord(p, ptr + 8, warnings);
|
|
2852
|
+
ptr = headersEnd;
|
|
2853
|
+
if (supported.headers) {
|
|
2854
|
+
formParam.headers = headers;
|
|
2855
|
+
}
|
|
2856
|
+
else {
|
|
2857
|
+
warnings.push([
|
|
2858
|
+
"unsupported-form-detail",
|
|
2859
|
+
"Field headers not allowed here: " + headers.toString(),
|
|
2860
|
+
]);
|
|
2861
|
+
}
|
|
2862
|
+
}
|
|
2863
|
+
else {
|
|
2864
|
+
// TODO: it would be more consistent for curl to skip until the first "=", then
|
|
2865
|
+
// getParamWord, because quoting a ; in an unknown value breaks values that
|
|
2866
|
+
// come after it, e.g.:
|
|
2867
|
+
// curl -F 'myname=myvalue;bfilename="f;oo";filename=oeu' localhost:8888
|
|
2868
|
+
const unknown = getParamWord(p, ptr, warnings);
|
|
2869
|
+
const unknownEnd = unknown[1];
|
|
2870
|
+
ptr = unknownEnd;
|
|
2871
|
+
warnings.push([
|
|
2872
|
+
"unknown-form-detail",
|
|
2873
|
+
"skip unknown form field: " + value.toString(),
|
|
2874
|
+
]);
|
|
2875
|
+
}
|
|
2876
|
+
}
|
|
2877
|
+
return formParam;
|
|
2878
|
+
}
|
|
2879
|
+
function isSpace(c) {
|
|
2880
|
+
// Implements the following macro from curl:
|
|
2881
|
+
// #define ISBLANK(x) (((x) == ' ') || ((x) == '\t'))
|
|
2882
|
+
// #define ISSPACE(x) (ISBLANK(x) || (((x) >= 0xa) && ((x) <= 0x0d)))
|
|
2883
|
+
return (typeof c === "string" &&
|
|
2884
|
+
(c === " " || c === "\t" || (c >= "\n" && c <= "\r")));
|
|
2885
|
+
}
|
|
2886
|
+
function getParamWord(p, start, warnings) {
|
|
2887
|
+
let ptr = start;
|
|
2888
|
+
if (p.charAt(ptr) === '"') {
|
|
2889
|
+
ptr += 1;
|
|
2890
|
+
const parts = [];
|
|
2891
|
+
while (ptr < p.length) {
|
|
2892
|
+
let curChar = p.charAt(ptr);
|
|
2893
|
+
if (curChar === "\\") {
|
|
2894
|
+
if (ptr + 1 < p.length) {
|
|
2895
|
+
const nextChar = p.charAt(ptr + 1);
|
|
2896
|
+
if (nextChar === '"' || nextChar === "\\") {
|
|
2897
|
+
ptr += 1;
|
|
2898
|
+
curChar = p.charAt(ptr);
|
|
2899
|
+
}
|
|
2900
|
+
}
|
|
2901
|
+
}
|
|
2902
|
+
else if (curChar === '"') {
|
|
2903
|
+
ptr += 1;
|
|
2904
|
+
let trailingData = false;
|
|
2905
|
+
while (ptr < p.length && p.charAt(ptr) !== ";") {
|
|
2906
|
+
if (!isSpace(p.charAt(ptr))) {
|
|
2907
|
+
trailingData = true;
|
|
2908
|
+
}
|
|
2909
|
+
ptr += 1;
|
|
2910
|
+
}
|
|
2911
|
+
if (trailingData) {
|
|
2912
|
+
warnings.push([
|
|
2913
|
+
"trailing-form-data",
|
|
2914
|
+
"Trailing data after quoted form parameter",
|
|
2915
|
+
]);
|
|
2916
|
+
}
|
|
2917
|
+
return [new Word(parts), ptr];
|
|
2918
|
+
}
|
|
2919
|
+
parts.push(curChar);
|
|
2920
|
+
ptr += 1;
|
|
2921
|
+
}
|
|
2922
|
+
}
|
|
2923
|
+
let sepIdx = p.indexOf(";", start);
|
|
2924
|
+
if (sepIdx === -1) {
|
|
2925
|
+
sepIdx = p.length;
|
|
2926
|
+
}
|
|
2927
|
+
return [p.slice(start, sepIdx), sepIdx];
|
|
2928
|
+
}
|
|
2929
|
+
function getParamPart(formParam, p, ptr, supported, warnings) {
|
|
2930
|
+
while (ptr < p.length && isSpace(p.charAt(ptr))) {
|
|
2931
|
+
ptr += 1;
|
|
2932
|
+
}
|
|
2933
|
+
const [content, contentEnd] = getParamWord(p, ptr, warnings);
|
|
2934
|
+
formParam.content = content;
|
|
2935
|
+
parseDetails(formParam, p, contentEnd, supported, warnings);
|
|
2936
|
+
return formParam;
|
|
2937
|
+
}
|
|
2938
|
+
// TODO: https://curl.se/docs/manpage.html#-F
|
|
2939
|
+
// https://github.com/curl/curl/blob/curl-7_88_1/src/tool_formparse.c
|
|
2940
|
+
// -F is a complicated option to parse.
|
|
2941
|
+
function parseForm(form, warnings) {
|
|
2942
|
+
const multipartUploads = [];
|
|
2943
|
+
let depth = 0;
|
|
2944
|
+
for (const multipartArgument of form) {
|
|
2945
|
+
const isString = multipartArgument.type === "string";
|
|
2946
|
+
if (!multipartArgument.value.includes("=")) {
|
|
2947
|
+
throw new CCError('invalid value for --form/-F, missing "=": ' +
|
|
2948
|
+
JSON.stringify(multipartArgument.value.toString()));
|
|
2949
|
+
}
|
|
2950
|
+
const [name, value] = multipartArgument.value.split("=", 2);
|
|
2951
|
+
const formParam = { name };
|
|
2952
|
+
if (!isString && value.charAt(0) === "(") {
|
|
2953
|
+
depth += 1;
|
|
2954
|
+
warnings.push([
|
|
2955
|
+
"nested-form",
|
|
2956
|
+
'Nested form data with "=(" is not supported, it will be flattened',
|
|
2957
|
+
]);
|
|
2958
|
+
getParamPart(formParam, value, 1, {
|
|
2959
|
+
headers: true,
|
|
2960
|
+
}, warnings);
|
|
2961
|
+
}
|
|
2962
|
+
else if (!isString && name.length === 0 && eq(value, ")")) {
|
|
2963
|
+
depth -= 1;
|
|
2964
|
+
if (depth < 0) {
|
|
2965
|
+
throw new CCError("no multipart to terminate: " +
|
|
2966
|
+
JSON.stringify(multipartArgument.value.toString()));
|
|
2967
|
+
}
|
|
2968
|
+
}
|
|
2969
|
+
else if (!isString && value.charAt(0) === "@") {
|
|
2970
|
+
// TODO: there can be multiple files separated by a comma
|
|
2971
|
+
getParamPart(formParam, value, 1, {
|
|
2972
|
+
filename: true,
|
|
2973
|
+
encoder: true,
|
|
2974
|
+
headers: true,
|
|
2975
|
+
}, warnings);
|
|
2976
|
+
formParam.contentFile = formParam.content;
|
|
2977
|
+
delete formParam.content;
|
|
2978
|
+
if (formParam.filename === null || formParam.filename === undefined) {
|
|
2979
|
+
formParam.filename = formParam.contentFile;
|
|
2980
|
+
}
|
|
2981
|
+
if (formParam.contentType === null ||
|
|
2982
|
+
formParam.contentType === undefined) ;
|
|
2983
|
+
}
|
|
2984
|
+
else if (!isString && value.charAt(0) === "<") {
|
|
2985
|
+
getParamPart(formParam, value, 1, {
|
|
2986
|
+
encoder: true,
|
|
2987
|
+
headers: true,
|
|
2988
|
+
}, warnings);
|
|
2989
|
+
formParam.contentFile = formParam.content;
|
|
2990
|
+
delete formParam.content;
|
|
2991
|
+
if (formParam.contentType === null ||
|
|
2992
|
+
formParam.contentType === undefined) ;
|
|
2993
|
+
}
|
|
2994
|
+
else {
|
|
2995
|
+
if (isString) {
|
|
2996
|
+
formParam.content = value;
|
|
2997
|
+
}
|
|
2998
|
+
else {
|
|
2999
|
+
getParamPart(formParam, value, 0, {
|
|
3000
|
+
filename: true,
|
|
3001
|
+
encoder: true,
|
|
3002
|
+
headers: true,
|
|
3003
|
+
}, warnings);
|
|
3004
|
+
}
|
|
3005
|
+
}
|
|
3006
|
+
multipartUploads.push(formParam);
|
|
3007
|
+
}
|
|
3008
|
+
return multipartUploads;
|
|
3009
|
+
}
|
|
3010
|
+
|
|
3011
|
+
function buildURL(global, config, url, uploadFile, outputFile, stdin, stdinFile) {
|
|
3012
|
+
const originalUrl = url;
|
|
3013
|
+
const u = parseurl(global, config, url);
|
|
3014
|
+
// https://github.com/curl/curl/blob/curl-7_85_0/src/tool_operate.c#L1124
|
|
3015
|
+
// https://github.com/curl/curl/blob/curl-7_85_0/src/tool_operhlp.c#L76
|
|
3016
|
+
if (uploadFile) {
|
|
3017
|
+
// TODO: it's more complicated
|
|
3018
|
+
if (u.path.isEmpty()) {
|
|
3019
|
+
u.path = uploadFile.prepend("/");
|
|
3020
|
+
}
|
|
3021
|
+
else if (u.path.endsWith("/")) {
|
|
3022
|
+
u.path = u.path.add(uploadFile);
|
|
3023
|
+
}
|
|
3024
|
+
if (config.get) {
|
|
3025
|
+
warnf(global, [
|
|
3026
|
+
"data-ignored",
|
|
3027
|
+
"curl doesn't let you pass --get and --upload-file together",
|
|
3028
|
+
]);
|
|
3029
|
+
}
|
|
3030
|
+
}
|
|
3031
|
+
const urlWithOriginalQuery = mergeWords(u.scheme, "://", u.host, u.path, u.query, u.fragment);
|
|
3032
|
+
// curl example.com example.com?foo=bar --url-query isshared=t
|
|
3033
|
+
// will make requests for
|
|
3034
|
+
// example.com/?isshared=t
|
|
3035
|
+
// example.com/?foo=bar&isshared=t
|
|
3036
|
+
//
|
|
3037
|
+
// so the query could come from
|
|
3038
|
+
// 1. `--url` (i.e. the requested URL)
|
|
3039
|
+
// 2. `--url-query` or `--get --data` (the latter takes precedence)
|
|
3040
|
+
//
|
|
3041
|
+
// If it comes from the latter, we might need to generate code to read
|
|
3042
|
+
// from one or more files.
|
|
3043
|
+
// When there's multiple urls, the latter applies to all of them
|
|
3044
|
+
// but the query from --url only applies to that URL.
|
|
3045
|
+
//
|
|
3046
|
+
// There's 3 cases for the query:
|
|
3047
|
+
// 1. it's well-formed and can be expressed as a list of tuples (or a dict)
|
|
3048
|
+
// `?one=1&one=1&two=2`
|
|
3049
|
+
// 2. it can't, for example because one of the pieces doesn't have a '='
|
|
3050
|
+
// `?one`
|
|
3051
|
+
// 3. we need to generate code that reads from a file
|
|
3052
|
+
//
|
|
3053
|
+
// If there's only one URL we merge the query from the URL with the shared part.
|
|
3054
|
+
//
|
|
3055
|
+
// If there's multiple URLs and a shared part that reads from a file (case 3),
|
|
3056
|
+
// we only write the file reading code once, pass it as the params= argument
|
|
3057
|
+
// and the part from the URL has to be passed as a string in the URL
|
|
3058
|
+
// and requests will combine the query in the URL with the query in params=.
|
|
3059
|
+
//
|
|
3060
|
+
// Otherwise, we print each query for each URL individually, either as a
|
|
3061
|
+
// list of tuples if we can or in the URL if we can't.
|
|
3062
|
+
//
|
|
3063
|
+
// When files are passed in through --data-urlencode or --url-query
|
|
3064
|
+
// we can usually treat them as case 1 as well (in Python), but that would
|
|
3065
|
+
// generate code slightly different from curl because curl reads the file once
|
|
3066
|
+
// upfront, whereas we would read the file multiple times and it might contain
|
|
3067
|
+
// different data each time (for example if it's /dev/urandom).
|
|
3068
|
+
let urlQueryArray = null;
|
|
3069
|
+
let queryArray = null;
|
|
3070
|
+
let queryStrReadsFile = null;
|
|
3071
|
+
if (u.query.toBool() || (config["url-query"] && config["url-query"].length)) {
|
|
3072
|
+
let queryStr = null;
|
|
3073
|
+
let queryParts = [];
|
|
3074
|
+
if (u.query.toBool()) {
|
|
3075
|
+
// remove the leading '?'
|
|
3076
|
+
queryParts.push(["raw", u.query.slice(1)]);
|
|
3077
|
+
[queryArray, queryStr, queryStrReadsFile] = buildData(queryParts, stdin, stdinFile);
|
|
3078
|
+
urlQueryArray = queryArray;
|
|
3079
|
+
}
|
|
3080
|
+
if (config["url-query"]) {
|
|
3081
|
+
queryParts = queryParts.concat(config["url-query"]);
|
|
3082
|
+
[queryArray, queryStr, queryStrReadsFile] = buildData(queryParts, stdin, stdinFile);
|
|
3083
|
+
}
|
|
3084
|
+
// TODO: check the curl source code
|
|
3085
|
+
// TODO: curl localhost:8888/?
|
|
3086
|
+
// will request /?
|
|
3087
|
+
// but
|
|
3088
|
+
// curl localhost:8888/? --url-query ''
|
|
3089
|
+
// (or --get --data '') will request /
|
|
3090
|
+
u.query = new Word();
|
|
3091
|
+
if (queryStr && queryStr.toBool()) {
|
|
3092
|
+
u.query = queryStr.prepend("?");
|
|
3093
|
+
}
|
|
3094
|
+
}
|
|
3095
|
+
const urlWithoutQueryArray = mergeWords(u.scheme, "://", u.host, u.path, u.fragment);
|
|
3096
|
+
url = mergeWords(u.scheme, "://", u.host, u.path, u.query, u.fragment);
|
|
3097
|
+
let urlWithoutQueryList = url;
|
|
3098
|
+
// TODO: parseQueryString() doesn't accept leading '?'
|
|
3099
|
+
let [queryList, queryDict] = parseQueryString(u.query.toBool() ? u.query.slice(1) : new Word());
|
|
3100
|
+
if (queryList && queryList.length) {
|
|
3101
|
+
// TODO: remove the fragment too?
|
|
3102
|
+
urlWithoutQueryList = mergeWords(u.scheme, "://", u.host, u.path, u.fragment);
|
|
3103
|
+
}
|
|
3104
|
+
else {
|
|
3105
|
+
queryList = null;
|
|
3106
|
+
queryDict = null;
|
|
3107
|
+
}
|
|
3108
|
+
// TODO: --path-as-is
|
|
3109
|
+
// TODO: --request-target
|
|
3110
|
+
// curl expects you to uppercase methods always. If you do -X PoSt, that's what it
|
|
3111
|
+
// will send, but most APIs will helpfully uppercase what you pass in as the method.
|
|
3112
|
+
//
|
|
3113
|
+
// There are many places where curl determines the method, this is the last one:
|
|
3114
|
+
// https://github.com/curl/curl/blob/curl-7_85_0/lib/http.c#L2032
|
|
3115
|
+
let method = new Word("GET");
|
|
3116
|
+
if (config.request &&
|
|
3117
|
+
// Safari adds `-X null` if it can't determine the request type
|
|
3118
|
+
// https://github.com/WebKit/WebKit/blob/f58ef38d48f42f5d7723691cb090823908ff5f9f/Source/WebInspectorUI/UserInterface/Models/Resource.js#L1250
|
|
3119
|
+
!eq(config.request, "null")) {
|
|
3120
|
+
method = config.request;
|
|
3121
|
+
}
|
|
3122
|
+
else if (config.head) {
|
|
3123
|
+
method = new Word("HEAD");
|
|
3124
|
+
}
|
|
3125
|
+
else if (uploadFile && uploadFile.toBool()) {
|
|
3126
|
+
// --upload-file '' doesn't do anything.
|
|
3127
|
+
method = new Word("PUT");
|
|
3128
|
+
}
|
|
3129
|
+
else if (!config.get && (has(config, "data") || has(config, "form"))) {
|
|
3130
|
+
method = new Word("POST");
|
|
3131
|
+
}
|
|
3132
|
+
const requestUrl = {
|
|
3133
|
+
originalUrl,
|
|
3134
|
+
urlWithoutQueryList,
|
|
3135
|
+
url,
|
|
3136
|
+
urlObj: u,
|
|
3137
|
+
urlWithOriginalQuery,
|
|
3138
|
+
urlWithoutQueryArray,
|
|
3139
|
+
method,
|
|
3140
|
+
};
|
|
3141
|
+
if (queryStrReadsFile) {
|
|
3142
|
+
requestUrl.queryReadsFile = queryStrReadsFile;
|
|
3143
|
+
}
|
|
3144
|
+
if (queryList) {
|
|
3145
|
+
requestUrl.queryList = queryList;
|
|
3146
|
+
if (queryDict) {
|
|
3147
|
+
requestUrl.queryDict = queryDict;
|
|
3148
|
+
}
|
|
3149
|
+
}
|
|
3150
|
+
if (queryArray) {
|
|
3151
|
+
requestUrl.queryArray = queryArray;
|
|
3152
|
+
}
|
|
3153
|
+
if (urlQueryArray) {
|
|
3154
|
+
requestUrl.urlQueryArray = urlQueryArray;
|
|
3155
|
+
}
|
|
3156
|
+
if (uploadFile) {
|
|
3157
|
+
if (eq(uploadFile, "-") || eq(uploadFile, ".")) {
|
|
3158
|
+
if (stdinFile) {
|
|
3159
|
+
requestUrl.uploadFile = stdinFile;
|
|
3160
|
+
}
|
|
3161
|
+
else if (stdin) {
|
|
3162
|
+
warnf(global, [
|
|
3163
|
+
"upload-file-with-stdin-content",
|
|
3164
|
+
"--upload-file with stdin content is not supported",
|
|
3165
|
+
]);
|
|
3166
|
+
requestUrl.uploadFile = uploadFile;
|
|
3167
|
+
// TODO: this is complicated,
|
|
3168
|
+
// --upload-file only applies per-URL so .data needs to become per-URL...
|
|
3169
|
+
// if you pass --data and --upload-file or --get and --upload-file, curl will error
|
|
3170
|
+
// if (config.url && config.url.length === 1) {
|
|
3171
|
+
// config.data = [["raw", stdin]];
|
|
3172
|
+
// } else {
|
|
3173
|
+
// warnf(global, [
|
|
3174
|
+
// "upload-file-with-stdin-content-and-multiple-urls",
|
|
3175
|
+
// "--upload-file with stdin content and multiple URLs is not supported",
|
|
3176
|
+
// ]);
|
|
3177
|
+
// }
|
|
3178
|
+
}
|
|
3179
|
+
else {
|
|
3180
|
+
requestUrl.uploadFile = uploadFile;
|
|
3181
|
+
}
|
|
3182
|
+
}
|
|
3183
|
+
else {
|
|
3184
|
+
requestUrl.uploadFile = uploadFile;
|
|
3185
|
+
}
|
|
3186
|
+
}
|
|
3187
|
+
if (outputFile) {
|
|
3188
|
+
// TODO: get stdout redirects of command
|
|
3189
|
+
requestUrl.output = outputFile;
|
|
3190
|
+
}
|
|
3191
|
+
// --user takes precedence over the URL
|
|
3192
|
+
const auth = config.user || u.auth;
|
|
3193
|
+
if (auth) {
|
|
3194
|
+
const [user, pass] = auth.split(":", 2);
|
|
3195
|
+
requestUrl.auth = [user, pass || new Word()];
|
|
3196
|
+
}
|
|
3197
|
+
return requestUrl;
|
|
3198
|
+
}
|
|
3199
|
+
function buildData(configData, stdin, stdinFile) {
|
|
3200
|
+
const data = [];
|
|
3201
|
+
let dataStrState = new Word();
|
|
3202
|
+
for (const [i, x] of configData.entries()) {
|
|
3203
|
+
const type = x[0];
|
|
3204
|
+
let value = x[1];
|
|
3205
|
+
let name = null;
|
|
3206
|
+
if (i > 0 && type !== "json") {
|
|
3207
|
+
dataStrState = dataStrState.append("&");
|
|
3208
|
+
}
|
|
3209
|
+
if (type === "urlencode") {
|
|
3210
|
+
// curl checks for = before @
|
|
3211
|
+
const splitOn = value.includes("=") || !value.includes("@") ? "=" : "@";
|
|
3212
|
+
// If there's no = or @ then the entire content is treated as a value and encoded
|
|
3213
|
+
if (value.includes("@") || value.includes("=")) {
|
|
3214
|
+
[name, value] = value.split(splitOn, 2);
|
|
3215
|
+
}
|
|
3216
|
+
if (splitOn === "=") {
|
|
3217
|
+
if (name && name.toBool()) {
|
|
3218
|
+
dataStrState = dataStrState.add(name).append("=");
|
|
3219
|
+
}
|
|
3220
|
+
// curl's --data-urlencode percent-encodes spaces as "+"
|
|
3221
|
+
// https://github.com/curl/curl/blob/curl-7_86_0/src/tool_getparam.c#L630
|
|
3222
|
+
dataStrState = dataStrState.add(percentEncodePlus(value));
|
|
3223
|
+
continue;
|
|
3224
|
+
}
|
|
3225
|
+
name = name && name.toBool() ? name : null;
|
|
3226
|
+
value = value.prepend("@");
|
|
3227
|
+
}
|
|
3228
|
+
let filename = null;
|
|
3229
|
+
if (type !== "raw" && value.startsWith("@")) {
|
|
3230
|
+
filename = value.slice(1);
|
|
3231
|
+
if (eq(filename, "-")) {
|
|
3232
|
+
if (stdin !== undefined) {
|
|
3233
|
+
switch (type) {
|
|
3234
|
+
case "binary":
|
|
3235
|
+
case "json":
|
|
3236
|
+
value = stdin;
|
|
3237
|
+
break;
|
|
3238
|
+
case "urlencode":
|
|
3239
|
+
value = mergeWords(name && name.length ? name.append("=") : new Word(), percentEncodePlus(stdin));
|
|
3240
|
+
break;
|
|
3241
|
+
default:
|
|
3242
|
+
value = stdin.replace(/[\n\r]/g, "");
|
|
3243
|
+
}
|
|
3244
|
+
filename = null;
|
|
3245
|
+
}
|
|
3246
|
+
else if (stdinFile !== undefined) {
|
|
3247
|
+
filename = stdinFile;
|
|
3248
|
+
}
|
|
3249
|
+
else ;
|
|
3250
|
+
}
|
|
3251
|
+
}
|
|
3252
|
+
if (filename !== null) {
|
|
3253
|
+
if (dataStrState.toBool()) {
|
|
3254
|
+
data.push(dataStrState);
|
|
3255
|
+
dataStrState = new Word();
|
|
3256
|
+
}
|
|
3257
|
+
const dataParam = {
|
|
3258
|
+
// If `filename` isn't null, then `type` can't be "raw"
|
|
3259
|
+
filetype: type,
|
|
3260
|
+
filename,
|
|
3261
|
+
};
|
|
3262
|
+
if (name) {
|
|
3263
|
+
dataParam.name = name;
|
|
3264
|
+
}
|
|
3265
|
+
data.push(dataParam);
|
|
3266
|
+
}
|
|
3267
|
+
else {
|
|
3268
|
+
dataStrState = dataStrState.add(value);
|
|
3269
|
+
}
|
|
3270
|
+
}
|
|
3271
|
+
if (dataStrState.toBool()) {
|
|
3272
|
+
data.push(dataStrState);
|
|
3273
|
+
}
|
|
3274
|
+
let dataStrReadsFile = null;
|
|
3275
|
+
const dataStr = mergeWords(...data.map((d) => {
|
|
3276
|
+
if (!(d instanceof Word)) {
|
|
3277
|
+
dataStrReadsFile || (dataStrReadsFile = d.filename.toString()); // report first file
|
|
3278
|
+
if (d.name) {
|
|
3279
|
+
return mergeWords(d.name, "=@", d.filename);
|
|
3280
|
+
}
|
|
3281
|
+
return d.filename.prepend("@");
|
|
3282
|
+
}
|
|
3283
|
+
return d;
|
|
3284
|
+
}));
|
|
3285
|
+
return [data, dataStr, dataStrReadsFile];
|
|
3286
|
+
}
|
|
3287
|
+
function buildRequest(global, config, stdin, stdinFile) {
|
|
3288
|
+
var _a, _b;
|
|
3289
|
+
if (!config.url || !config.url.length) {
|
|
3290
|
+
// TODO: better error message (could be parsing fail)
|
|
3291
|
+
throw new CCError("no URL specified!");
|
|
3292
|
+
}
|
|
3293
|
+
const headers = new Headers(config.header);
|
|
3294
|
+
let cookies;
|
|
3295
|
+
const cookieFiles = [];
|
|
3296
|
+
const cookieHeader = headers.get("cookie");
|
|
3297
|
+
if (cookieHeader) {
|
|
3298
|
+
const parsedCookies = parseCookiesStrict(cookieHeader);
|
|
3299
|
+
if (parsedCookies) {
|
|
3300
|
+
cookies = parsedCookies;
|
|
3301
|
+
}
|
|
3302
|
+
}
|
|
3303
|
+
else if (cookieHeader === undefined && config.cookie) {
|
|
3304
|
+
// If there is a Cookie header, --cookies is ignored
|
|
3305
|
+
const cookieStrings = [];
|
|
3306
|
+
for (const c of config.cookie) {
|
|
3307
|
+
// a --cookie without a = character reads from it as a filename
|
|
3308
|
+
if (c.includes("=")) {
|
|
3309
|
+
cookieStrings.push(c);
|
|
3310
|
+
}
|
|
3311
|
+
else {
|
|
3312
|
+
cookieFiles.push(c);
|
|
3313
|
+
}
|
|
3314
|
+
}
|
|
3315
|
+
if (cookieStrings.length) {
|
|
3316
|
+
const cookieString = joinWords(config.cookie, "; ");
|
|
3317
|
+
headers.setIfMissing("Cookie", cookieString);
|
|
3318
|
+
const parsedCookies = parseCookies(cookieString);
|
|
3319
|
+
if (parsedCookies) {
|
|
3320
|
+
cookies = parsedCookies;
|
|
3321
|
+
}
|
|
3322
|
+
}
|
|
3323
|
+
}
|
|
3324
|
+
if (config["user-agent"]) {
|
|
3325
|
+
headers.setIfMissing("User-Agent", config["user-agent"]);
|
|
3326
|
+
}
|
|
3327
|
+
if (config.referer) {
|
|
3328
|
+
// referer can be ";auto" or followed by ";auto", we ignore that.
|
|
3329
|
+
const referer = config.referer.replace(/;auto$/, "");
|
|
3330
|
+
if (referer.length) {
|
|
3331
|
+
headers.setIfMissing("Referer", referer);
|
|
3332
|
+
}
|
|
3333
|
+
}
|
|
3334
|
+
if (config.range) {
|
|
3335
|
+
let range = config.range.prepend("bytes=");
|
|
3336
|
+
if (!range.includes("-")) {
|
|
3337
|
+
range = range.append("-");
|
|
3338
|
+
}
|
|
3339
|
+
headers.setIfMissing("Range", range);
|
|
3340
|
+
}
|
|
3341
|
+
if (config["time-cond"]) {
|
|
3342
|
+
let timecond = config["time-cond"];
|
|
3343
|
+
let header = "If-Modified-Since";
|
|
3344
|
+
switch (timecond.charAt(0)) {
|
|
3345
|
+
case "+":
|
|
3346
|
+
timecond = timecond.slice(1);
|
|
3347
|
+
break;
|
|
3348
|
+
case "-":
|
|
3349
|
+
timecond = timecond.slice(1);
|
|
3350
|
+
header = "If-Unmodified-Since";
|
|
3351
|
+
break;
|
|
3352
|
+
case "=":
|
|
3353
|
+
timecond = timecond.slice(1);
|
|
3354
|
+
header = "Last-Modified";
|
|
3355
|
+
break;
|
|
3356
|
+
}
|
|
3357
|
+
// TODO: parse date
|
|
3358
|
+
headers.setIfMissing(header, timecond);
|
|
3359
|
+
}
|
|
3360
|
+
let data;
|
|
3361
|
+
let dataStr;
|
|
3362
|
+
let dataStrReadsFile;
|
|
3363
|
+
let queryArray;
|
|
3364
|
+
if (config.data && config.data.length) {
|
|
3365
|
+
if (config.get) {
|
|
3366
|
+
// https://github.com/curl/curl/blob/curl-7_85_0/src/tool_operate.c#L721
|
|
3367
|
+
// --get --data will overwrite --url-query, but if there's no --data, for example,
|
|
3368
|
+
// curl --url-query bar --get example.com
|
|
3369
|
+
// it won't
|
|
3370
|
+
// https://daniel.haxx.se/blog/2022/11/10/append-data-to-the-url-query/
|
|
3371
|
+
config["url-query"] = config.data;
|
|
3372
|
+
delete config.data;
|
|
3373
|
+
}
|
|
3374
|
+
else {
|
|
3375
|
+
[data, dataStr, dataStrReadsFile] = buildData(config.data, stdin, stdinFile);
|
|
3376
|
+
}
|
|
3377
|
+
}
|
|
3378
|
+
if (config["url-query"]) {
|
|
3379
|
+
[queryArray] = buildData(config["url-query"], stdin, stdinFile);
|
|
3380
|
+
}
|
|
3381
|
+
const urls = [];
|
|
3382
|
+
const uploadFiles = config["upload-file"] || [];
|
|
3383
|
+
const outputFiles = config.output || [];
|
|
3384
|
+
for (const [i, url] of config.url.entries()) {
|
|
3385
|
+
urls.push(buildURL(global, config, url, uploadFiles[i], outputFiles[i], stdin, stdinFile));
|
|
3386
|
+
}
|
|
3387
|
+
// --get moves --data into the URL's query string
|
|
3388
|
+
if (config.get && config.data) {
|
|
3389
|
+
delete config.data;
|
|
3390
|
+
}
|
|
3391
|
+
if ((config["upload-file"] || []).length > config.url.length) {
|
|
3392
|
+
warnf(global, [
|
|
3393
|
+
"too-many-upload-files",
|
|
3394
|
+
"Got more --upload-file/-T options than URLs: " +
|
|
3395
|
+
((_a = config["upload-file"]) === null || _a === void 0 ? void 0 : _a.map((f) => JSON.stringify(f.toString())).join(", ")),
|
|
3396
|
+
]);
|
|
3397
|
+
}
|
|
3398
|
+
if ((config.output || []).length > config.url.length) {
|
|
3399
|
+
warnf(global, [
|
|
3400
|
+
"too-many-ouptut-files",
|
|
3401
|
+
"Got more --output/-o options than URLs: " +
|
|
3402
|
+
((_b = config.output) === null || _b === void 0 ? void 0 : _b.map((f) => JSON.stringify(f.toString())).join(", ")),
|
|
3403
|
+
]);
|
|
3404
|
+
}
|
|
3405
|
+
const request = {
|
|
3406
|
+
urls,
|
|
3407
|
+
authType: pickAuth(config.authtype),
|
|
3408
|
+
headers,
|
|
3409
|
+
};
|
|
3410
|
+
// TODO: warn about unused stdin?
|
|
3411
|
+
if (stdin) {
|
|
3412
|
+
request.stdin = stdin;
|
|
3413
|
+
}
|
|
3414
|
+
if (stdinFile) {
|
|
3415
|
+
request.stdinFile = stdinFile;
|
|
3416
|
+
}
|
|
3417
|
+
if (config.globoff !== undefined) {
|
|
3418
|
+
request.globoff = config.globoff;
|
|
3419
|
+
}
|
|
3420
|
+
if (cookies) {
|
|
3421
|
+
// generators that use .cookies need to do
|
|
3422
|
+
// deleteHeader(request, 'cookie')
|
|
3423
|
+
request.cookies = cookies;
|
|
3424
|
+
}
|
|
3425
|
+
if (cookieFiles.length) {
|
|
3426
|
+
request.cookieFiles = cookieFiles;
|
|
3427
|
+
}
|
|
3428
|
+
if (config["cookie-jar"]) {
|
|
3429
|
+
request.cookieJar = config["cookie-jar"];
|
|
3430
|
+
}
|
|
3431
|
+
if (config.compressed !== undefined) {
|
|
3432
|
+
request.compressed = config.compressed;
|
|
3433
|
+
}
|
|
3434
|
+
if (config.json) {
|
|
3435
|
+
headers.setIfMissing("Content-Type", "application/json");
|
|
3436
|
+
headers.setIfMissing("Accept", "application/json");
|
|
3437
|
+
}
|
|
3438
|
+
else if (config.data) {
|
|
3439
|
+
headers.setIfMissing("Content-Type", "application/x-www-form-urlencoded");
|
|
3440
|
+
}
|
|
3441
|
+
else if (config.form) {
|
|
3442
|
+
// TODO: warn when details (;filename=, etc.) are not supported
|
|
3443
|
+
// by each converter.
|
|
3444
|
+
request.multipartUploads = parseForm(config.form, global.warnings);
|
|
3445
|
+
}
|
|
3446
|
+
if (config["aws-sigv4"]) {
|
|
3447
|
+
// https://github.com/curl/curl/blob/curl-7_86_0/lib/setopt.c#L678-L679
|
|
3448
|
+
request.authType = "aws-sigv4";
|
|
3449
|
+
request.awsSigV4 = config["aws-sigv4"];
|
|
3450
|
+
}
|
|
3451
|
+
if (request.authType === "bearer" && config["oauth2-bearer"]) {
|
|
3452
|
+
const bearer = config["oauth2-bearer"].prepend("Bearer ");
|
|
3453
|
+
headers.setIfMissing("Authorization", bearer);
|
|
3454
|
+
}
|
|
3455
|
+
if (config.delegation) {
|
|
3456
|
+
request.delegation = config.delegation;
|
|
3457
|
+
}
|
|
3458
|
+
// TODO: ideally we should generate code that explicitly unsets the header too
|
|
3459
|
+
// no HTTP libraries allow that.
|
|
3460
|
+
headers.clearNulls();
|
|
3461
|
+
if (config.data && config.data.length) {
|
|
3462
|
+
request.data = dataStr;
|
|
3463
|
+
if (dataStrReadsFile) {
|
|
3464
|
+
request.dataReadsFile = dataStrReadsFile;
|
|
3465
|
+
}
|
|
3466
|
+
request.dataArray = data;
|
|
3467
|
+
// TODO: remove these
|
|
3468
|
+
request.isDataRaw = false;
|
|
3469
|
+
request.isDataBinary = (data || []).some((d) => !(d instanceof Word) && d.filetype === "binary");
|
|
3470
|
+
}
|
|
3471
|
+
if (queryArray) {
|
|
3472
|
+
// If we have to generate code that reads from a file, we
|
|
3473
|
+
// need to do it once for all URLs.
|
|
3474
|
+
request.queryArray = queryArray;
|
|
3475
|
+
}
|
|
3476
|
+
if (config["ipv4"] !== undefined) {
|
|
3477
|
+
request["ipv4"] = config["ipv4"];
|
|
3478
|
+
}
|
|
3479
|
+
if (config["ipv6"] !== undefined) {
|
|
3480
|
+
request["ipv6"] = config["ipv6"];
|
|
3481
|
+
}
|
|
3482
|
+
if (config.ciphers) {
|
|
3483
|
+
request.ciphers = config.ciphers;
|
|
3484
|
+
}
|
|
3485
|
+
if (config.insecure) {
|
|
3486
|
+
request.insecure = true;
|
|
3487
|
+
}
|
|
3488
|
+
// TODO: if the URL doesn't start with https://, curl doesn't verify
|
|
3489
|
+
// certificates, etc.
|
|
3490
|
+
if (config.cert) {
|
|
3491
|
+
if (config.cert.startsWith("pkcs11:") || !config.cert.match(/[:\\]/)) {
|
|
3492
|
+
request.cert = [config.cert, null];
|
|
3493
|
+
}
|
|
3494
|
+
else {
|
|
3495
|
+
// TODO: curl does more complex processing
|
|
3496
|
+
// find un-backslash-escaped colon, backslash might also be escaped with a backslash
|
|
3497
|
+
let colon = -1;
|
|
3498
|
+
try {
|
|
3499
|
+
// Safari versions older than 16.4 don't support negative lookbehind
|
|
3500
|
+
colon = config.cert.search(/(?<!\\)(?:\\\\)*:/);
|
|
3501
|
+
}
|
|
3502
|
+
catch (_c) {
|
|
3503
|
+
colon = config.cert.search(/:/);
|
|
3504
|
+
}
|
|
3505
|
+
if (colon === -1) {
|
|
3506
|
+
request.cert = [config.cert, null];
|
|
3507
|
+
}
|
|
3508
|
+
else {
|
|
3509
|
+
const cert = config.cert.slice(0, colon);
|
|
3510
|
+
const password = config.cert.slice(colon + 1);
|
|
3511
|
+
if (password.toBool()) {
|
|
3512
|
+
request.cert = [cert, password];
|
|
3513
|
+
}
|
|
3514
|
+
else {
|
|
3515
|
+
request.cert = [cert, null];
|
|
3516
|
+
}
|
|
3517
|
+
}
|
|
3518
|
+
}
|
|
3519
|
+
}
|
|
3520
|
+
if (config["cert-type"]) {
|
|
3521
|
+
request.certType = config["cert-type"];
|
|
3522
|
+
}
|
|
3523
|
+
if (config.key) {
|
|
3524
|
+
request.key = config.key;
|
|
3525
|
+
}
|
|
3526
|
+
if (config["key-type"]) {
|
|
3527
|
+
request.keyType = config["key-type"];
|
|
3528
|
+
}
|
|
3529
|
+
if (config.cacert) {
|
|
3530
|
+
request.cacert = config.cacert;
|
|
3531
|
+
}
|
|
3532
|
+
if (config.capath) {
|
|
3533
|
+
request.capath = config.capath;
|
|
3534
|
+
}
|
|
3535
|
+
if (config.crlfile) {
|
|
3536
|
+
request.crlfile = config.crlfile;
|
|
3537
|
+
}
|
|
3538
|
+
if (config.pinnedpubkey) {
|
|
3539
|
+
request.pinnedpubkey = config.pinnedpubkey;
|
|
3540
|
+
}
|
|
3541
|
+
if (config["random-file"]) {
|
|
3542
|
+
request.randomFile = config["random-file"];
|
|
3543
|
+
}
|
|
3544
|
+
if (config["egd-file"]) {
|
|
3545
|
+
request.egdFile = config["egd-file"];
|
|
3546
|
+
}
|
|
3547
|
+
if (config.hsts) {
|
|
3548
|
+
request.hsts = config.hsts;
|
|
3549
|
+
}
|
|
3550
|
+
if (config.proxy) {
|
|
3551
|
+
// https://github.com/curl/curl/blob/e498a9b1fe5964a18eb2a3a99dc52160d2768261/lib/url.c#L2388-L2390
|
|
3552
|
+
request.proxy = config.proxy;
|
|
3553
|
+
if (config["proxy-user"]) {
|
|
3554
|
+
request.proxyAuth = config["proxy-user"];
|
|
3555
|
+
}
|
|
3556
|
+
}
|
|
3557
|
+
if (config.noproxy) {
|
|
3558
|
+
request.noproxy = config.noproxy;
|
|
3559
|
+
}
|
|
3560
|
+
if (config["max-time"]) {
|
|
3561
|
+
request.timeout = config["max-time"];
|
|
3562
|
+
if (config["max-time"].isString() &&
|
|
3563
|
+
// TODO: parseFloat() like curl
|
|
3564
|
+
isNaN(parseFloat(config["max-time"].toString()))) {
|
|
3565
|
+
warnf(global, [
|
|
3566
|
+
"max-time-not-number",
|
|
3567
|
+
"option --max-time: expected a proper numerical parameter: " +
|
|
3568
|
+
JSON.stringify(config["max-time"].toString()),
|
|
3569
|
+
]);
|
|
3570
|
+
}
|
|
3571
|
+
}
|
|
3572
|
+
if (config["connect-timeout"]) {
|
|
3573
|
+
request.connectTimeout = config["connect-timeout"];
|
|
3574
|
+
if (config["connect-timeout"].isString() &&
|
|
3575
|
+
isNaN(parseFloat(config["connect-timeout"].toString()))) {
|
|
3576
|
+
warnf(global, [
|
|
3577
|
+
"connect-timeout-not-number",
|
|
3578
|
+
"option --connect-timeout: expected a proper numerical parameter: " +
|
|
3579
|
+
JSON.stringify(config["connect-timeout"].toString()),
|
|
3580
|
+
]);
|
|
3581
|
+
}
|
|
3582
|
+
}
|
|
3583
|
+
if (config["limit-rate"]) {
|
|
3584
|
+
request.limitRate = config["limit-rate"];
|
|
3585
|
+
}
|
|
3586
|
+
if (Object.prototype.hasOwnProperty.call(config, "keepalive")) {
|
|
3587
|
+
request.keepAlive = config.keepalive;
|
|
3588
|
+
}
|
|
3589
|
+
if (Object.prototype.hasOwnProperty.call(config, "location")) {
|
|
3590
|
+
request.followRedirects = config.location;
|
|
3591
|
+
}
|
|
3592
|
+
if (config["location-trusted"]) {
|
|
3593
|
+
request.followRedirectsTrusted = config["location-trusted"];
|
|
3594
|
+
}
|
|
3595
|
+
if (config["max-redirs"]) {
|
|
3596
|
+
request.maxRedirects = config["max-redirs"].trim();
|
|
3597
|
+
if (config["max-redirs"].isString() &&
|
|
3598
|
+
!isInt(config["max-redirs"].toString())) {
|
|
3599
|
+
warnf(global, [
|
|
3600
|
+
"max-redirs-not-int",
|
|
3601
|
+
"option --max-redirs: expected a proper numerical parameter: " +
|
|
3602
|
+
JSON.stringify(config["max-redirs"].toString()),
|
|
3603
|
+
]);
|
|
3604
|
+
}
|
|
3605
|
+
}
|
|
3606
|
+
if (config.retry) {
|
|
3607
|
+
request.retry = config.retry;
|
|
3608
|
+
}
|
|
3609
|
+
// TODO: this should write to the same "httpVersion" variable
|
|
3610
|
+
const http2 = config.http2 || config["http2-prior-knowledge"];
|
|
3611
|
+
if (http2) {
|
|
3612
|
+
request.http2 = http2;
|
|
3613
|
+
}
|
|
3614
|
+
if (config.http3 || config["http3-only"]) {
|
|
3615
|
+
request.http3 = true;
|
|
3616
|
+
}
|
|
3617
|
+
if (config["unix-socket"]) {
|
|
3618
|
+
request.unixSocket = config["unix-socket"];
|
|
3619
|
+
}
|
|
3620
|
+
if (config["netrc-optional"] || config["netrc-file"]) {
|
|
3621
|
+
request.netrc = "optional";
|
|
3622
|
+
}
|
|
3623
|
+
else if (config.netrc) {
|
|
3624
|
+
request.netrc = "required";
|
|
3625
|
+
}
|
|
3626
|
+
else if (config.netrc === false) {
|
|
3627
|
+
// TODO || config["netrc-optional"] === false ?
|
|
3628
|
+
request.netrc = "ignored";
|
|
3629
|
+
}
|
|
3630
|
+
if (config["continue-at"]) {
|
|
3631
|
+
request.continueAt = config["continue-at"];
|
|
3632
|
+
}
|
|
3633
|
+
if (Object.prototype.hasOwnProperty.call(config, "clobber")) {
|
|
3634
|
+
request.clobber = config.clobber;
|
|
3635
|
+
}
|
|
3636
|
+
if (Object.prototype.hasOwnProperty.call(config, "remote-time")) {
|
|
3637
|
+
request.remoteTime = config["remote-time"];
|
|
3638
|
+
}
|
|
3639
|
+
// Global options
|
|
3640
|
+
if (Object.prototype.hasOwnProperty.call(global, "verbose")) {
|
|
3641
|
+
request.verbose = global.verbose;
|
|
3642
|
+
}
|
|
3643
|
+
if (Object.prototype.hasOwnProperty.call(global, "silent")) {
|
|
3644
|
+
request.silent = global.silent;
|
|
3645
|
+
}
|
|
3646
|
+
return request;
|
|
3647
|
+
}
|
|
3648
|
+
function buildRequests(global, stdin, stdinFile) {
|
|
3649
|
+
if (!global.configs.length) {
|
|
3650
|
+
// shouldn't happen
|
|
3651
|
+
warnf(global, ["no-configs", "got empty config object"]);
|
|
3652
|
+
}
|
|
3653
|
+
return global.configs.map((config) => buildRequest(global, config, stdin, stdinFile));
|
|
3654
|
+
}
|
|
3655
|
+
function getFirst(requests, warnings, support) {
|
|
3656
|
+
if (requests.length > 1) {
|
|
3657
|
+
warnings.push([
|
|
3658
|
+
"next",
|
|
3659
|
+
// TODO: better message, we might have two requests because of
|
|
3660
|
+
// --next or because of multiple curl commands or both
|
|
3661
|
+
"got " +
|
|
3662
|
+
requests.length +
|
|
3663
|
+
" curl requests, only converting the first one",
|
|
3664
|
+
]);
|
|
3665
|
+
}
|
|
3666
|
+
const request = requests[0];
|
|
3667
|
+
warnIfPartsIgnored(request, warnings, support);
|
|
3668
|
+
return request;
|
|
3669
|
+
}
|
|
3670
|
+
|
|
3671
|
+
function clip(s, maxLength = 30) {
|
|
3672
|
+
if (s.length > maxLength) {
|
|
3673
|
+
return s.slice(0, maxLength - 3) + "...";
|
|
3674
|
+
}
|
|
3675
|
+
return s;
|
|
3676
|
+
}
|
|
3677
|
+
function findCommands(curlCommand, warnings) {
|
|
3678
|
+
if (typeof curlCommand === "string") {
|
|
3679
|
+
return tokenize(curlCommand, warnings);
|
|
3680
|
+
}
|
|
3681
|
+
if (curlCommand.length === 0) {
|
|
3682
|
+
throw new CCError("no arguments provided");
|
|
3683
|
+
}
|
|
3684
|
+
if (curlCommand[0].trim() !== "curl") {
|
|
3685
|
+
throw new CCError('command should begin with "curl" but instead begins with ' +
|
|
3686
|
+
JSON.stringify(clip(curlCommand[0])));
|
|
3687
|
+
}
|
|
3688
|
+
return [[curlCommand.map((arg) => new Word(arg)), undefined, undefined]];
|
|
3689
|
+
}
|
|
3690
|
+
/**
|
|
3691
|
+
* Accepts a string of Bash code or a tokenized argv array.
|
|
3692
|
+
* Returns an array of parsed curl objects.
|
|
3693
|
+
* @param command a string of Bash code containing at least one curl command or an
|
|
3694
|
+
* array of shell argument tokens (meant for passing process.argv).
|
|
3695
|
+
*/
|
|
3696
|
+
function parse(command, supportedArgs, warnings = []) {
|
|
3697
|
+
let requests = [];
|
|
3698
|
+
const curlCommands = findCommands(command, warnings);
|
|
3699
|
+
for (const [argv, stdin, stdinFile] of curlCommands) {
|
|
3700
|
+
const globalConfig = parseArgs(argv, curlLongOpts, curlLongOptsShortened, curlShortOpts, supportedArgs, warnings);
|
|
3701
|
+
requests = requests.concat(buildRequests(globalConfig, stdin, stdinFile));
|
|
3702
|
+
}
|
|
3703
|
+
return requests;
|
|
3704
|
+
}
|
|
3705
|
+
|
|
3706
|
+
exports.COMMON_SUPPORTED_ARGS = COMMON_SUPPORTED_ARGS;
|
|
3707
|
+
exports.clip = clip;
|
|
3708
|
+
exports.getFirst = getFirst;
|
|
3709
|
+
exports.parse = parse;
|