@traffic-twin/sdk 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +202 -0
- package/README.md +171 -2
- package/dist/cjs/agent.d.ts +143 -0
- package/dist/cjs/agent.js +927 -0
- package/dist/cjs/capture/body.d.ts +11 -0
- package/dist/cjs/capture/body.js +73 -0
- package/dist/cjs/capture/common.d.ts +75 -0
- package/dist/cjs/capture/common.js +180 -0
- package/dist/cjs/capture/context.d.ts +28 -0
- package/dist/cjs/capture/context.js +92 -0
- package/dist/cjs/capture/inbound.d.ts +12 -0
- package/dist/cjs/capture/inbound.js +195 -0
- package/dist/cjs/capture/outbound-http.d.ts +4 -0
- package/dist/cjs/capture/outbound-http.js +200 -0
- package/dist/cjs/capture/outbound-undici.d.ts +2 -0
- package/dist/cjs/capture/outbound-undici.js +242 -0
- package/dist/cjs/capture/wire.d.ts +27 -0
- package/dist/cjs/capture/wire.js +68 -0
- package/dist/cjs/export/event.d.ts +46 -0
- package/dist/cjs/export/event.js +91 -0
- package/dist/cjs/export/exporter.d.ts +50 -0
- package/dist/cjs/export/exporter.js +192 -0
- package/dist/cjs/export/pipeline.d.ts +20 -0
- package/dist/cjs/export/pipeline.js +40 -0
- package/dist/cjs/fleet/asn1.d.ts +44 -0
- package/dist/cjs/fleet/asn1.js +150 -0
- package/dist/cjs/fleet/client.d.ts +84 -0
- package/dist/cjs/fleet/client.js +216 -0
- package/dist/cjs/fleet/http.d.ts +17 -0
- package/dist/cjs/fleet/http.js +80 -0
- package/dist/cjs/fleet/store.d.ts +18 -0
- package/dist/cjs/fleet/store.js +115 -0
- package/dist/cjs/index.d.ts +14 -0
- package/dist/cjs/index.js +69 -0
- package/dist/cjs/log.d.ts +4 -0
- package/dist/cjs/log.js +30 -0
- package/dist/cjs/package.json +1 -0
- package/dist/cjs/register.d.ts +1 -0
- package/dist/cjs/register.js +18 -0
- package/dist/cjs/sanitizer/bodies.d.ts +55 -0
- package/dist/cjs/sanitizer/bodies.js +710 -0
- package/dist/cjs/sanitizer/formats.d.ts +26 -0
- package/dist/cjs/sanitizer/formats.js +110 -0
- package/dist/cjs/sanitizer/graphql.d.ts +23 -0
- package/dist/cjs/sanitizer/graphql.js +478 -0
- package/dist/cjs/sanitizer/hostpat.d.ts +15 -0
- package/dist/cjs/sanitizer/hostpat.js +129 -0
- package/dist/cjs/sanitizer/json.d.ts +60 -0
- package/dist/cjs/sanitizer/json.js +460 -0
- package/dist/cjs/sanitizer/policy.d.ts +224 -0
- package/dist/cjs/sanitizer/policy.js +820 -0
- package/dist/cjs/sanitizer/sanitizer.d.ts +149 -0
- package/dist/cjs/sanitizer/sanitizer.js +1473 -0
- package/dist/cjs/sanitizer/utf8.d.ts +22 -0
- package/dist/cjs/sanitizer/utf8.js +180 -0
- package/dist/cjs/sanitizer/vectors.d.ts +22 -0
- package/dist/cjs/sanitizer/vectors.js +130 -0
- package/dist/cjs/version.d.ts +2 -0
- package/dist/cjs/version.js +5 -0
- package/dist/cjs/worker/dir.d.ts +1 -0
- package/dist/cjs/worker/dir.js +8 -0
- package/dist/cjs/worker/pool.d.ts +47 -0
- package/dist/cjs/worker/pool.js +179 -0
- package/dist/cjs/worker/protocol.d.ts +37 -0
- package/dist/cjs/worker/protocol.js +3 -0
- package/dist/cjs/worker/sanitizer-worker.d.ts +1 -0
- package/dist/cjs/worker/sanitizer-worker.js +53 -0
- package/dist/esm/agent.d.ts +143 -0
- package/dist/esm/agent.js +921 -0
- package/dist/esm/capture/body.d.ts +11 -0
- package/dist/esm/capture/body.js +69 -0
- package/dist/esm/capture/common.d.ts +75 -0
- package/dist/esm/capture/common.js +166 -0
- package/dist/esm/capture/context.d.ts +28 -0
- package/dist/esm/capture/context.js +84 -0
- package/dist/esm/capture/inbound.d.ts +12 -0
- package/dist/esm/capture/inbound.js +187 -0
- package/dist/esm/capture/outbound-http.d.ts +4 -0
- package/dist/esm/capture/outbound-http.js +193 -0
- package/dist/esm/capture/outbound-undici.d.ts +2 -0
- package/dist/esm/capture/outbound-undici.js +236 -0
- package/dist/esm/capture/wire.d.ts +27 -0
- package/dist/esm/capture/wire.js +65 -0
- package/dist/esm/export/event.d.ts +46 -0
- package/dist/esm/export/event.js +85 -0
- package/dist/esm/export/exporter.d.ts +50 -0
- package/dist/esm/export/exporter.js +188 -0
- package/dist/esm/export/pipeline.d.ts +20 -0
- package/dist/esm/export/pipeline.js +37 -0
- package/dist/esm/fleet/asn1.d.ts +44 -0
- package/dist/esm/fleet/asn1.js +128 -0
- package/dist/esm/fleet/client.d.ts +84 -0
- package/dist/esm/fleet/client.js +204 -0
- package/dist/esm/fleet/http.d.ts +17 -0
- package/dist/esm/fleet/http.js +73 -0
- package/dist/esm/fleet/store.d.ts +18 -0
- package/dist/esm/fleet/store.js +110 -0
- package/dist/esm/index.d.ts +14 -0
- package/dist/esm/index.js +48 -0
- package/dist/esm/log.d.ts +4 -0
- package/dist/esm/log.js +25 -0
- package/dist/esm/package.json +1 -0
- package/dist/esm/register.d.ts +1 -0
- package/dist/esm/register.js +16 -0
- package/dist/esm/sanitizer/bodies.d.ts +55 -0
- package/dist/esm/sanitizer/bodies.js +694 -0
- package/dist/esm/sanitizer/formats.d.ts +26 -0
- package/dist/esm/sanitizer/formats.js +101 -0
- package/dist/esm/sanitizer/graphql.d.ts +23 -0
- package/dist/esm/sanitizer/graphql.js +472 -0
- package/dist/esm/sanitizer/hostpat.d.ts +15 -0
- package/dist/esm/sanitizer/hostpat.js +122 -0
- package/dist/esm/sanitizer/json.d.ts +60 -0
- package/dist/esm/sanitizer/json.js +447 -0
- package/dist/esm/sanitizer/policy.d.ts +224 -0
- package/dist/esm/sanitizer/policy.js +800 -0
- package/dist/esm/sanitizer/sanitizer.d.ts +149 -0
- package/dist/esm/sanitizer/sanitizer.js +1459 -0
- package/dist/esm/sanitizer/utf8.d.ts +22 -0
- package/dist/esm/sanitizer/utf8.js +166 -0
- package/dist/esm/sanitizer/vectors.d.ts +22 -0
- package/dist/esm/sanitizer/vectors.js +125 -0
- package/dist/esm/version.d.ts +2 -0
- package/dist/esm/version.js +2 -0
- package/dist/esm/worker/dir.d.ts +1 -0
- package/dist/esm/worker/dir.js +5 -0
- package/dist/esm/worker/pool.d.ts +47 -0
- package/dist/esm/worker/pool.js +175 -0
- package/dist/esm/worker/protocol.d.ts +37 -0
- package/dist/esm/worker/protocol.js +2 -0
- package/dist/esm/worker/sanitizer-worker.d.ts +1 -0
- package/dist/esm/worker/sanitizer-worker.js +51 -0
- package/package.json +75 -4
|
@@ -0,0 +1,694 @@
|
|
|
1
|
+
// Body formats of spec v1.19 (sanitizer.md 22): XML and SOAP (22.2), multipart/form-data
|
|
2
|
+
// (22.3), base64 documents and the document size classes (22.4). Pure functions: they never
|
|
3
|
+
// see the policy. Mirrors conformance/ref/bodies.py.
|
|
4
|
+
//
|
|
5
|
+
// The XML reader is a hand-written strict subset of XML 1.0 + Namespaces: no DTD, no entity
|
|
6
|
+
// other than the five predefined ones and character references, never a platform XML library
|
|
7
|
+
// (hardening: XXE and entity expansion are impossible by construction).
|
|
8
|
+
import { decodeUtf8 } from './utf8.js';
|
|
9
|
+
// ------------------------------------------------------------------ documents (22.4)
|
|
10
|
+
/** 22.4: document class on the decoded length. */
|
|
11
|
+
export function docClass(n) {
|
|
12
|
+
if (n <= 1024)
|
|
13
|
+
return 'l1k';
|
|
14
|
+
if (n <= 16384)
|
|
15
|
+
return 'l16k';
|
|
16
|
+
if (n <= 131072)
|
|
17
|
+
return 'l128k';
|
|
18
|
+
if (n <= 1048576)
|
|
19
|
+
return 'l1m';
|
|
20
|
+
if (n <= 8388608)
|
|
21
|
+
return 'l8m';
|
|
22
|
+
return 'lmax';
|
|
23
|
+
}
|
|
24
|
+
const PNG = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
|
|
25
|
+
const OLE = Buffer.from([0xd0, 0xcf, 0x11, 0xe0, 0xa1, 0xb1, 0x1a, 0xe1]);
|
|
26
|
+
function startsWith(b, prefix, at = 0) {
|
|
27
|
+
const n = prefix.length;
|
|
28
|
+
if (b.length < at + n)
|
|
29
|
+
return false;
|
|
30
|
+
for (let i = 0; i < n; i++) {
|
|
31
|
+
const c = typeof prefix === 'string' ? prefix.charCodeAt(i) : prefix[i];
|
|
32
|
+
if (b[at + i] !== c)
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
/** 22.4 point 3 without the zip refinement: kind from the first 12 bytes; 'zip?' for a zip. */
|
|
38
|
+
function headKind(b) {
|
|
39
|
+
if (startsWith(b, '%PDF-'))
|
|
40
|
+
return 'pdf';
|
|
41
|
+
if (startsWith(b, PNG))
|
|
42
|
+
return 'png';
|
|
43
|
+
if (b.length >= 3 && b[0] === 0xff && b[1] === 0xd8 && b[2] === 0xff)
|
|
44
|
+
return 'jpeg';
|
|
45
|
+
if (startsWith(b, 'GIF87a') || startsWith(b, 'GIF89a'))
|
|
46
|
+
return 'gif';
|
|
47
|
+
if (startsWith(b, 'RIFF') && startsWith(b, 'WEBP', 8))
|
|
48
|
+
return 'webp';
|
|
49
|
+
if (startsWith(b, OLE))
|
|
50
|
+
return 'ole';
|
|
51
|
+
if (startsWith(b, 'PK\x03\x04'))
|
|
52
|
+
return 'zip?';
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
function zipKind(b) {
|
|
56
|
+
if (b.indexOf('word/document.xml', 0, 'latin1') >= 0)
|
|
57
|
+
return 'docx';
|
|
58
|
+
if (b.indexOf('xl/workbook.xml', 0, 'latin1') >= 0)
|
|
59
|
+
return 'xlsx';
|
|
60
|
+
if (b.indexOf('ppt/presentation.xml', 0, 'latin1') >= 0)
|
|
61
|
+
return 'pptx';
|
|
62
|
+
return 'zip';
|
|
63
|
+
}
|
|
64
|
+
/** 22.4 point 3: kind of a document from its bytes, or null. */
|
|
65
|
+
export function magicKind(b) {
|
|
66
|
+
const k = headKind(b);
|
|
67
|
+
return k === 'zip?' ? zipKind(b) : k;
|
|
68
|
+
}
|
|
69
|
+
const B64_RE = /^[A-Za-z0-9+/_-]*={0,2}$/;
|
|
70
|
+
const DATA_PREFIX_RE = /^data:[!-+\--~]*;base64$/; // printable ASCII without ','
|
|
71
|
+
const DOC_MIN_CHARS = 16;
|
|
72
|
+
/** 22.4: 'drop:doc:<kind>:<class>' if the string is the base64 of a document, else null. */
|
|
73
|
+
export function base64Doc(str) {
|
|
74
|
+
let s = str;
|
|
75
|
+
if (s.startsWith('data:')) {
|
|
76
|
+
const i = s.indexOf(',');
|
|
77
|
+
if (i < 0 || !DATA_PREFIX_RE.test(s.slice(0, i)))
|
|
78
|
+
return null;
|
|
79
|
+
s = s.slice(i + 1);
|
|
80
|
+
}
|
|
81
|
+
if (s.length < DOC_MIN_CHARS || !B64_RE.test(s))
|
|
82
|
+
return null;
|
|
83
|
+
let end = s.length;
|
|
84
|
+
while (end > 0 && s.charCodeAt(end - 1) === 0x3d)
|
|
85
|
+
end--;
|
|
86
|
+
if (end !== s.length && s.length % 4 !== 0)
|
|
87
|
+
return null;
|
|
88
|
+
if (end % 4 === 1)
|
|
89
|
+
return null;
|
|
90
|
+
const body = s.slice(0, end);
|
|
91
|
+
// The first 16 characters give the first 12 bytes: enough for every magic number but
|
|
92
|
+
// the zip names, which need the whole document.
|
|
93
|
+
const head = Buffer.from(body.slice(0, 16), 'base64url');
|
|
94
|
+
let kind = headKind(head);
|
|
95
|
+
if (kind === null)
|
|
96
|
+
return null;
|
|
97
|
+
const length = Math.floor((end * 3) / 4);
|
|
98
|
+
if (kind === 'zip?')
|
|
99
|
+
kind = zipKind(Buffer.from(body, 'base64url'));
|
|
100
|
+
return 'drop:doc:' + kind + ':' + docClass(length);
|
|
101
|
+
}
|
|
102
|
+
// ------------------------------------------------------------------ media type parameters (22.2, 22.3)
|
|
103
|
+
function trimSpTab(s) {
|
|
104
|
+
let a = 0;
|
|
105
|
+
let b = s.length;
|
|
106
|
+
while (a < b && (s[a] === ' ' || s[a] === '\t'))
|
|
107
|
+
a++;
|
|
108
|
+
while (b > a && (s[b - 1] === ' ' || s[b - 1] === '\t'))
|
|
109
|
+
b--;
|
|
110
|
+
return s.slice(a, b);
|
|
111
|
+
}
|
|
112
|
+
function asciiLowerStr(s) {
|
|
113
|
+
return s.replace(/[A-Z]+/g, (m) => m.toLowerCase());
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Parameters of a normalized content-type value: lowercase name -> value, first occurrence
|
|
117
|
+
* wins. Split on ';', each piece trimmed and split on the first '='; a value starting with '"'
|
|
118
|
+
* must end with '"' (length >= 2) and loses both quotes, no escapes. Not UTF-8: no parameters.
|
|
119
|
+
*/
|
|
120
|
+
export function mediaParams(value) {
|
|
121
|
+
const out = new Map();
|
|
122
|
+
if (!value)
|
|
123
|
+
return out;
|
|
124
|
+
const s = decodeUtf8(value);
|
|
125
|
+
if (s === null)
|
|
126
|
+
return out;
|
|
127
|
+
const pieces = s.split(';');
|
|
128
|
+
for (let i = 1; i < pieces.length; i++) {
|
|
129
|
+
const piece = trimSpTab(pieces[i]);
|
|
130
|
+
const eq = piece.indexOf('=');
|
|
131
|
+
if (eq < 0)
|
|
132
|
+
continue;
|
|
133
|
+
const name = asciiLowerStr(trimSpTab(piece.slice(0, eq)));
|
|
134
|
+
let v = trimSpTab(piece.slice(eq + 1));
|
|
135
|
+
if (v.startsWith('"')) {
|
|
136
|
+
if (v.length < 2 || !v.endsWith('"'))
|
|
137
|
+
continue;
|
|
138
|
+
v = v.slice(1, -1);
|
|
139
|
+
}
|
|
140
|
+
if (!out.has(name))
|
|
141
|
+
out.set(name, v);
|
|
142
|
+
}
|
|
143
|
+
return out;
|
|
144
|
+
}
|
|
145
|
+
// ------------------------------------------------------------------ multipart (22.3)
|
|
146
|
+
const BOUNDARY_RE = /^[0-9A-Za-z'()+_,./:=? -]{0,69}[0-9A-Za-z'()+_,./:=?-]$/;
|
|
147
|
+
const PART_HEADER_NAME_RE = /^[A-Za-z0-9!#$%&'*+.^_`|~-]+$/;
|
|
148
|
+
const FILE_TYPES = new Map([
|
|
149
|
+
['text/plain', 'text'], ['text/csv', 'csv'], ['application/json', 'json'], ['application/xml', 'xml'], ['text/xml', 'xml'],
|
|
150
|
+
]);
|
|
151
|
+
const CRLF = Buffer.from('\r\n');
|
|
152
|
+
const CRLFCRLF = Buffer.from('\r\n\r\n');
|
|
153
|
+
export class InvalidMultipart extends Error {
|
|
154
|
+
}
|
|
155
|
+
const bad = (why) => {
|
|
156
|
+
throw new InvalidMultipart(why);
|
|
157
|
+
};
|
|
158
|
+
/** content-disposition of a part -> params (first wins); InvalidMultipart if not form-data. */
|
|
159
|
+
function disposition(s) {
|
|
160
|
+
const i = s.indexOf(';');
|
|
161
|
+
const kind = trimSpTab(i < 0 ? s : s.slice(0, i));
|
|
162
|
+
if (kind.toLowerCase() !== 'form-data')
|
|
163
|
+
bad('disposition');
|
|
164
|
+
const params = new Map();
|
|
165
|
+
let pos = i < 0 ? s.length : i;
|
|
166
|
+
const n = s.length;
|
|
167
|
+
while (pos < n) {
|
|
168
|
+
while (pos < n && (s[pos] === ' ' || s[pos] === '\t' || s[pos] === ';'))
|
|
169
|
+
pos++;
|
|
170
|
+
if (pos >= n)
|
|
171
|
+
break;
|
|
172
|
+
const start = pos;
|
|
173
|
+
while (pos < n && s[pos] !== '=' && s[pos] !== ';')
|
|
174
|
+
pos++;
|
|
175
|
+
const name = trimSpTab(s.slice(start, pos)).toLowerCase();
|
|
176
|
+
if (pos >= n || s[pos] === ';') {
|
|
177
|
+
if (!params.has(name))
|
|
178
|
+
params.set(name, '');
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
pos++; // '='
|
|
182
|
+
while (pos < n && (s[pos] === ' ' || s[pos] === '\t'))
|
|
183
|
+
pos++;
|
|
184
|
+
let val;
|
|
185
|
+
if (pos < n && s[pos] === '"') {
|
|
186
|
+
pos++;
|
|
187
|
+
let buf = '';
|
|
188
|
+
for (;;) {
|
|
189
|
+
if (pos >= n)
|
|
190
|
+
bad('unterminated quoted string');
|
|
191
|
+
const c = s[pos];
|
|
192
|
+
if (c === '\\' && pos + 1 < n) {
|
|
193
|
+
buf += s[pos + 1];
|
|
194
|
+
pos += 2;
|
|
195
|
+
continue;
|
|
196
|
+
}
|
|
197
|
+
if (c === '"') {
|
|
198
|
+
pos++;
|
|
199
|
+
break;
|
|
200
|
+
}
|
|
201
|
+
buf += c;
|
|
202
|
+
pos++;
|
|
203
|
+
}
|
|
204
|
+
val = buf;
|
|
205
|
+
while (pos < n && (s[pos] === ' ' || s[pos] === '\t'))
|
|
206
|
+
pos++;
|
|
207
|
+
if (pos < n && s[pos] !== ';')
|
|
208
|
+
bad('garbage after quoted string');
|
|
209
|
+
}
|
|
210
|
+
else {
|
|
211
|
+
const start2 = pos;
|
|
212
|
+
while (pos < n && s[pos] !== ';')
|
|
213
|
+
pos++;
|
|
214
|
+
val = trimSpTab(s.slice(start2, pos));
|
|
215
|
+
}
|
|
216
|
+
if (!params.has(name))
|
|
217
|
+
params.set(name, val);
|
|
218
|
+
}
|
|
219
|
+
return params;
|
|
220
|
+
}
|
|
221
|
+
function trimSpTabBytes(b) {
|
|
222
|
+
let a = 0;
|
|
223
|
+
let e = b.length;
|
|
224
|
+
while (a < e && (b[a] === 0x20 || b[a] === 0x09))
|
|
225
|
+
a++;
|
|
226
|
+
while (e > a && (b[e - 1] === 0x20 || b[e - 1] === 0x09))
|
|
227
|
+
e--;
|
|
228
|
+
return b.subarray(a, e);
|
|
229
|
+
}
|
|
230
|
+
/** 22.3 points 1-5: the parts of a multipart body. Throws InvalidMultipart. */
|
|
231
|
+
export function parseMultipart(data, boundary) {
|
|
232
|
+
if (boundary === undefined || !BOUNDARY_RE.test(boundary))
|
|
233
|
+
bad('boundary');
|
|
234
|
+
const delim = Buffer.from('--' + boundary, 'latin1');
|
|
235
|
+
const crlfDelim = Buffer.concat([CRLF, delim]);
|
|
236
|
+
let pos;
|
|
237
|
+
if (startsWith(data, delim))
|
|
238
|
+
pos = 0;
|
|
239
|
+
else {
|
|
240
|
+
const i = data.indexOf(crlfDelim);
|
|
241
|
+
if (i < 0)
|
|
242
|
+
bad('no delimiter');
|
|
243
|
+
pos = i + 2;
|
|
244
|
+
}
|
|
245
|
+
const parts = [];
|
|
246
|
+
for (;;) {
|
|
247
|
+
pos += delim.length;
|
|
248
|
+
if (data[pos] === 0x2d && data[pos + 1] === 0x2d)
|
|
249
|
+
break;
|
|
250
|
+
while (pos < data.length && (data[pos] === 0x20 || data[pos] === 0x09))
|
|
251
|
+
pos++;
|
|
252
|
+
if (!(data[pos] === 0x0d && data[pos + 1] === 0x0a))
|
|
253
|
+
bad('delimiter line');
|
|
254
|
+
pos += 2;
|
|
255
|
+
const nxt = data.indexOf(crlfDelim, pos);
|
|
256
|
+
if (nxt < 0)
|
|
257
|
+
bad('no close delimiter');
|
|
258
|
+
parts.push(data.subarray(pos, nxt));
|
|
259
|
+
pos = nxt + 2;
|
|
260
|
+
}
|
|
261
|
+
const out = [];
|
|
262
|
+
for (const p of parts) {
|
|
263
|
+
let head;
|
|
264
|
+
let content;
|
|
265
|
+
if (p[0] === 0x0d && p[1] === 0x0a) {
|
|
266
|
+
head = p.subarray(0, 0);
|
|
267
|
+
content = p.subarray(2);
|
|
268
|
+
}
|
|
269
|
+
else {
|
|
270
|
+
const j = p.indexOf(CRLFCRLF);
|
|
271
|
+
if (j < 0)
|
|
272
|
+
bad('part headers');
|
|
273
|
+
head = p.subarray(0, j);
|
|
274
|
+
content = p.subarray(j + 4);
|
|
275
|
+
}
|
|
276
|
+
const headers = new Map();
|
|
277
|
+
if (head.length > 0) {
|
|
278
|
+
let start = 0;
|
|
279
|
+
for (;;) {
|
|
280
|
+
const e = head.indexOf(CRLF, start);
|
|
281
|
+
const line = head.subarray(start, e < 0 ? head.length : e);
|
|
282
|
+
const k = line.indexOf(0x3a);
|
|
283
|
+
if (k <= 0)
|
|
284
|
+
bad('part header line');
|
|
285
|
+
const nm = line.subarray(0, k).toString('latin1');
|
|
286
|
+
if (!PART_HEADER_NAME_RE.test(nm))
|
|
287
|
+
bad('part header line');
|
|
288
|
+
const lname = nm.toLowerCase();
|
|
289
|
+
if (!headers.has(lname))
|
|
290
|
+
headers.set(lname, trimSpTabBytes(line.subarray(k + 1)));
|
|
291
|
+
if (e < 0)
|
|
292
|
+
break;
|
|
293
|
+
start = e + 2;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
const cd = headers.get('content-disposition');
|
|
297
|
+
if (cd === undefined)
|
|
298
|
+
bad('no content-disposition');
|
|
299
|
+
const cds = decodeUtf8(cd);
|
|
300
|
+
if (cds === null)
|
|
301
|
+
bad('content-disposition not UTF-8');
|
|
302
|
+
const params = disposition(cds);
|
|
303
|
+
const name = params.get('name');
|
|
304
|
+
if (name === undefined)
|
|
305
|
+
bad('no name');
|
|
306
|
+
const isFile = params.has('filename') || params.has('filename*');
|
|
307
|
+
let ct = null;
|
|
308
|
+
const ctb = headers.get('content-type');
|
|
309
|
+
if (ctb !== undefined) {
|
|
310
|
+
const cts = decodeUtf8(ctb);
|
|
311
|
+
if (cts !== null)
|
|
312
|
+
ct = trimSpTab(cts.split(';', 1)[0]).toLowerCase();
|
|
313
|
+
}
|
|
314
|
+
out.push({ name: name, isFile, content, ct });
|
|
315
|
+
}
|
|
316
|
+
return out;
|
|
317
|
+
}
|
|
318
|
+
/** 22.3: drop:file:<kind>:<class> of a file part. */
|
|
319
|
+
export function fileToken(content, ct) {
|
|
320
|
+
const kind = magicKind(content) ?? FILE_TYPES.get(ct ?? '') ?? 'bin';
|
|
321
|
+
return 'drop:file:' + kind + ':' + docClass(content.length);
|
|
322
|
+
}
|
|
323
|
+
// ------------------------------------------------------------------ XML (22.2)
|
|
324
|
+
export const NS_XML = 'http://www.w3.org/XML/1998/namespace';
|
|
325
|
+
export const SOAP11 = 'http://schemas.xmlsoap.org/soap/envelope/';
|
|
326
|
+
export const SOAP12 = 'http://www.w3.org/2003/05/soap-envelope';
|
|
327
|
+
export const XML_MAX_NESTING = 256;
|
|
328
|
+
const NS_MAX_BYTES = 512;
|
|
329
|
+
export class InvalidXml extends Error {
|
|
330
|
+
}
|
|
331
|
+
const xbad = (why) => {
|
|
332
|
+
throw new InvalidXml(why);
|
|
333
|
+
};
|
|
334
|
+
function nameStart(c) {
|
|
335
|
+
return (c >= 0x41 && c <= 0x5a) || (c >= 0x61 && c <= 0x7a) || c === 0x5f || c >= 0x80;
|
|
336
|
+
}
|
|
337
|
+
function nameChar(c) {
|
|
338
|
+
return nameStart(c) || (c >= 0x30 && c <= 0x39) || c === 0x2e || c === 0x2d || c === 0x3a;
|
|
339
|
+
}
|
|
340
|
+
function xmlChar(cp) {
|
|
341
|
+
return cp === 0x9 || cp === 0xa || cp === 0xd || (cp >= 0x20 && cp <= 0xd7ff) || (cp >= 0xe000 && cp <= 0xfffd) || (cp >= 0x10000 && cp <= 0x10ffff);
|
|
342
|
+
}
|
|
343
|
+
function isWs(c) {
|
|
344
|
+
return c === 0x20 || c === 0x09 || c === 0x0a;
|
|
345
|
+
}
|
|
346
|
+
const ENTITIES = new Map([['lt', '<'], ['gt', '>'], ['amp', '&'], ['apos', "'"], ['quot', '"']]);
|
|
347
|
+
const HEX_DIGITS = /^[0-9a-fA-F]+$/;
|
|
348
|
+
const DEC_DIGITS = /^[0-9]+$/;
|
|
349
|
+
/** Character and predefined entity references; attribute values turn tab and newline into spaces. */
|
|
350
|
+
function refs(raw, attr) {
|
|
351
|
+
if (raw.indexOf('&') < 0 && !(attr && /[\t\n]/.test(raw)))
|
|
352
|
+
return raw;
|
|
353
|
+
let out = '';
|
|
354
|
+
let i = 0;
|
|
355
|
+
const n = raw.length;
|
|
356
|
+
while (i < n) {
|
|
357
|
+
const c = raw[i];
|
|
358
|
+
if (c === '&') {
|
|
359
|
+
const j = raw.indexOf(';', i + 1);
|
|
360
|
+
if (j < 0)
|
|
361
|
+
xbad('unterminated reference');
|
|
362
|
+
const ent = raw.slice(i + 1, j);
|
|
363
|
+
const named = ENTITIES.get(ent);
|
|
364
|
+
if (named !== undefined)
|
|
365
|
+
out += named;
|
|
366
|
+
else {
|
|
367
|
+
let digits;
|
|
368
|
+
let radix;
|
|
369
|
+
if (ent.startsWith('#x') && ent.length > 2 && HEX_DIGITS.test(ent.slice(2))) {
|
|
370
|
+
digits = ent.slice(2);
|
|
371
|
+
radix = 16;
|
|
372
|
+
}
|
|
373
|
+
else if (ent.startsWith('#') && ent.length > 1 && DEC_DIGITS.test(ent.slice(1))) {
|
|
374
|
+
digits = ent.slice(1);
|
|
375
|
+
radix = 10;
|
|
376
|
+
}
|
|
377
|
+
else {
|
|
378
|
+
return xbad('unknown entity');
|
|
379
|
+
}
|
|
380
|
+
let cp = 0;
|
|
381
|
+
for (const d of digits) {
|
|
382
|
+
cp = cp * radix + parseInt(d, 16);
|
|
383
|
+
if (cp > 0x10ffff)
|
|
384
|
+
xbad('reference out of range');
|
|
385
|
+
}
|
|
386
|
+
if (!xmlChar(cp))
|
|
387
|
+
xbad('reference to a forbidden character');
|
|
388
|
+
out += String.fromCodePoint(cp);
|
|
389
|
+
}
|
|
390
|
+
i = j + 1;
|
|
391
|
+
continue;
|
|
392
|
+
}
|
|
393
|
+
out += attr && (c === '\t' || c === '\n') ? ' ' : c;
|
|
394
|
+
i++;
|
|
395
|
+
}
|
|
396
|
+
return out;
|
|
397
|
+
}
|
|
398
|
+
function splitQname(q) {
|
|
399
|
+
const i = q.indexOf(':');
|
|
400
|
+
if (i < 0)
|
|
401
|
+
return [null, q];
|
|
402
|
+
const p = q.slice(0, i);
|
|
403
|
+
const l = q.slice(i + 1);
|
|
404
|
+
if (!p || !l || l.includes(':'))
|
|
405
|
+
xbad('qualified name');
|
|
406
|
+
return [p, l];
|
|
407
|
+
}
|
|
408
|
+
/** 22.2 points 1-6 -> root element. Throws InvalidXml for anything outside the accepted subset. */
|
|
409
|
+
export function parseXml(input) {
|
|
410
|
+
let data = input;
|
|
411
|
+
if (data.length >= 3 && data[0] === 0xef && data[1] === 0xbb && data[2] === 0xbf)
|
|
412
|
+
data = data.subarray(3);
|
|
413
|
+
const decoded = decodeUtf8(data);
|
|
414
|
+
if (decoded === null)
|
|
415
|
+
xbad('not UTF-8');
|
|
416
|
+
let s = decoded;
|
|
417
|
+
for (let i = 0; i < s.length; i++) {
|
|
418
|
+
const c = s.charCodeAt(i);
|
|
419
|
+
// Valid UTF-8 never yields lone surrogates: only pairs, which are allowed.
|
|
420
|
+
if ((c < 0x20 && c !== 9 && c !== 10 && c !== 13) || c === 0xfffe || c === 0xffff)
|
|
421
|
+
xbad('forbidden character');
|
|
422
|
+
}
|
|
423
|
+
if (s.indexOf('\r') >= 0)
|
|
424
|
+
s = s.replace(/\r\n?/g, '\n');
|
|
425
|
+
const n = s.length;
|
|
426
|
+
let pos = 0;
|
|
427
|
+
const readName = (p) => {
|
|
428
|
+
if (p >= n || !nameStart(s.charCodeAt(p)))
|
|
429
|
+
xbad('name');
|
|
430
|
+
let q = p + 1;
|
|
431
|
+
while (q < n && nameChar(s.charCodeAt(q)))
|
|
432
|
+
q++;
|
|
433
|
+
return [s.slice(p, q), q];
|
|
434
|
+
};
|
|
435
|
+
const skipWs = (p) => {
|
|
436
|
+
while (p < n && isWs(s.charCodeAt(p)))
|
|
437
|
+
p++;
|
|
438
|
+
return p;
|
|
439
|
+
};
|
|
440
|
+
/** Comment or processing instruction at p -> its end, or -1 if none starts there. */
|
|
441
|
+
const misc = (p) => {
|
|
442
|
+
if (s.startsWith('<!--', p)) {
|
|
443
|
+
const e = s.indexOf('-->', p + 4);
|
|
444
|
+
if (e < 0)
|
|
445
|
+
xbad('unterminated comment');
|
|
446
|
+
return e + 3;
|
|
447
|
+
}
|
|
448
|
+
if (s.startsWith('<?', p)) {
|
|
449
|
+
const e = s.indexOf('?>', p + 2);
|
|
450
|
+
if (e < 0)
|
|
451
|
+
xbad('unterminated processing instruction');
|
|
452
|
+
return e + 2;
|
|
453
|
+
}
|
|
454
|
+
return -1;
|
|
455
|
+
};
|
|
456
|
+
const startTag = (p0, parentNs) => {
|
|
457
|
+
let [qname, p] = readName(p0 + 1);
|
|
458
|
+
const rawAttrs = [];
|
|
459
|
+
const seen = new Set();
|
|
460
|
+
let empty = false;
|
|
461
|
+
for (;;) {
|
|
462
|
+
let q = skipWs(p);
|
|
463
|
+
if (s.startsWith('/>', q)) {
|
|
464
|
+
p = q + 2;
|
|
465
|
+
empty = true;
|
|
466
|
+
break;
|
|
467
|
+
}
|
|
468
|
+
if (s.startsWith('>', q)) {
|
|
469
|
+
p = q + 1;
|
|
470
|
+
break;
|
|
471
|
+
}
|
|
472
|
+
if (q === p)
|
|
473
|
+
xbad('attribute without space');
|
|
474
|
+
let aname;
|
|
475
|
+
[aname, q] = readName(q);
|
|
476
|
+
q = skipWs(q);
|
|
477
|
+
if (q >= n || s[q] !== '=')
|
|
478
|
+
xbad('attribute without =');
|
|
479
|
+
q = skipWs(q + 1);
|
|
480
|
+
if (q >= n || (s[q] !== '"' && s[q] !== "'"))
|
|
481
|
+
xbad('attribute value');
|
|
482
|
+
const e = s.indexOf(s[q], q + 1);
|
|
483
|
+
if (e < 0)
|
|
484
|
+
xbad('unterminated attribute value');
|
|
485
|
+
const raw = s.slice(q + 1, e);
|
|
486
|
+
if (raw.includes('<'))
|
|
487
|
+
xbad('< in attribute value');
|
|
488
|
+
if (seen.has(aname))
|
|
489
|
+
xbad('duplicate attribute');
|
|
490
|
+
seen.add(aname);
|
|
491
|
+
rawAttrs.push([aname, refs(raw, true)]);
|
|
492
|
+
p = e + 1;
|
|
493
|
+
}
|
|
494
|
+
let nsmap = parentNs;
|
|
495
|
+
for (const [aname, v] of rawAttrs) {
|
|
496
|
+
let pre = null;
|
|
497
|
+
if (aname === 'xmlns')
|
|
498
|
+
pre = '';
|
|
499
|
+
else if (aname.startsWith('xmlns:')) {
|
|
500
|
+
pre = aname.slice(6);
|
|
501
|
+
if (!pre || pre.includes(':'))
|
|
502
|
+
xbad('namespace declaration');
|
|
503
|
+
}
|
|
504
|
+
if (pre === null)
|
|
505
|
+
continue;
|
|
506
|
+
if (nsmap === parentNs)
|
|
507
|
+
nsmap = new Map(parentNs);
|
|
508
|
+
nsmap.set(pre, v);
|
|
509
|
+
}
|
|
510
|
+
const [prefix, local] = splitQname(qname);
|
|
511
|
+
let ns;
|
|
512
|
+
if (prefix === null)
|
|
513
|
+
ns = nsmap.get('') ?? '';
|
|
514
|
+
else if (prefix === 'xml')
|
|
515
|
+
ns = NS_XML;
|
|
516
|
+
else if (nsmap.has(prefix))
|
|
517
|
+
ns = nsmap.get(prefix);
|
|
518
|
+
else
|
|
519
|
+
return xbad('undeclared prefix');
|
|
520
|
+
const attrs = [];
|
|
521
|
+
for (const [aname, v] of rawAttrs) {
|
|
522
|
+
if (aname === 'xmlns' || aname.startsWith('xmlns:'))
|
|
523
|
+
continue;
|
|
524
|
+
const [ap, al] = splitQname(aname);
|
|
525
|
+
if (ap !== null && ap !== 'xml' && !nsmap.has(ap))
|
|
526
|
+
xbad('undeclared attribute prefix');
|
|
527
|
+
attrs.push([al, v]);
|
|
528
|
+
}
|
|
529
|
+
return { qname, el: { local, ns, attrs, children: [], text: [] }, nsmap, empty, pos: p };
|
|
530
|
+
};
|
|
531
|
+
// prolog
|
|
532
|
+
for (;;) {
|
|
533
|
+
pos = skipWs(pos);
|
|
534
|
+
if (pos >= n)
|
|
535
|
+
xbad('no root element');
|
|
536
|
+
const e = misc(pos);
|
|
537
|
+
if (e >= 0) {
|
|
538
|
+
pos = e;
|
|
539
|
+
continue;
|
|
540
|
+
}
|
|
541
|
+
if (s.startsWith('<!', pos) || s[pos] !== '<')
|
|
542
|
+
xbad('prolog');
|
|
543
|
+
break;
|
|
544
|
+
}
|
|
545
|
+
const stack = [];
|
|
546
|
+
const first = startTag(pos, new Map());
|
|
547
|
+
const root = first.el;
|
|
548
|
+
pos = first.pos;
|
|
549
|
+
if (!first.empty)
|
|
550
|
+
stack.push({ qname: first.qname, el: first.el, nsmap: first.nsmap });
|
|
551
|
+
while (stack.length > 0) {
|
|
552
|
+
if (pos >= n)
|
|
553
|
+
xbad('unclosed element');
|
|
554
|
+
const top = stack[stack.length - 1];
|
|
555
|
+
if (s.charCodeAt(pos) !== 0x3c) {
|
|
556
|
+
const e = s.indexOf('<', pos);
|
|
557
|
+
if (e < 0)
|
|
558
|
+
xbad('unclosed element');
|
|
559
|
+
top.el.text.push(refs(s.slice(pos, e), false));
|
|
560
|
+
pos = e;
|
|
561
|
+
continue;
|
|
562
|
+
}
|
|
563
|
+
if (s.startsWith('<![CDATA[', pos)) {
|
|
564
|
+
const e = s.indexOf(']]>', pos + 9);
|
|
565
|
+
if (e < 0)
|
|
566
|
+
xbad('unterminated CDATA');
|
|
567
|
+
top.el.text.push(s.slice(pos + 9, e));
|
|
568
|
+
pos = e + 3;
|
|
569
|
+
continue;
|
|
570
|
+
}
|
|
571
|
+
const e = misc(pos);
|
|
572
|
+
if (e >= 0) {
|
|
573
|
+
pos = e;
|
|
574
|
+
continue;
|
|
575
|
+
}
|
|
576
|
+
if (s.startsWith('<!', pos))
|
|
577
|
+
xbad('declaration in content');
|
|
578
|
+
if (s.startsWith('</', pos)) {
|
|
579
|
+
const [name, q0] = readName(pos + 2);
|
|
580
|
+
const q = skipWs(q0);
|
|
581
|
+
if (q >= n || s[q] !== '>' || name !== top.qname)
|
|
582
|
+
xbad('end tag');
|
|
583
|
+
stack.pop();
|
|
584
|
+
pos = q + 1;
|
|
585
|
+
continue;
|
|
586
|
+
}
|
|
587
|
+
const t = startTag(pos, top.nsmap);
|
|
588
|
+
top.el.children.push(t.el);
|
|
589
|
+
pos = t.pos;
|
|
590
|
+
if (!t.empty) {
|
|
591
|
+
if (stack.length >= XML_MAX_NESTING)
|
|
592
|
+
xbad('nesting');
|
|
593
|
+
stack.push({ qname: t.qname, el: t.el, nsmap: t.nsmap });
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
// epilogue
|
|
597
|
+
for (;;) {
|
|
598
|
+
pos = skipWs(pos);
|
|
599
|
+
if (pos >= n)
|
|
600
|
+
break;
|
|
601
|
+
const e = misc(pos);
|
|
602
|
+
if (e < 0)
|
|
603
|
+
xbad('content after the root element');
|
|
604
|
+
pos = e;
|
|
605
|
+
}
|
|
606
|
+
return root;
|
|
607
|
+
}
|
|
608
|
+
function hasNonSpace(t) {
|
|
609
|
+
for (let i = 0; i < t.length; i++) {
|
|
610
|
+
const c = t.charCodeAt(i);
|
|
611
|
+
if (c !== 0x20 && c !== 0x09 && c !== 0x0a && c !== 0x0d)
|
|
612
|
+
return true;
|
|
613
|
+
}
|
|
614
|
+
return false;
|
|
615
|
+
}
|
|
616
|
+
export function xmlValue(el) {
|
|
617
|
+
if (el.attrs.length === 0 && el.children.length === 0)
|
|
618
|
+
return el.text.join('');
|
|
619
|
+
let members = [];
|
|
620
|
+
const index = new Map();
|
|
621
|
+
for (const [local, v] of el.attrs) {
|
|
622
|
+
// Same local name twice: the last wins, at its own position.
|
|
623
|
+
const k = '@' + local;
|
|
624
|
+
const prev = index.get(k);
|
|
625
|
+
if (prev !== undefined)
|
|
626
|
+
members[prev] = null;
|
|
627
|
+
index.set(k, members.length);
|
|
628
|
+
members.push([k, v]);
|
|
629
|
+
}
|
|
630
|
+
members = members.filter((m) => m !== null);
|
|
631
|
+
const groups = new Map();
|
|
632
|
+
for (const c of el.children) {
|
|
633
|
+
let g = groups.get(c.local);
|
|
634
|
+
if (!g) {
|
|
635
|
+
g = [];
|
|
636
|
+
groups.set(c.local, g);
|
|
637
|
+
}
|
|
638
|
+
g.push(xmlValue(c));
|
|
639
|
+
}
|
|
640
|
+
for (const [k, vs] of groups)
|
|
641
|
+
members.push([k, vs.length === 1 ? vs[0] : vs]);
|
|
642
|
+
const text = el.text.join('');
|
|
643
|
+
if (hasNonSpace(text))
|
|
644
|
+
members.push(['#text', text]);
|
|
645
|
+
return { members: members };
|
|
646
|
+
}
|
|
647
|
+
const NS_RUN_RE = /[A-Za-z0-9_-]+/g;
|
|
648
|
+
const NS_BAD_CHAR = /[^!-~]|[@{}]/;
|
|
649
|
+
/** 22.2 point 9: 'ns:' + pattern of a namespace URI. */
|
|
650
|
+
export function nsPattern(uri) {
|
|
651
|
+
if (uri === '')
|
|
652
|
+
return 'ns:{none}';
|
|
653
|
+
if (Buffer.byteLength(uri, 'utf8') > NS_MAX_BYTES || NS_BAD_CHAR.test(uri))
|
|
654
|
+
return 'ns:{*}';
|
|
655
|
+
return 'ns:' + uri.replace(NS_RUN_RE, (t) => (t.length > 64 || /[0-9]{6}/.test(t) || /[0-9A-Fa-f]{12}/.test(t) ? '{*}' : t));
|
|
656
|
+
}
|
|
657
|
+
/** 22.2 points 9-10: xmlns entries [(path, token)] in document order (no limit) and fmt. */
|
|
658
|
+
export function xmlNamespaces(root, literal, maxDepth) {
|
|
659
|
+
const entries = [];
|
|
660
|
+
const seen = new Set();
|
|
661
|
+
const visit = (el, path, parentNs, depth) => {
|
|
662
|
+
if (depth > maxDepth)
|
|
663
|
+
return;
|
|
664
|
+
if (el.ns !== parentNs && !seen.has(path)) {
|
|
665
|
+
seen.add(path);
|
|
666
|
+
entries.push([path, nsPattern(el.ns)]);
|
|
667
|
+
}
|
|
668
|
+
const counts = new Map();
|
|
669
|
+
for (const c of el.children)
|
|
670
|
+
counts.set(c.local, (counts.get(c.local) ?? 0) + 1);
|
|
671
|
+
for (const c of el.children) {
|
|
672
|
+
const p = path + (literal(c.local) ? '.' + c.local : '.*') + (counts.get(c.local) > 1 ? '[*]' : '');
|
|
673
|
+
visit(c, p, el.ns, depth + 1);
|
|
674
|
+
}
|
|
675
|
+
};
|
|
676
|
+
visit(root, '$' + (literal(root.local) ? '.' + root.local : '.*'), '', 1);
|
|
677
|
+
let fmt = 'xml';
|
|
678
|
+
if (root.local === 'Envelope' && root.ns === SOAP11)
|
|
679
|
+
fmt = 'soap11';
|
|
680
|
+
else if (root.local === 'Envelope' && root.ns === SOAP12)
|
|
681
|
+
fmt = 'soap12';
|
|
682
|
+
return { entries, fmt };
|
|
683
|
+
}
|
|
684
|
+
/** 22.2 point 11: the operation name of a SOAPAction value, or null when empty. */
|
|
685
|
+
export function soapActionName(raw, literal) {
|
|
686
|
+
let s = trimSpTab(raw);
|
|
687
|
+
if (s.length >= 2 && s.startsWith('"') && s.endsWith('"'))
|
|
688
|
+
s = s.slice(1, -1);
|
|
689
|
+
if (s === '')
|
|
690
|
+
return null;
|
|
691
|
+
const cut = Math.max(s.lastIndexOf('/'), s.lastIndexOf('#'), s.lastIndexOf(':'));
|
|
692
|
+
const name = s.slice(cut + 1);
|
|
693
|
+
return literal(name) ? name : '{*}';
|
|
694
|
+
}
|