@patchstack/connect 0.5.6 → 0.5.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/chunk-2K5SHFUM.js +295 -0
- package/dist/chunk-2K5SHFUM.js.map +1 -0
- package/dist/cli.js +1001 -466
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +364 -39
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +6 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +375 -37
- package/dist/index.js.map +1 -1
- package/dist/protect/templates/demo-rules.json +1 -1
- package/dist/protect.cjs +1135 -241
- package/dist/protect.cjs.map +1 -1
- package/dist/protect.d.cts +7 -2
- package/dist/protect.d.ts +7 -2
- package/dist/protect.edge.js +988 -179
- package/dist/protect.edge.js.map +3 -3
- package/dist/protect.js +842 -192
- package/dist/protect.js.map +1 -1
- package/dist/{refresh-manifest-2ALIJGZ3.js → refresh-manifest-3YWIYENU.js} +82 -39
- package/dist/refresh-manifest-3YWIYENU.js.map +1 -0
- package/package.json +1 -1
- package/dist/chunk-IOODP4ZB.js +0 -104
- package/dist/chunk-IOODP4ZB.js.map +0 -1
- package/dist/refresh-manifest-2ALIJGZ3.js.map +0 -1
package/dist/protect.js
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import {
|
|
2
2
|
canonicalBuildId,
|
|
3
|
+
isSafeOrigin,
|
|
3
4
|
pulseAuthHeader,
|
|
4
5
|
pulseFetch,
|
|
5
|
-
|
|
6
|
-
|
|
6
|
+
readBoundedJson,
|
|
7
|
+
readBuildStamp,
|
|
8
|
+
safeBaseUrl
|
|
9
|
+
} from "./chunk-2K5SHFUM.js";
|
|
7
10
|
|
|
8
11
|
// src/protect/client-ip.js
|
|
9
12
|
var ZONE = /^[A-Za-z0-9._-]{1,64}$/;
|
|
@@ -219,27 +222,49 @@ function clientIpFields(resolved) {
|
|
|
219
222
|
return established && address !== null ? { client_ip: address, client_ip_source: resolved.source } : { client_ip_source: "unavailable" };
|
|
220
223
|
}
|
|
221
224
|
|
|
222
|
-
// src/protect/
|
|
223
|
-
|
|
225
|
+
// src/protect/engine/response-json.js
|
|
226
|
+
var MAX_RULE_RESPONSE_BYTES = 5 * 1024 * 1024;
|
|
227
|
+
async function readBoundedJson2(response, maxBytes = MAX_RULE_RESPONSE_BYTES) {
|
|
228
|
+
const stated = Number(response.headers?.get?.("content-length"));
|
|
229
|
+
if (Number.isFinite(stated) && stated > maxBytes) {
|
|
230
|
+
throw new Error(`rule response exceeds ${maxBytes} bytes`);
|
|
231
|
+
}
|
|
232
|
+
const stream = response.body;
|
|
233
|
+
if (!stream || typeof stream.getReader !== "function") {
|
|
234
|
+
if (typeof response.text === "function") {
|
|
235
|
+
const text = await response.text();
|
|
236
|
+
if (new TextEncoder().encode(text).byteLength > maxBytes) {
|
|
237
|
+
throw new Error(`rule response exceeds ${maxBytes} bytes`);
|
|
238
|
+
}
|
|
239
|
+
return JSON.parse(text);
|
|
240
|
+
}
|
|
241
|
+
throw new Error("rule response body cannot be read with a size bound");
|
|
242
|
+
}
|
|
243
|
+
const reader = stream.getReader();
|
|
244
|
+
const chunks = [];
|
|
245
|
+
let total = 0;
|
|
224
246
|
try {
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
247
|
+
for (; ; ) {
|
|
248
|
+
const { done, value } = await reader.read();
|
|
249
|
+
if (done) break;
|
|
250
|
+
total += value.byteLength;
|
|
251
|
+
if (total > maxBytes) {
|
|
252
|
+
void reader.cancel().catch(() => {
|
|
253
|
+
});
|
|
254
|
+
throw new Error(`rule response exceeds ${maxBytes} bytes`);
|
|
255
|
+
}
|
|
256
|
+
chunks.push(value);
|
|
257
|
+
}
|
|
258
|
+
} finally {
|
|
259
|
+
reader.releaseLock();
|
|
230
260
|
}
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
var warned = /* @__PURE__ */ new Set();
|
|
239
|
-
function warnOnce(key, message) {
|
|
240
|
-
if (warned.has(key)) return;
|
|
241
|
-
warned.add(key);
|
|
242
|
-
console.warn(message);
|
|
261
|
+
const bytes = new Uint8Array(total);
|
|
262
|
+
let offset = 0;
|
|
263
|
+
for (const chunk of chunks) {
|
|
264
|
+
bytes.set(chunk, offset);
|
|
265
|
+
offset += chunk.byteLength;
|
|
266
|
+
}
|
|
267
|
+
return JSON.parse(new TextDecoder().decode(bytes));
|
|
243
268
|
}
|
|
244
269
|
|
|
245
270
|
// src/protect/engine/client.js
|
|
@@ -298,7 +323,7 @@ var PatchstackRuleClient = class {
|
|
|
298
323
|
whitelist_keys: {}
|
|
299
324
|
};
|
|
300
325
|
}
|
|
301
|
-
const data = await response
|
|
326
|
+
const data = await readBoundedJson2(response);
|
|
302
327
|
if (!data || !Array.isArray(data.firewall)) {
|
|
303
328
|
return { success: false, error: "unexpected response shape (no firewall array)", firewall: [], whitelists: [], whitelist_keys: {} };
|
|
304
329
|
}
|
|
@@ -335,6 +360,22 @@ var PatchstackRuleClient = class {
|
|
|
335
360
|
}
|
|
336
361
|
};
|
|
337
362
|
|
|
363
|
+
// src/protect/engine/own.js
|
|
364
|
+
function setOwn(target, key, value) {
|
|
365
|
+
Object.defineProperty(target, key, {
|
|
366
|
+
configurable: true,
|
|
367
|
+
enumerable: true,
|
|
368
|
+
writable: true,
|
|
369
|
+
value
|
|
370
|
+
});
|
|
371
|
+
return value;
|
|
372
|
+
}
|
|
373
|
+
function appendOwn(target, key, value) {
|
|
374
|
+
if (!Object.hasOwn(target, key)) return setOwn(target, key, value);
|
|
375
|
+
const previous = target[key];
|
|
376
|
+
return setOwn(target, key, Array.isArray(previous) ? [...previous, value] : [previous, value]);
|
|
377
|
+
}
|
|
378
|
+
|
|
338
379
|
// src/protect/engine/request.js
|
|
339
380
|
var FILE_ATTRS = /* @__PURE__ */ new Set(["content", "filename", "type"]);
|
|
340
381
|
var fileFilename = (f) => f && typeof f === "object" ? f.filename : f;
|
|
@@ -453,8 +494,8 @@ var RequestResolver = class {
|
|
|
453
494
|
}
|
|
454
495
|
if (key.startsWith("header.")) {
|
|
455
496
|
const name = key.slice("header.".length).toLowerCase();
|
|
456
|
-
const
|
|
457
|
-
return
|
|
497
|
+
const headers = resp.headers ?? {};
|
|
498
|
+
return Object.hasOwn(headers, name) ? [headers[name]] : [];
|
|
458
499
|
}
|
|
459
500
|
return [];
|
|
460
501
|
}
|
|
@@ -549,7 +590,7 @@ var RequestResolver = class {
|
|
|
549
590
|
...this.#resolveWildcard(cookies, key)
|
|
550
591
|
];
|
|
551
592
|
}
|
|
552
|
-
const value = this.#getNestedValue(query, key) ?? this.#getNestedValue(body, key) ?? cookies[key];
|
|
593
|
+
const value = this.#getNestedValue(query, key) ?? this.#getNestedValue(body, key) ?? (Object.hasOwn(cookies, key) ? cookies[key] : void 0);
|
|
553
594
|
return value !== void 0 ? [value] : [];
|
|
554
595
|
}
|
|
555
596
|
#resolveCookie(key) {
|
|
@@ -557,8 +598,7 @@ var RequestResolver = class {
|
|
|
557
598
|
if (key.endsWith("*")) {
|
|
558
599
|
return this.#resolveWildcard(cookies, key);
|
|
559
600
|
}
|
|
560
|
-
|
|
561
|
-
return value !== void 0 ? [value] : [];
|
|
601
|
+
return Object.hasOwn(cookies, key) ? [cookies[key]] : [];
|
|
562
602
|
}
|
|
563
603
|
#resolveServer(key) {
|
|
564
604
|
const req = this.#req;
|
|
@@ -734,7 +774,7 @@ var RequestResolver = class {
|
|
|
734
774
|
}
|
|
735
775
|
const name = pair.substring(0, eqIndex).trim();
|
|
736
776
|
const value = pair.substring(eqIndex + 1).trim();
|
|
737
|
-
cookies
|
|
777
|
+
setOwn(cookies, name, value);
|
|
738
778
|
}
|
|
739
779
|
this.#cookies = cookies;
|
|
740
780
|
return this.#cookies;
|
|
@@ -749,17 +789,17 @@ function notify(fn, arg, label) {
|
|
|
749
789
|
const result = fn(arg);
|
|
750
790
|
if (result !== null && typeof result === "object" && typeof result.then === "function") {
|
|
751
791
|
try {
|
|
752
|
-
result.then(void 0, (err) =>
|
|
792
|
+
result.then(void 0, (err) => warnOnce(label, err));
|
|
753
793
|
} catch {
|
|
754
794
|
}
|
|
755
795
|
}
|
|
756
796
|
return true;
|
|
757
797
|
} catch (err) {
|
|
758
|
-
|
|
798
|
+
warnOnce(label, err);
|
|
759
799
|
return false;
|
|
760
800
|
}
|
|
761
801
|
}
|
|
762
|
-
function
|
|
802
|
+
function warnOnce(label, err) {
|
|
763
803
|
if (reported.has(label)) return;
|
|
764
804
|
reported.add(label);
|
|
765
805
|
try {
|
|
@@ -891,30 +931,113 @@ function normalizeWhitespace(value) {
|
|
|
891
931
|
result = result.replace(/ {2,}/g, " ");
|
|
892
932
|
return result;
|
|
893
933
|
}
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
}
|
|
904
|
-
|
|
905
|
-
|
|
934
|
+
var MAX_RAW_CHARS = 1024 * 1024;
|
|
935
|
+
var MAX_RAW_NODES = 25e3;
|
|
936
|
+
var MAX_RAW_DEPTH = 200;
|
|
937
|
+
var MAX_RAW_ENTRIES = 2e3;
|
|
938
|
+
function serializeForRawDetection(body) {
|
|
939
|
+
if (body === null || body === void 0) return "";
|
|
940
|
+
if (typeof body === "string") return body.slice(0, MAX_RAW_CHARS);
|
|
941
|
+
const visited = /* @__PURE__ */ new Set();
|
|
942
|
+
const output = [];
|
|
943
|
+
const stack = [{ kind: "value", value: body, depth: 0, root: true }];
|
|
944
|
+
let chars = 0;
|
|
945
|
+
let nodes = 0;
|
|
946
|
+
const append = (text) => {
|
|
947
|
+
if (chars >= MAX_RAW_CHARS) return;
|
|
948
|
+
const piece = String(text).slice(0, MAX_RAW_CHARS - chars);
|
|
949
|
+
output.push(piece);
|
|
950
|
+
chars += piece.length;
|
|
951
|
+
};
|
|
952
|
+
while (stack.length > 0 && chars < MAX_RAW_CHARS) {
|
|
953
|
+
const task = stack.pop();
|
|
954
|
+
if (task.kind === "text") {
|
|
955
|
+
append(task.text);
|
|
956
|
+
continue;
|
|
957
|
+
}
|
|
958
|
+
if (task.kind === "key") {
|
|
959
|
+
append(boundedJsonString(task.value));
|
|
960
|
+
append(":");
|
|
961
|
+
continue;
|
|
962
|
+
}
|
|
963
|
+
nodes++;
|
|
964
|
+
if (nodes > MAX_RAW_NODES) {
|
|
965
|
+
append('"[NodeLimit]"');
|
|
966
|
+
continue;
|
|
967
|
+
}
|
|
968
|
+
const value = task.value;
|
|
969
|
+
if (value === null || value === void 0) {
|
|
970
|
+
append(task.root ? "" : "null");
|
|
971
|
+
continue;
|
|
972
|
+
}
|
|
973
|
+
if (typeof value === "string") {
|
|
974
|
+
append(task.root ? value.slice(0, MAX_RAW_CHARS) : boundedJsonString(value));
|
|
975
|
+
continue;
|
|
976
|
+
}
|
|
977
|
+
if (typeof value !== "object") {
|
|
978
|
+
append(String(value).slice(0, MAX_RAW_CHARS));
|
|
979
|
+
continue;
|
|
980
|
+
}
|
|
981
|
+
if (visited.has(value)) {
|
|
982
|
+
append("[Circular]");
|
|
983
|
+
continue;
|
|
984
|
+
}
|
|
985
|
+
if (task.depth >= MAX_RAW_DEPTH) {
|
|
986
|
+
append('"[DepthLimit]"');
|
|
987
|
+
continue;
|
|
988
|
+
}
|
|
989
|
+
visited.add(value);
|
|
990
|
+
if (Array.isArray(value)) {
|
|
991
|
+
const length2 = Math.min(value.length, MAX_RAW_ENTRIES);
|
|
992
|
+
const items = [];
|
|
993
|
+
for (let i = 0; i < length2; i++) {
|
|
994
|
+
if (i > 0) items.push({ kind: "text", text: "," });
|
|
995
|
+
items.push({ kind: "value", value: readOwnValue(value, String(i)), depth: task.depth + 1, root: false });
|
|
996
|
+
}
|
|
997
|
+
if (value.length > length2) {
|
|
998
|
+
if (length2 > 0) items.push({ kind: "text", text: "," });
|
|
999
|
+
items.push({ kind: "text", text: '"[EntryLimit]"' });
|
|
1000
|
+
}
|
|
1001
|
+
stack.push({ kind: "text", text: "]" });
|
|
1002
|
+
for (let i = items.length - 1; i >= 0; i--) stack.push(items[i]);
|
|
1003
|
+
stack.push({ kind: "text", text: "[" });
|
|
1004
|
+
continue;
|
|
1005
|
+
}
|
|
1006
|
+
let keys;
|
|
1007
|
+
try {
|
|
1008
|
+
keys = Object.getOwnPropertyNames(value);
|
|
1009
|
+
} catch {
|
|
1010
|
+
append('"[Unreadable]"');
|
|
1011
|
+
continue;
|
|
1012
|
+
}
|
|
1013
|
+
const length = Math.min(keys.length, MAX_RAW_ENTRIES);
|
|
1014
|
+
const entries = [];
|
|
1015
|
+
for (let i = 0; i < length; i++) {
|
|
1016
|
+
const key = keys[i];
|
|
1017
|
+
if (i > 0) entries.push({ kind: "text", text: "," });
|
|
1018
|
+
entries.push({ kind: "key", value: key });
|
|
1019
|
+
entries.push({ kind: "value", value: readOwnValue(value, key), depth: task.depth + 1, root: false });
|
|
1020
|
+
}
|
|
1021
|
+
if (keys.length > length) {
|
|
1022
|
+
if (length > 0) entries.push({ kind: "text", text: "," });
|
|
1023
|
+
entries.push({ kind: "text", text: '"[EntryLimit]":null' });
|
|
1024
|
+
}
|
|
1025
|
+
stack.push({ kind: "text", text: "}" });
|
|
1026
|
+
for (let i = entries.length - 1; i >= 0; i--) stack.push(entries[i]);
|
|
1027
|
+
stack.push({ kind: "text", text: "{" });
|
|
906
1028
|
}
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
1029
|
+
return output.join("");
|
|
1030
|
+
}
|
|
1031
|
+
function boundedJsonString(value) {
|
|
1032
|
+
return JSON.stringify(String(value).slice(0, MAX_RAW_CHARS));
|
|
1033
|
+
}
|
|
1034
|
+
function readOwnValue(value, key) {
|
|
1035
|
+
try {
|
|
1036
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
1037
|
+
return descriptor && Object.hasOwn(descriptor, "value") ? descriptor.value : "[Accessor]";
|
|
1038
|
+
} catch {
|
|
1039
|
+
return "[Unreadable]";
|
|
911
1040
|
}
|
|
912
|
-
const keys = Object.getOwnPropertyNames(body);
|
|
913
|
-
const parts = keys.map((key) => {
|
|
914
|
-
const val = serializeForRawDetection(body[key], visited, false);
|
|
915
|
-
return JSON.stringify(key) + ":" + val;
|
|
916
|
-
});
|
|
917
|
-
return "{" + parts.join(",") + "}";
|
|
918
1041
|
}
|
|
919
1042
|
var INHERITED_ACCESSORS = /* @__PURE__ */ new Set(["headers", "query"]);
|
|
920
1043
|
function requestField(req, key) {
|
|
@@ -956,7 +1079,7 @@ function normalizeObject(value, options = {}, depth = 0) {
|
|
|
956
1079
|
if (typeof value === "object" && value !== null) {
|
|
957
1080
|
const result = {};
|
|
958
1081
|
for (const [key, val] of Object.entries(value)) {
|
|
959
|
-
result
|
|
1082
|
+
setOwn(result, key, normalizeObject(val, options, depth + 1));
|
|
960
1083
|
}
|
|
961
1084
|
return result;
|
|
962
1085
|
}
|
|
@@ -973,6 +1096,111 @@ var REDOS_PATTERNS = [
|
|
|
973
1096
|
new RegExp("\\(" + GRP + "\\|" + GRP + "\\)\\s*[+*]")
|
|
974
1097
|
// nested alternation under a quantifier
|
|
975
1098
|
];
|
|
1099
|
+
function hasAdjacentUnbounded(body, flags) {
|
|
1100
|
+
let previousUnboundedAtom = null;
|
|
1101
|
+
for (let i = 0; i < body.length; ) {
|
|
1102
|
+
const start = i;
|
|
1103
|
+
const ch = body[i];
|
|
1104
|
+
let assertion = false;
|
|
1105
|
+
let literal = null;
|
|
1106
|
+
let atom = ch;
|
|
1107
|
+
if (ch === "\\") {
|
|
1108
|
+
const escaped = body[i + 1] ?? "";
|
|
1109
|
+
assertion = /[bBAZz]/.test(escaped);
|
|
1110
|
+
if (!/[bBAZzdswDSWnrtvf0-9pPkKxuc]/.test(escaped)) literal = escaped;
|
|
1111
|
+
i += Math.min(2, body.length - i);
|
|
1112
|
+
atom = body.slice(start, i);
|
|
1113
|
+
} else if (ch === "[") {
|
|
1114
|
+
i++;
|
|
1115
|
+
while (i < body.length) {
|
|
1116
|
+
if (body[i] === "\\") i += 2;
|
|
1117
|
+
else if (body[i++] === "]") break;
|
|
1118
|
+
}
|
|
1119
|
+
atom = body.slice(start, i);
|
|
1120
|
+
} else if (ch === ")") {
|
|
1121
|
+
i++;
|
|
1122
|
+
atom = ".";
|
|
1123
|
+
} else if (ch === "(") {
|
|
1124
|
+
const lookaround = body.startsWith("(?=", i) || body.startsWith("(?!", i) || body.startsWith("(?<=", i) || body.startsWith("(?<!", i);
|
|
1125
|
+
if (lookaround) {
|
|
1126
|
+
i = groupEnd(body, i);
|
|
1127
|
+
continue;
|
|
1128
|
+
}
|
|
1129
|
+
i++;
|
|
1130
|
+
if (body.startsWith("?:", i)) i += 2;
|
|
1131
|
+
else if (body.startsWith("?<", i)) {
|
|
1132
|
+
const nameEnd = body.indexOf(">", i + 2);
|
|
1133
|
+
if (nameEnd !== -1) i = nameEnd + 1;
|
|
1134
|
+
}
|
|
1135
|
+
continue;
|
|
1136
|
+
} else if (ch === "|") {
|
|
1137
|
+
previousUnboundedAtom = null;
|
|
1138
|
+
i++;
|
|
1139
|
+
continue;
|
|
1140
|
+
} else if ("^$".includes(ch)) {
|
|
1141
|
+
i++;
|
|
1142
|
+
continue;
|
|
1143
|
+
} else if ("?*+{}".includes(ch)) {
|
|
1144
|
+
previousUnboundedAtom = null;
|
|
1145
|
+
i++;
|
|
1146
|
+
continue;
|
|
1147
|
+
} else {
|
|
1148
|
+
if (ch !== ".") literal = ch;
|
|
1149
|
+
i++;
|
|
1150
|
+
}
|
|
1151
|
+
if (assertion) continue;
|
|
1152
|
+
let unbounded = false;
|
|
1153
|
+
let mayBeEmpty = false;
|
|
1154
|
+
if (body[i] === "*" || body[i] === "+") {
|
|
1155
|
+
unbounded = true;
|
|
1156
|
+
mayBeEmpty = body[i] === "*";
|
|
1157
|
+
i++;
|
|
1158
|
+
} else if (body[i] === "{") {
|
|
1159
|
+
const quantifier = /^\{(\d+)(?:,(\d*))?\}/.exec(body.slice(i));
|
|
1160
|
+
if (quantifier) {
|
|
1161
|
+
unbounded = quantifier[2] === "";
|
|
1162
|
+
mayBeEmpty = Number(quantifier[1]) === 0;
|
|
1163
|
+
i += quantifier[0].length;
|
|
1164
|
+
}
|
|
1165
|
+
} else if (body[i] === "?") {
|
|
1166
|
+
mayBeEmpty = true;
|
|
1167
|
+
i++;
|
|
1168
|
+
}
|
|
1169
|
+
if (unbounded && (body[i] === "?" || body[i] === "+")) i++;
|
|
1170
|
+
if (unbounded && previousUnboundedAtom !== null) return true;
|
|
1171
|
+
if (unbounded) {
|
|
1172
|
+
previousUnboundedAtom = atom;
|
|
1173
|
+
} else if (previousUnboundedAtom !== null && !mayBeEmpty) {
|
|
1174
|
+
try {
|
|
1175
|
+
const atomFlags = flags.replace(/[^iu]/g, "");
|
|
1176
|
+
const complementaryClass = previousUnboundedAtom === "\\s" && atom === "\\S" || previousUnboundedAtom === "\\S" && atom === "\\s" || previousUnboundedAtom === "\\d" && atom === "\\D" || previousUnboundedAtom === "\\D" && atom === "\\d" || previousUnboundedAtom === "\\w" && atom === "\\W" || previousUnboundedAtom === "\\W" && atom === "\\w";
|
|
1177
|
+
const excludedLiteral = literal !== null && !new RegExp(`^(?:${previousUnboundedAtom})$`, atomFlags).test(literal);
|
|
1178
|
+
if (complementaryClass || excludedLiteral) {
|
|
1179
|
+
previousUnboundedAtom = null;
|
|
1180
|
+
}
|
|
1181
|
+
} catch {
|
|
1182
|
+
}
|
|
1183
|
+
}
|
|
1184
|
+
if (i === start) i++;
|
|
1185
|
+
}
|
|
1186
|
+
return false;
|
|
1187
|
+
}
|
|
1188
|
+
function groupEnd(body, start) {
|
|
1189
|
+
let depth = 0;
|
|
1190
|
+
let inClass = false;
|
|
1191
|
+
for (let i = start; i < body.length; i++) {
|
|
1192
|
+
const ch = body[i];
|
|
1193
|
+
if (ch === "\\") {
|
|
1194
|
+
i++;
|
|
1195
|
+
continue;
|
|
1196
|
+
}
|
|
1197
|
+
if (ch === "[") inClass = true;
|
|
1198
|
+
else if (ch === "]") inClass = false;
|
|
1199
|
+
else if (!inClass && ch === "(") depth++;
|
|
1200
|
+
else if (!inClass && ch === ")" && --depth === 0) return i + 1;
|
|
1201
|
+
}
|
|
1202
|
+
return body.length;
|
|
1203
|
+
}
|
|
976
1204
|
var warnedRejectedPatterns = /* @__PURE__ */ new Set();
|
|
977
1205
|
function warnRejectedPatternOnce(pattern) {
|
|
978
1206
|
const key = String(pattern);
|
|
@@ -990,15 +1218,14 @@ function safeRegExp(pattern) {
|
|
|
990
1218
|
if (typeof pattern !== "string" || pattern.length > MAX_PATTERN_LENGTH) {
|
|
991
1219
|
return null;
|
|
992
1220
|
}
|
|
993
|
-
for (const dangerous of REDOS_PATTERNS) {
|
|
994
|
-
if (dangerous.test(pattern)) {
|
|
995
|
-
return null;
|
|
996
|
-
}
|
|
997
|
-
}
|
|
998
1221
|
const match = pattern.match(/^\/(.+?)\/([gimsuy]*)$/s);
|
|
999
1222
|
if (!match) {
|
|
1000
1223
|
return null;
|
|
1001
1224
|
}
|
|
1225
|
+
for (const dangerous of REDOS_PATTERNS) {
|
|
1226
|
+
if (dangerous.test(match[1])) return null;
|
|
1227
|
+
}
|
|
1228
|
+
if (hasAdjacentUnbounded(match[1], match[2])) return null;
|
|
1002
1229
|
try {
|
|
1003
1230
|
return new RegExp(match[1], match[2]);
|
|
1004
1231
|
} catch {
|
|
@@ -1069,7 +1296,7 @@ function collectLeafValues(root, nodeCap = 2e4, maxDepth = 1e3) {
|
|
|
1069
1296
|
return out;
|
|
1070
1297
|
}
|
|
1071
1298
|
var warnedKeys = /* @__PURE__ */ new Set();
|
|
1072
|
-
function
|
|
1299
|
+
function warnOnce2(key, message) {
|
|
1073
1300
|
if (warnedKeys.has(key)) return;
|
|
1074
1301
|
warnedKeys.add(key);
|
|
1075
1302
|
console.warn(message);
|
|
@@ -1451,13 +1678,13 @@ var RuleEngine = class {
|
|
|
1451
1678
|
this.#whitelistKeys = whitelist_keys;
|
|
1452
1679
|
this.#onError = onError;
|
|
1453
1680
|
if (Array.isArray(whitelists) && whitelists.some((w) => w && Array.isArray(w.rule_v2) && !w.rule_id)) {
|
|
1454
|
-
|
|
1681
|
+
warnOnce2(
|
|
1455
1682
|
"whitelist-no-rule-id",
|
|
1456
1683
|
"[patchstack] a whitelist has no `rule_id` \u2014 it suppresses ALL rules when it matches. Scope each whitelist to a specific rule_id, and key it only on values an attacker cannot set."
|
|
1457
1684
|
);
|
|
1458
1685
|
}
|
|
1459
1686
|
if (whitelist_keys && typeof whitelist_keys === "object" && Object.keys(whitelist_keys).length > 0) {
|
|
1460
|
-
|
|
1687
|
+
warnOnce2("whitelist-keys-unimplemented", "[patchstack] `whitelist_keys` is not implemented and has no effect.");
|
|
1461
1688
|
}
|
|
1462
1689
|
}
|
|
1463
1690
|
// A mitigation engine must never take down the app it protects: any error while
|
|
@@ -1742,6 +1969,16 @@ function operandShapeProblem(shape, value) {
|
|
|
1742
1969
|
const isFilledString = (v) => typeof v === "string" && v.trim() !== "";
|
|
1743
1970
|
const isScalar = (v) => typeof v === "string" || typeof v === "number" || typeof v === "boolean";
|
|
1744
1971
|
const isHeaderName = (v) => typeof v === "string" && HEADER_NAME.test(v);
|
|
1972
|
+
const stringLimit = (v) => typeof v === "string" && v.length > LIMITS.maxValueLength ? `must not be longer than ${LIMITS.maxValueLength} chars` : null;
|
|
1973
|
+
const listLimit = (v) => Array.isArray(v) && v.length > LIMITS.maxOperandItems ? `must not have more than ${LIMITS.maxOperandItems} entries` : null;
|
|
1974
|
+
const tooLong = stringLimit(value);
|
|
1975
|
+
if (tooLong) return tooLong;
|
|
1976
|
+
const tooMany = listLimit(value);
|
|
1977
|
+
if (tooMany) return tooMany;
|
|
1978
|
+
if (Array.isArray(value)) {
|
|
1979
|
+
const longMember = value.find((member) => stringLimit(member) !== null);
|
|
1980
|
+
if (longMember !== void 0) return `contains a string longer than ${LIMITS.maxValueLength} chars`;
|
|
1981
|
+
}
|
|
1745
1982
|
switch (shape) {
|
|
1746
1983
|
case "positive-number":
|
|
1747
1984
|
return typeof value === "number" && Number.isFinite(value) && value > 0 ? null : "must be a finite positive number";
|
|
@@ -1772,8 +2009,11 @@ function operandShapeProblem(shape, value) {
|
|
|
1772
2009
|
if (value === null || typeof value !== "object" || Array.isArray(value)) return "must be an object";
|
|
1773
2010
|
const names = Object.keys(value);
|
|
1774
2011
|
if (names.length === 0) return "must be an object with at least one entry";
|
|
2012
|
+
if (names.length > LIMITS.maxMapEntries) return `must not have more than ${LIMITS.maxMapEntries} entries`;
|
|
1775
2013
|
const bad = names.find((name) => !isHeaderName(name));
|
|
1776
|
-
|
|
2014
|
+
if (bad !== void 0) return `names an invalid HTTP header, ${JSON.stringify(bad)}`;
|
|
2015
|
+
const badValue = Object.values(value).find((entry) => typeof entry !== "string" || entry.length > LIMITS.maxValueLength);
|
|
2016
|
+
return badValue === void 0 ? null : `values must be strings no longer than ${LIMITS.maxValueLength} chars`;
|
|
1777
2017
|
}
|
|
1778
2018
|
case "header-name-list": {
|
|
1779
2019
|
if (!Array.isArray(value) || value.length === 0) return "must be a non-empty list of HTTP header names";
|
|
@@ -1919,9 +2159,16 @@ var LIMITS = Object.freeze({
|
|
|
1919
2159
|
maxRules: 5e3,
|
|
1920
2160
|
maxWhitelists: 2e3,
|
|
1921
2161
|
maxConditionsPerRule: 250,
|
|
2162
|
+
maxConditionNodesPerRule: 1e3,
|
|
2163
|
+
maxConditionNodesPerBundle: 25e3,
|
|
1922
2164
|
maxNestingDepth: 12,
|
|
1923
2165
|
maxRegexLength: 1e3,
|
|
1924
|
-
maxValueLength: 8192
|
|
2166
|
+
maxValueLength: 8192,
|
|
2167
|
+
maxParameterLength: 512,
|
|
2168
|
+
maxParameterItems: 64,
|
|
2169
|
+
maxOperandItems: 256,
|
|
2170
|
+
maxMapEntries: 128,
|
|
2171
|
+
maxPathSegments: 64
|
|
1925
2172
|
});
|
|
1926
2173
|
var GROUP_PARAMETER = "rules";
|
|
1927
2174
|
function isGroup(condition) {
|
|
@@ -1948,6 +2195,9 @@ function parameterProblem(parameter) {
|
|
|
1948
2195
|
if (parameter === void 0 || parameter === null) return null;
|
|
1949
2196
|
if (Array.isArray(parameter)) {
|
|
1950
2197
|
if (parameter.length === 0) return "parameter list is empty";
|
|
2198
|
+
if (parameter.length > LIMITS.maxParameterItems) {
|
|
2199
|
+
return `parameter list has more than ${LIMITS.maxParameterItems} entries`;
|
|
2200
|
+
}
|
|
1951
2201
|
for (const member of parameter) {
|
|
1952
2202
|
if (Array.isArray(member)) return "a parameter list holds parameters, not more lists";
|
|
1953
2203
|
const problem = parameterProblem(member);
|
|
@@ -1956,6 +2206,9 @@ function parameterProblem(parameter) {
|
|
|
1956
2206
|
return null;
|
|
1957
2207
|
}
|
|
1958
2208
|
if (typeof parameter !== "string" || parameter === "") return "parameter must be a non-empty string";
|
|
2209
|
+
if (parameter.length > LIMITS.maxParameterLength) {
|
|
2210
|
+
return `parameter is longer than ${LIMITS.maxParameterLength} chars`;
|
|
2211
|
+
}
|
|
1959
2212
|
if (parameter === GROUP_PARAMETER) {
|
|
1960
2213
|
return `"${GROUP_PARAMETER}" is the group source and is only valid as a whole group, not as a parameter`;
|
|
1961
2214
|
}
|
|
@@ -1991,6 +2244,20 @@ function matchProblem(match, parameter) {
|
|
|
1991
2244
|
if (!match || typeof match !== "object") return "condition has no match object";
|
|
1992
2245
|
if (typeof match.type !== "string" || match.type === "") return "match.type must be a non-empty string";
|
|
1993
2246
|
if (!MATCH_TYPES.includes(match.type)) return `unknown match type "${match.type}"`;
|
|
2247
|
+
for (const operand of ["value", "key", "claim"]) {
|
|
2248
|
+
const value = match[operand];
|
|
2249
|
+
if (typeof value === "string" && value.length > LIMITS.maxValueLength) {
|
|
2250
|
+
return `match operand "${operand}" is longer than ${LIMITS.maxValueLength} chars`;
|
|
2251
|
+
}
|
|
2252
|
+
if (Array.isArray(value)) {
|
|
2253
|
+
if (value.length > LIMITS.maxOperandItems) {
|
|
2254
|
+
return `match operand "${operand}" has more than ${LIMITS.maxOperandItems} entries`;
|
|
2255
|
+
}
|
|
2256
|
+
if (value.some((entry) => typeof entry === "string" && entry.length > LIMITS.maxValueLength)) {
|
|
2257
|
+
return `match operand "${operand}" contains a string longer than ${LIMITS.maxValueLength} chars`;
|
|
2258
|
+
}
|
|
2259
|
+
}
|
|
2260
|
+
}
|
|
1994
2261
|
const parameterless = PARAMETERLESS_MATCH_TYPES.includes(match.type);
|
|
1995
2262
|
if (!parameterless && (parameter === void 0 || parameter === null)) {
|
|
1996
2263
|
return `match type "${match.type}" needs a parameter`;
|
|
@@ -2013,11 +2280,20 @@ function matchProblem(match, parameter) {
|
|
|
2013
2280
|
} else if (!OPERANDLESS_MATCH_TYPES.includes(match.type) && match.value === void 0) {
|
|
2014
2281
|
return `match type "${match.type}" needs a value`;
|
|
2015
2282
|
}
|
|
2283
|
+
if (match.type === "array_key_value") {
|
|
2284
|
+
const paths = Array.isArray(match.key) ? match.key : [match.key];
|
|
2285
|
+
if (paths.some((path) => typeof path === "string" && path.split(".").length > LIMITS.maxPathSegments)) {
|
|
2286
|
+
return `match type "array_key_value" key has more than ${LIMITS.maxPathSegments} path segments`;
|
|
2287
|
+
}
|
|
2288
|
+
}
|
|
2016
2289
|
return null;
|
|
2017
2290
|
}
|
|
2018
2291
|
function mutationsProblem(mutations) {
|
|
2019
2292
|
if (mutations === void 0 || mutations === null) return null;
|
|
2020
2293
|
if (!Array.isArray(mutations)) return "mutations must be an array";
|
|
2294
|
+
if (mutations.length > LIMITS.maxOperandItems) {
|
|
2295
|
+
return `mutations has more than ${LIMITS.maxOperandItems} entries`;
|
|
2296
|
+
}
|
|
2021
2297
|
for (const mutation of mutations) {
|
|
2022
2298
|
if (typeof mutation !== "string" || !MUTATIONS.includes(mutation)) {
|
|
2023
2299
|
return `unknown mutation "${String(mutation)}" \u2014 applied as a no-op, so the condition would test the untransformed value`;
|
|
@@ -2037,10 +2313,19 @@ function whenProblem(when) {
|
|
|
2037
2313
|
if ("path" in when && !isNonEmptyString(when.path)) {
|
|
2038
2314
|
return "when.path must be a non-empty string";
|
|
2039
2315
|
}
|
|
2316
|
+
if (typeof when.path === "string" && when.path.length > LIMITS.maxValueLength) {
|
|
2317
|
+
return `when.path is longer than ${LIMITS.maxValueLength} chars`;
|
|
2318
|
+
}
|
|
2040
2319
|
if ("method" in when) {
|
|
2041
2320
|
const methods = Array.isArray(when.method) ? when.method : [when.method];
|
|
2042
2321
|
if (methods.length === 0) return "when.method is an empty list, so the rule would match no method";
|
|
2322
|
+
if (methods.length > LIMITS.maxParameterItems) {
|
|
2323
|
+
return `when.method has more than ${LIMITS.maxParameterItems} entries`;
|
|
2324
|
+
}
|
|
2043
2325
|
if (!methods.every(isNonEmptyString)) return "when.method must be a non-empty string, or a list of them";
|
|
2326
|
+
if (methods.some((method) => method.length > LIMITS.maxParameterLength)) {
|
|
2327
|
+
return `when.method entries must not be longer than ${LIMITS.maxParameterLength} chars`;
|
|
2328
|
+
}
|
|
2044
2329
|
}
|
|
2045
2330
|
return null;
|
|
2046
2331
|
}
|
|
@@ -2064,6 +2349,11 @@ function rulePropertyProblem(rule) {
|
|
|
2064
2349
|
const problem = operandShapeProblem(shape, rule[property]);
|
|
2065
2350
|
if (problem) return `"${property}" ${problem}`;
|
|
2066
2351
|
}
|
|
2352
|
+
for (const property of ["id", "rule_id", "title", "category", "message", "source_revision"]) {
|
|
2353
|
+
if (typeof rule[property] === "string" && rule[property].length > LIMITS.maxValueLength) {
|
|
2354
|
+
return `"${property}" is longer than ${LIMITS.maxValueLength} chars`;
|
|
2355
|
+
}
|
|
2356
|
+
}
|
|
2067
2357
|
return null;
|
|
2068
2358
|
}
|
|
2069
2359
|
function actionProblem(action, rule) {
|
|
@@ -2093,46 +2383,76 @@ var PHASES2 = new Set(PHASES);
|
|
|
2093
2383
|
var ACTIONS2 = new Set(ACTIONS);
|
|
2094
2384
|
function validateBundle(bundle, opts = {}) {
|
|
2095
2385
|
const rejected = [];
|
|
2386
|
+
let omittedRejections = 0;
|
|
2387
|
+
const reject = (entry) => {
|
|
2388
|
+
if (rejected.length < 100) rejected.push(entry);
|
|
2389
|
+
else omittedRejections++;
|
|
2390
|
+
};
|
|
2096
2391
|
const inFirewall = Array.isArray(bundle?.firewall) ? bundle.firewall : [];
|
|
2097
2392
|
const inWhitelists = Array.isArray(bundle?.whitelists) ? bundle.whitelists : [];
|
|
2393
|
+
const bundleBudget = { count: 0 };
|
|
2098
2394
|
const firewall = [];
|
|
2099
|
-
for (const rule of inFirewall) {
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
continue;
|
|
2103
|
-
}
|
|
2104
|
-
const reason = enforceableRuleProblem(rule);
|
|
2105
|
-
if (reason) rejected.push({ id: idOf(rule), reason });
|
|
2395
|
+
for (const rule of inFirewall.slice(0, LIMITS2.maxRules)) {
|
|
2396
|
+
const reason = enforceableRuleProblem(rule, bundleBudget);
|
|
2397
|
+
if (reason) reject({ id: idOf(rule), reason });
|
|
2106
2398
|
else firewall.push(rule);
|
|
2107
2399
|
}
|
|
2400
|
+
if (inFirewall.length > LIMITS2.maxRules) {
|
|
2401
|
+
reject({ id: "(bundle)", reason: `bundle exceeds maxRules (${LIMITS2.maxRules})` });
|
|
2402
|
+
}
|
|
2108
2403
|
const whitelists = [];
|
|
2109
|
-
for (const wl of inWhitelists) {
|
|
2110
|
-
if (whitelists.length >= LIMITS2.maxWhitelists) {
|
|
2111
|
-
rejected.push({ id: idOf(wl), reason: `bundle exceeds maxWhitelists (${LIMITS2.maxWhitelists})` });
|
|
2112
|
-
continue;
|
|
2113
|
-
}
|
|
2404
|
+
for (const wl of inWhitelists.slice(0, LIMITS2.maxWhitelists)) {
|
|
2114
2405
|
if (!opts.allowGlobalWhitelists && wl && Array.isArray(wl.rule_v2) && !wl.rule_id) {
|
|
2115
|
-
|
|
2406
|
+
reject({ id: idOf(wl), reason: "whitelist has no rule_id (would suppress every rule); set allowGlobalWhitelists to permit" });
|
|
2116
2407
|
continue;
|
|
2117
2408
|
}
|
|
2118
2409
|
if (wl && typeof wl === "object" && BUILD_SCOPE_PROPERTY in wl) {
|
|
2119
|
-
|
|
2410
|
+
reject({ id: idOf(wl), reason: `whitelist may not carry ${BUILD_SCOPE_PROPERTY}` });
|
|
2120
2411
|
continue;
|
|
2121
2412
|
}
|
|
2122
|
-
const reason = conditionsProblem(wl?.rule_v2);
|
|
2123
|
-
if (reason)
|
|
2413
|
+
const reason = conditionsProblem(wl?.rule_v2, 0, { count: 0 }, bundleBudget);
|
|
2414
|
+
if (reason) reject({ id: idOf(wl), reason: `whitelist: ${reason}` });
|
|
2124
2415
|
else whitelists.push(wl);
|
|
2125
2416
|
}
|
|
2417
|
+
if (inWhitelists.length > LIMITS2.maxWhitelists) {
|
|
2418
|
+
reject({ id: "(bundle)", reason: `bundle exceeds maxWhitelists (${LIMITS2.maxWhitelists})` });
|
|
2419
|
+
}
|
|
2420
|
+
let whitelistKeys = {};
|
|
2421
|
+
if (bundle?.whitelist_keys !== void 0) {
|
|
2422
|
+
if (!bundle.whitelist_keys || typeof bundle.whitelist_keys !== "object" || Array.isArray(bundle.whitelist_keys)) {
|
|
2423
|
+
reject({ id: "(bundle)", reason: "whitelist_keys must be an object" });
|
|
2424
|
+
} else {
|
|
2425
|
+
const entries = Object.entries(bundle.whitelist_keys);
|
|
2426
|
+
if (entries.length > LIMITS2.maxMapEntries) {
|
|
2427
|
+
reject({ id: "(bundle)", reason: `whitelist_keys has more than ${LIMITS2.maxMapEntries} entries` });
|
|
2428
|
+
} else {
|
|
2429
|
+
const invalid = entries.find(([key, value]) => key.length > LIMITS2.maxParameterLength || !Array.isArray(value) || value.length > LIMITS2.maxOperandItems || value.some((entry) => typeof entry !== "string" || entry.length > LIMITS2.maxValueLength));
|
|
2430
|
+
if (invalid) {
|
|
2431
|
+
reject({ id: "(bundle)", reason: `whitelist_keys entry "${invalid[0].slice(0, 64)}" has an unsupported shape` });
|
|
2432
|
+
} else {
|
|
2433
|
+
whitelistKeys = bundle.whitelist_keys;
|
|
2434
|
+
}
|
|
2435
|
+
}
|
|
2436
|
+
}
|
|
2437
|
+
}
|
|
2438
|
+
if (omittedRejections > 0) {
|
|
2439
|
+
rejected.push({ id: "(bundle)", reason: `${omittedRejections} additional rejected entries omitted` });
|
|
2440
|
+
}
|
|
2126
2441
|
return {
|
|
2127
|
-
bundle: { ...bundle, firewall, whitelists },
|
|
2442
|
+
bundle: { ...bundle, firewall, whitelists, whitelist_keys: whitelistKeys },
|
|
2128
2443
|
rejected
|
|
2129
2444
|
};
|
|
2130
2445
|
}
|
|
2131
2446
|
function idOf(rule) {
|
|
2132
2447
|
const id = rule?.id ?? rule?.rule_id;
|
|
2133
|
-
|
|
2448
|
+
if (id === void 0 || id === null) return "(unidentified)";
|
|
2449
|
+
try {
|
|
2450
|
+
return String(id).slice(0, 128);
|
|
2451
|
+
} catch {
|
|
2452
|
+
return "(unidentified)";
|
|
2453
|
+
}
|
|
2134
2454
|
}
|
|
2135
|
-
function enforceableRuleProblem(rule) {
|
|
2455
|
+
function enforceableRuleProblem(rule, bundleBudget = null) {
|
|
2136
2456
|
if (!rule || typeof rule !== "object") return "not an object";
|
|
2137
2457
|
const nullReason = nullPropertyProblem(rule);
|
|
2138
2458
|
if (nullReason) return nullReason;
|
|
@@ -2143,9 +2463,9 @@ function enforceableRuleProblem(rule) {
|
|
|
2143
2463
|
if (scopeReason) return scopeReason;
|
|
2144
2464
|
const propertyReason = rulePropertyProblem(rule);
|
|
2145
2465
|
if (propertyReason) return propertyReason;
|
|
2146
|
-
return conditionsProblem(rule.rule_v2);
|
|
2466
|
+
return conditionsProblem(rule.rule_v2, 0, { count: 0 }, bundleBudget);
|
|
2147
2467
|
}
|
|
2148
|
-
function conditionsProblem(conditions, depth = 0) {
|
|
2468
|
+
function conditionsProblem(conditions, depth = 0, ruleBudget = { count: 0 }, bundleBudget = null) {
|
|
2149
2469
|
if (!Array.isArray(conditions)) return "rule_v2 must be an array of conditions";
|
|
2150
2470
|
if (conditions.length === 0) return "rule_v2 is empty (would never match)";
|
|
2151
2471
|
if (depth > LIMITS2.maxNestingDepth) return `nesting deeper than ${LIMITS2.maxNestingDepth}`;
|
|
@@ -2153,10 +2473,20 @@ function conditionsProblem(conditions, depth = 0) {
|
|
|
2153
2473
|
return `more than ${LIMITS2.maxConditionsPerRule} conditions`;
|
|
2154
2474
|
}
|
|
2155
2475
|
for (const c of conditions) {
|
|
2476
|
+
ruleBudget.count++;
|
|
2477
|
+
if (ruleBudget.count > LIMITS2.maxConditionNodesPerRule) {
|
|
2478
|
+
return `more than ${LIMITS2.maxConditionNodesPerRule} total condition nodes`;
|
|
2479
|
+
}
|
|
2480
|
+
if (bundleBudget) {
|
|
2481
|
+
bundleBudget.count++;
|
|
2482
|
+
if (bundleBudget.count > LIMITS2.maxConditionNodesPerBundle) {
|
|
2483
|
+
return `bundle exceeds ${LIMITS2.maxConditionNodesPerBundle} total condition nodes`;
|
|
2484
|
+
}
|
|
2485
|
+
}
|
|
2156
2486
|
const shapeReason = conditionShapeProblem(c);
|
|
2157
2487
|
if (shapeReason) return shapeReason;
|
|
2158
2488
|
if (isGroup(c)) {
|
|
2159
|
-
const nested = conditionsProblem(c.rules, depth + 1);
|
|
2489
|
+
const nested = conditionsProblem(c.rules, depth + 1, ruleBudget, bundleBudget);
|
|
2160
2490
|
if (nested) return nested;
|
|
2161
2491
|
continue;
|
|
2162
2492
|
}
|
|
@@ -2170,11 +2500,17 @@ function conditionsProblem(conditions, depth = 0) {
|
|
|
2170
2500
|
if (m.type === "regex") {
|
|
2171
2501
|
if (typeof m.value !== "string") return "regex match.value must be a string";
|
|
2172
2502
|
if (m.value.length > LIMITS2.maxRegexLength) return `regex longer than ${LIMITS2.maxRegexLength} chars`;
|
|
2503
|
+
if (safeRegExp(m.value) === null) return "regex is invalid or has an unsafe repetition shape";
|
|
2173
2504
|
} else if (typeof m.value === "string" && m.value.length > LIMITS2.maxValueLength) {
|
|
2174
2505
|
return `match.value longer than ${LIMITS2.maxValueLength} chars`;
|
|
2175
2506
|
}
|
|
2176
2507
|
if (m.match) {
|
|
2177
|
-
const nested = conditionsProblem(
|
|
2508
|
+
const nested = conditionsProblem(
|
|
2509
|
+
[{ parameter: c.parameter, match: m.match }],
|
|
2510
|
+
depth + 1,
|
|
2511
|
+
ruleBudget,
|
|
2512
|
+
bundleBudget
|
|
2513
|
+
);
|
|
2178
2514
|
if (nested) return nested;
|
|
2179
2515
|
}
|
|
2180
2516
|
}
|
|
@@ -2454,7 +2790,7 @@ var PulseRuleClient = class {
|
|
|
2454
2790
|
if (!response.ok) {
|
|
2455
2791
|
return { success: false, error: `API returned ${response.status}`, firewall: [], whitelists: [], whitelist_keys: {} };
|
|
2456
2792
|
}
|
|
2457
|
-
const data = await response
|
|
2793
|
+
const data = await readBoundedJson2(response);
|
|
2458
2794
|
if (!data || !Array.isArray(data.firewall)) {
|
|
2459
2795
|
return { success: false, error: "unexpected response shape (no firewall array)", firewall: [], whitelists: [], whitelist_keys: {} };
|
|
2460
2796
|
}
|
|
@@ -2515,19 +2851,18 @@ async function fromFetchRequest(request, options = {}) {
|
|
|
2515
2851
|
const method = (request.method || "GET").toUpperCase();
|
|
2516
2852
|
const headers = {};
|
|
2517
2853
|
request.headers.forEach((value, key) => {
|
|
2518
|
-
headers
|
|
2854
|
+
setOwn(headers, key.toLowerCase(), value);
|
|
2519
2855
|
});
|
|
2520
2856
|
const query = {};
|
|
2521
2857
|
for (const [key, value] of url.searchParams) {
|
|
2522
|
-
|
|
2523
|
-
query[key] = Array.isArray(query[key]) ? [...query[key], value] : [query[key], value];
|
|
2524
|
-
} else {
|
|
2525
|
-
query[key] = value;
|
|
2526
|
-
}
|
|
2858
|
+
appendOwn(query, key, value);
|
|
2527
2859
|
}
|
|
2528
2860
|
let rawBody = "";
|
|
2861
|
+
let bodyInspectionSkip = null;
|
|
2529
2862
|
if (method !== "GET" && method !== "HEAD") {
|
|
2530
|
-
|
|
2863
|
+
const read = await readCappedText(request, maxBodyBytes);
|
|
2864
|
+
rawBody = read.text;
|
|
2865
|
+
bodyInspectionSkip = read.skip;
|
|
2531
2866
|
}
|
|
2532
2867
|
const contentType = headers["content-type"] || "";
|
|
2533
2868
|
let body = {};
|
|
@@ -2554,7 +2889,8 @@ async function fromFetchRequest(request, options = {}) {
|
|
|
2554
2889
|
cookies: parseCookies(headers.cookie),
|
|
2555
2890
|
// Verbatim body text: preserves literal keys (e.g. `__proto__`) that JSON.stringify
|
|
2556
2891
|
// drops, so prototype-pollution rules on `raw` are robust.
|
|
2557
|
-
_rawBody: rawBody
|
|
2892
|
+
_rawBody: rawBody,
|
|
2893
|
+
...bodyInspectionSkip === null ? {} : { _bodyInspectionSkip: bodyInspectionSkip }
|
|
2558
2894
|
};
|
|
2559
2895
|
}
|
|
2560
2896
|
async function readCappedText(request, max) {
|
|
@@ -2562,7 +2898,7 @@ async function readCappedText(request, max) {
|
|
|
2562
2898
|
try {
|
|
2563
2899
|
clone = request.clone();
|
|
2564
2900
|
} catch {
|
|
2565
|
-
return "";
|
|
2901
|
+
return { text: "", skip: "clone-failed" };
|
|
2566
2902
|
}
|
|
2567
2903
|
const body = clone.body;
|
|
2568
2904
|
if (body && typeof body.getReader === "function") {
|
|
@@ -2573,23 +2909,30 @@ async function readCappedText(request, max) {
|
|
|
2573
2909
|
for (; ; ) {
|
|
2574
2910
|
const { done, value } = await reader.read();
|
|
2575
2911
|
if (done) break;
|
|
2576
|
-
if (!value
|
|
2912
|
+
if (!value) continue;
|
|
2577
2913
|
const take = Math.min(value.byteLength, max - buffered);
|
|
2578
|
-
chunks.push(take === value.byteLength ? value : value.subarray(0, take));
|
|
2914
|
+
if (take > 0) chunks.push(take === value.byteLength ? value : value.subarray(0, take));
|
|
2579
2915
|
buffered += take;
|
|
2916
|
+
if (buffered >= max) {
|
|
2917
|
+
void reader.cancel().catch(() => {
|
|
2918
|
+
});
|
|
2919
|
+
return { text: decodeChunks(chunks, buffered), skip: "body-cap" };
|
|
2920
|
+
}
|
|
2580
2921
|
}
|
|
2581
2922
|
} catch {
|
|
2582
|
-
|
|
2583
|
-
|
|
2584
|
-
|
|
2585
|
-
|
|
2586
|
-
|
|
2587
|
-
return "";
|
|
2923
|
+
void reader.cancel().catch(() => {
|
|
2924
|
+
});
|
|
2925
|
+
return { text: "", skip: "read-failed" };
|
|
2926
|
+
} finally {
|
|
2927
|
+
reader.releaseLock();
|
|
2588
2928
|
}
|
|
2929
|
+
return { text: decodeChunks(chunks, buffered), skip: null };
|
|
2589
2930
|
}
|
|
2931
|
+
return { text: "", skip: "unreadable-body" };
|
|
2932
|
+
}
|
|
2933
|
+
function decodeChunks(chunks, total) {
|
|
2590
2934
|
try {
|
|
2591
|
-
|
|
2592
|
-
return text.length > max ? text.slice(0, max) : text;
|
|
2935
|
+
return new TextDecoder().decode(concatChunks(chunks, total));
|
|
2593
2936
|
} catch {
|
|
2594
2937
|
return "";
|
|
2595
2938
|
}
|
|
@@ -2616,7 +2959,7 @@ function parseBody(rawBody, contentType) {
|
|
|
2616
2959
|
}
|
|
2617
2960
|
if (isForm) {
|
|
2618
2961
|
const body = {};
|
|
2619
|
-
for (const [k, v] of new URLSearchParams(rawBody)) body
|
|
2962
|
+
for (const [k, v] of new URLSearchParams(rawBody)) appendOwn(body, k, v);
|
|
2620
2963
|
return { body, files: void 0 };
|
|
2621
2964
|
}
|
|
2622
2965
|
if (isJson || isAmbiguous) {
|
|
@@ -2644,9 +2987,9 @@ function parseMultipart(rawBody, boundary) {
|
|
|
2644
2987
|
if (filename !== void 0) {
|
|
2645
2988
|
const partType = /content-type:\s*([^\r\n;]+)/i.exec(rawHeaders)?.[1]?.trim() || "";
|
|
2646
2989
|
const file = { filename, type: partType, content };
|
|
2647
|
-
files
|
|
2990
|
+
appendOwn(files, name, file);
|
|
2648
2991
|
} else {
|
|
2649
|
-
body
|
|
2992
|
+
appendOwn(body, name, content);
|
|
2650
2993
|
}
|
|
2651
2994
|
}
|
|
2652
2995
|
return { body, files };
|
|
@@ -2661,7 +3004,7 @@ function parseCookies(header) {
|
|
|
2661
3004
|
if (idx === -1) {
|
|
2662
3005
|
continue;
|
|
2663
3006
|
}
|
|
2664
|
-
cookies
|
|
3007
|
+
setOwn(cookies, pair.slice(0, idx).trim(), pair.slice(idx + 1).trim());
|
|
2665
3008
|
}
|
|
2666
3009
|
return cookies;
|
|
2667
3010
|
}
|
|
@@ -2671,7 +3014,7 @@ function fromNodeRequest(req, rawBody = "", options = {}) {
|
|
|
2671
3014
|
const method = (req.method || "GET").toUpperCase();
|
|
2672
3015
|
const headers = {};
|
|
2673
3016
|
for (const [key, value] of Object.entries(req.headers || {})) {
|
|
2674
|
-
headers
|
|
3017
|
+
setOwn(headers, key.toLowerCase(), Array.isArray(value) ? value.join(", ") : value);
|
|
2675
3018
|
}
|
|
2676
3019
|
const host = headers.host || "localhost";
|
|
2677
3020
|
let url;
|
|
@@ -2686,11 +3029,7 @@ function fromNodeRequest(req, rawBody = "", options = {}) {
|
|
|
2686
3029
|
}
|
|
2687
3030
|
const query = {};
|
|
2688
3031
|
for (const [key, value] of url.searchParams) {
|
|
2689
|
-
|
|
2690
|
-
query[key] = Array.isArray(query[key]) ? [...query[key], value] : [query[key], value];
|
|
2691
|
-
} else {
|
|
2692
|
-
query[key] = value;
|
|
2693
|
-
}
|
|
3032
|
+
appendOwn(query, key, value);
|
|
2694
3033
|
}
|
|
2695
3034
|
const contentType = headers["content-type"] || "";
|
|
2696
3035
|
let body = {};
|
|
@@ -2731,7 +3070,7 @@ function parseCookies2(header) {
|
|
|
2731
3070
|
if (idx === -1) {
|
|
2732
3071
|
continue;
|
|
2733
3072
|
}
|
|
2734
|
-
cookies
|
|
3073
|
+
setOwn(cookies, pair.slice(0, idx).trim(), pair.slice(idx + 1).trim());
|
|
2735
3074
|
}
|
|
2736
3075
|
return cookies;
|
|
2737
3076
|
}
|
|
@@ -2787,6 +3126,67 @@ async function installEgressGuard({ shouldBlock, onBlock, onSkip, dnsScreen = tr
|
|
|
2787
3126
|
if (typeof originalFetch === "function" && !originalFetch.__patchstackGuarded) {
|
|
2788
3127
|
const REDIRECT_STATUSES = /* @__PURE__ */ new Set([301, 302, 303, 307, 308]);
|
|
2789
3128
|
const MAX_REDIRECTS = 20;
|
|
3129
|
+
const MAX_REPLAY_BODY_BYTES = 1024 * 1024;
|
|
3130
|
+
const captureReplayBody = (request) => {
|
|
3131
|
+
if (request.method === "GET" || request.method === "HEAD" || !request.body) {
|
|
3132
|
+
return { result: Promise.resolve({ body: void 0, tooLarge: false }), cancel() {
|
|
3133
|
+
} };
|
|
3134
|
+
}
|
|
3135
|
+
let reader;
|
|
3136
|
+
try {
|
|
3137
|
+
reader = request.clone().body?.getReader();
|
|
3138
|
+
} catch {
|
|
3139
|
+
reader = null;
|
|
3140
|
+
}
|
|
3141
|
+
if (!reader) {
|
|
3142
|
+
return { result: Promise.resolve({ body: void 0, tooLarge: true }), cancel() {
|
|
3143
|
+
} };
|
|
3144
|
+
}
|
|
3145
|
+
let cancelled = false;
|
|
3146
|
+
const result = (async () => {
|
|
3147
|
+
const chunks = [];
|
|
3148
|
+
let total = 0;
|
|
3149
|
+
try {
|
|
3150
|
+
for (; ; ) {
|
|
3151
|
+
const { done, value } = await reader.read();
|
|
3152
|
+
if (done || cancelled) break;
|
|
3153
|
+
total += value.byteLength;
|
|
3154
|
+
if (total > MAX_REPLAY_BODY_BYTES) {
|
|
3155
|
+
void reader.cancel().catch(() => {
|
|
3156
|
+
});
|
|
3157
|
+
return { body: void 0, tooLarge: true };
|
|
3158
|
+
}
|
|
3159
|
+
chunks.push(value);
|
|
3160
|
+
}
|
|
3161
|
+
if (cancelled) return { body: void 0, tooLarge: false };
|
|
3162
|
+
const body = new Uint8Array(total);
|
|
3163
|
+
let offset = 0;
|
|
3164
|
+
for (const chunk of chunks) {
|
|
3165
|
+
body.set(chunk, offset);
|
|
3166
|
+
offset += chunk.byteLength;
|
|
3167
|
+
}
|
|
3168
|
+
return { body, tooLarge: false };
|
|
3169
|
+
} catch {
|
|
3170
|
+
return { body: void 0, tooLarge: true };
|
|
3171
|
+
} finally {
|
|
3172
|
+
reader.releaseLock();
|
|
3173
|
+
}
|
|
3174
|
+
})();
|
|
3175
|
+
return {
|
|
3176
|
+
result,
|
|
3177
|
+
cancel() {
|
|
3178
|
+
cancelled = true;
|
|
3179
|
+
void reader.cancel().catch(() => {
|
|
3180
|
+
});
|
|
3181
|
+
}
|
|
3182
|
+
};
|
|
3183
|
+
};
|
|
3184
|
+
const discardResponseBody = async (response) => {
|
|
3185
|
+
try {
|
|
3186
|
+
await response?.body?.cancel?.();
|
|
3187
|
+
} catch {
|
|
3188
|
+
}
|
|
3189
|
+
};
|
|
2790
3190
|
const screenUrl = async (u, method) => {
|
|
2791
3191
|
let host = null;
|
|
2792
3192
|
try {
|
|
@@ -2812,20 +3212,43 @@ async function installEgressGuard({ shouldBlock, onBlock, onSkip, dnsScreen = tr
|
|
|
2812
3212
|
if (callerRedirect !== "follow") return originalFetch(input, init);
|
|
2813
3213
|
const headers = new Headers(cur.headers);
|
|
2814
3214
|
const signal = cur.signal;
|
|
2815
|
-
|
|
3215
|
+
const replay = captureReplayBody(cur);
|
|
3216
|
+
let body;
|
|
2816
3217
|
for (let hop = 0; ; hop++) {
|
|
2817
|
-
|
|
2818
|
-
|
|
2819
|
-
|
|
3218
|
+
let resp;
|
|
3219
|
+
try {
|
|
3220
|
+
resp = await originalFetch(
|
|
3221
|
+
hop === 0 ? cur : new Request(url, { method, headers, body, redirect: "manual", signal })
|
|
3222
|
+
);
|
|
3223
|
+
} catch (error) {
|
|
3224
|
+
replay.cancel();
|
|
3225
|
+
throw error;
|
|
3226
|
+
}
|
|
2820
3227
|
const location = REDIRECT_STATUSES.has(resp.status) ? resp.headers.get("location") : null;
|
|
2821
|
-
if (!location)
|
|
2822
|
-
|
|
3228
|
+
if (!location) {
|
|
3229
|
+
replay.cancel();
|
|
3230
|
+
return resp;
|
|
3231
|
+
}
|
|
3232
|
+
await discardResponseBody(resp);
|
|
3233
|
+
if (hop >= MAX_REDIRECTS) {
|
|
3234
|
+
replay.cancel();
|
|
3235
|
+
throw new Error("Patchstack blocked an outbound request: too many redirects");
|
|
3236
|
+
}
|
|
2823
3237
|
const next = new URL(location, url).href;
|
|
2824
3238
|
if (resp.status === 303 || (resp.status === 301 || resp.status === 302) && method === "POST") {
|
|
2825
3239
|
method = "GET";
|
|
2826
3240
|
body = void 0;
|
|
3241
|
+
replay.cancel();
|
|
2827
3242
|
headers.delete("content-type");
|
|
2828
3243
|
headers.delete("content-length");
|
|
3244
|
+
} else if (body === void 0 && method !== "GET" && method !== "HEAD") {
|
|
3245
|
+
const captured = await replay.result;
|
|
3246
|
+
if (captured.tooLarge) {
|
|
3247
|
+
throw new Error(
|
|
3248
|
+
`Patchstack blocked a redirect that required replaying more than ${MAX_REPLAY_BODY_BYTES} bytes`
|
|
3249
|
+
);
|
|
3250
|
+
}
|
|
3251
|
+
body = captured.body;
|
|
2829
3252
|
}
|
|
2830
3253
|
if (new URL(next).origin !== new URL(url).origin) {
|
|
2831
3254
|
headers.delete("authorization");
|
|
@@ -3136,7 +3559,7 @@ var DEFAULT_RESPONSE_RULES = [
|
|
|
3136
3559
|
//
|
|
3137
3560
|
// Accepts a real newline and a JSON-escaped one. Most traces reach a client inside a JSON error
|
|
3138
3561
|
// body, where the newline is the two characters `\` and `n`.
|
|
3139
|
-
rule_v2: [{ parameter: "response.body", match: { type: "regex", value: "/(?:\\n|\\\\n)\\s*at\\s
|
|
3562
|
+
rule_v2: [{ parameter: "response.body", match: { type: "regex", value: "/(?:\\n|\\\\n)\\s*at\\s+\\S[^\\r\\n(]*\\((?:node:|[A-Za-z]:[\\\\/])?[^:\\r\\n]+:\\d+:\\d+\\)/" } }]
|
|
3140
3563
|
},
|
|
3141
3564
|
{
|
|
3142
3565
|
id: "resp-sql-error",
|
|
@@ -3269,9 +3692,18 @@ async function loadFs() {
|
|
|
3269
3692
|
try {
|
|
3270
3693
|
const [fs, path] = await Promise.all([import("fs"), import("path")]);
|
|
3271
3694
|
fsMod = {
|
|
3695
|
+
chmodSync: fs.chmodSync,
|
|
3696
|
+
closeSync: fs.closeSync,
|
|
3697
|
+
constants: fs.constants,
|
|
3698
|
+
fstatSync: fs.fstatSync,
|
|
3699
|
+
fsyncSync: fs.fsyncSync,
|
|
3700
|
+
lstatSync: fs.lstatSync,
|
|
3701
|
+
openSync: fs.openSync,
|
|
3272
3702
|
readFileSync: fs.readFileSync,
|
|
3273
|
-
writeFileSync: fs.writeFileSync,
|
|
3274
3703
|
mkdirSync: fs.mkdirSync,
|
|
3704
|
+
renameSync: fs.renameSync,
|
|
3705
|
+
unlinkSync: fs.unlinkSync,
|
|
3706
|
+
writeFileSync: fs.writeFileSync,
|
|
3275
3707
|
join: path.join
|
|
3276
3708
|
};
|
|
3277
3709
|
} catch {
|
|
@@ -3324,22 +3756,84 @@ async function cacheWrite(dir, env) {
|
|
|
3324
3756
|
if (!dir) return;
|
|
3325
3757
|
const fs = await loadFs();
|
|
3326
3758
|
if (!fs) return;
|
|
3759
|
+
const target = fs.join(dir, "patchstack-rules.json");
|
|
3760
|
+
const temporary = fs.join(dir, `.patchstack-rules-${randomSuffix()}.tmp`);
|
|
3761
|
+
let descriptor = null;
|
|
3327
3762
|
try {
|
|
3328
3763
|
fs.mkdirSync(dir, { recursive: true });
|
|
3329
|
-
fs.
|
|
3764
|
+
const directory = fs.lstatSync(dir);
|
|
3765
|
+
if (directory.isSymbolicLink() || !directory.isDirectory()) return;
|
|
3766
|
+
let mode;
|
|
3767
|
+
try {
|
|
3768
|
+
const current = fs.lstatSync(target);
|
|
3769
|
+
if (current.isSymbolicLink() || !current.isFile()) return;
|
|
3770
|
+
mode = current.mode & 4095;
|
|
3771
|
+
} catch (error) {
|
|
3772
|
+
if (error?.code !== "ENOENT") return;
|
|
3773
|
+
}
|
|
3774
|
+
descriptor = fs.openSync(temporary, "wx", mode ?? 384);
|
|
3775
|
+
fs.writeFileSync(descriptor, JSON.stringify(env), "utf8");
|
|
3776
|
+
fs.fsyncSync(descriptor);
|
|
3777
|
+
fs.closeSync(descriptor);
|
|
3778
|
+
descriptor = null;
|
|
3779
|
+
if (mode !== void 0) fs.chmodSync(temporary, mode);
|
|
3780
|
+
try {
|
|
3781
|
+
fs.renameSync(temporary, target);
|
|
3782
|
+
} catch (error) {
|
|
3783
|
+
if (typeof process === "undefined" || process.platform !== "win32" || error?.code !== "EEXIST" && error?.code !== "EPERM") throw error;
|
|
3784
|
+
try {
|
|
3785
|
+
fs.unlinkSync(target);
|
|
3786
|
+
} catch (unlinkError) {
|
|
3787
|
+
if (unlinkError?.code !== "ENOENT") throw unlinkError;
|
|
3788
|
+
}
|
|
3789
|
+
fs.renameSync(temporary, target);
|
|
3790
|
+
}
|
|
3330
3791
|
} catch {
|
|
3792
|
+
} finally {
|
|
3793
|
+
if (descriptor !== null) {
|
|
3794
|
+
try {
|
|
3795
|
+
fs.closeSync(descriptor);
|
|
3796
|
+
} catch {
|
|
3797
|
+
}
|
|
3798
|
+
}
|
|
3799
|
+
try {
|
|
3800
|
+
fs.unlinkSync(temporary);
|
|
3801
|
+
} catch {
|
|
3802
|
+
}
|
|
3331
3803
|
}
|
|
3332
3804
|
}
|
|
3333
3805
|
async function cacheRead(dir) {
|
|
3334
3806
|
if (!dir) return null;
|
|
3335
3807
|
const fs = await loadFs();
|
|
3336
3808
|
if (!fs) return null;
|
|
3809
|
+
const target = fs.join(dir, "patchstack-rules.json");
|
|
3810
|
+
let descriptor = null;
|
|
3337
3811
|
try {
|
|
3338
|
-
|
|
3812
|
+
const entry = fs.lstatSync(target);
|
|
3813
|
+
if (entry.isSymbolicLink() || !entry.isFile() || entry.size > 16 * 1024 * 1024) return null;
|
|
3814
|
+
const flags = fs.constants.O_RDONLY | (fs.constants.O_NOFOLLOW ?? 0);
|
|
3815
|
+
descriptor = fs.openSync(target, flags);
|
|
3816
|
+
const opened = fs.fstatSync(descriptor);
|
|
3817
|
+
if (!opened.isFile() || opened.size > 16 * 1024 * 1024) return null;
|
|
3818
|
+
return toEnvelope(JSON.parse(fs.readFileSync(descriptor, "utf8")));
|
|
3339
3819
|
} catch {
|
|
3340
3820
|
return null;
|
|
3821
|
+
} finally {
|
|
3822
|
+
if (descriptor !== null) {
|
|
3823
|
+
try {
|
|
3824
|
+
fs.closeSync(descriptor);
|
|
3825
|
+
} catch {
|
|
3826
|
+
}
|
|
3827
|
+
}
|
|
3341
3828
|
}
|
|
3342
3829
|
}
|
|
3830
|
+
function randomSuffix() {
|
|
3831
|
+
try {
|
|
3832
|
+
if (typeof globalThis.crypto?.randomUUID === "function") return globalThis.crypto.randomUUID();
|
|
3833
|
+
} catch {
|
|
3834
|
+
}
|
|
3835
|
+
return `${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
|
3836
|
+
}
|
|
3343
3837
|
function toEnvelope(value) {
|
|
3344
3838
|
if (!value || typeof value !== "object") return null;
|
|
3345
3839
|
if (value.bundle && typeof value.bundle === "object") {
|
|
@@ -3364,7 +3858,11 @@ function toEnvelope(value) {
|
|
|
3364
3858
|
function liveUpdateRejections(res, options) {
|
|
3365
3859
|
if (options.acceptPartialBundle) return [];
|
|
3366
3860
|
const { rejected } = validateBundle(
|
|
3367
|
-
{
|
|
3861
|
+
{
|
|
3862
|
+
firewall: Array.isArray(res.firewall) ? res.firewall : [],
|
|
3863
|
+
whitelists: Array.isArray(res.whitelists) ? res.whitelists : [],
|
|
3864
|
+
whitelist_keys: res.whitelist_keys
|
|
3865
|
+
},
|
|
3368
3866
|
{ allowGlobalWhitelists: options.allowGlobalWhitelists }
|
|
3369
3867
|
);
|
|
3370
3868
|
return rejected;
|
|
@@ -3535,7 +4033,8 @@ function normalizeBundle(b, options = {}) {
|
|
|
3535
4033
|
const { bundle: checked, rejected } = validateBundle(
|
|
3536
4034
|
{
|
|
3537
4035
|
firewall: Array.isArray(b.firewall) ? b.firewall : [],
|
|
3538
|
-
whitelists: Array.isArray(b.whitelists) ? b.whitelists : []
|
|
4036
|
+
whitelists: Array.isArray(b.whitelists) ? b.whitelists : [],
|
|
4037
|
+
whitelist_keys: b.whitelist_keys
|
|
3539
4038
|
},
|
|
3540
4039
|
{ allowGlobalWhitelists: options.allowGlobalWhitelists }
|
|
3541
4040
|
);
|
|
@@ -3555,7 +4054,7 @@ function normalizeBundle(b, options = {}) {
|
|
|
3555
4054
|
return {
|
|
3556
4055
|
firewall: checked.firewall,
|
|
3557
4056
|
whitelists: checked.whitelists,
|
|
3558
|
-
whitelist_keys:
|
|
4057
|
+
whitelist_keys: checked.whitelist_keys,
|
|
3559
4058
|
...enforcement === "block" || enforcement === "dry-run" ? { enforcement } : {}
|
|
3560
4059
|
};
|
|
3561
4060
|
}
|
|
@@ -3599,18 +4098,23 @@ function startRefresh(tick, { refreshMs, onError } = {}) {
|
|
|
3599
4098
|
};
|
|
3600
4099
|
}
|
|
3601
4100
|
function makeRefreshHandler(tick, secret) {
|
|
4101
|
+
let inflight = null;
|
|
4102
|
+
const runOnce = () => {
|
|
4103
|
+
if (inflight) return inflight;
|
|
4104
|
+
let current;
|
|
4105
|
+
current = Promise.resolve().then(() => tick()).finally(() => {
|
|
4106
|
+
if (inflight === current) inflight = null;
|
|
4107
|
+
});
|
|
4108
|
+
inflight = current;
|
|
4109
|
+
return current;
|
|
4110
|
+
};
|
|
3602
4111
|
return async (request) => {
|
|
3603
4112
|
if (!secret) return new Response("not found", { status: 404 });
|
|
3604
|
-
|
|
3605
|
-
try {
|
|
3606
|
-
provided = request?.headers?.get?.("x-patchstack-refresh") ?? new URL(request.url).searchParams.get("token");
|
|
3607
|
-
} catch {
|
|
3608
|
-
provided = null;
|
|
3609
|
-
}
|
|
4113
|
+
const provided = request?.headers?.get?.("x-patchstack-refresh") ?? null;
|
|
3610
4114
|
if (provided !== secret) return new Response("forbidden", { status: 403 });
|
|
3611
4115
|
let refreshed = true;
|
|
3612
4116
|
try {
|
|
3613
|
-
const status = await
|
|
4117
|
+
const status = await runOnce();
|
|
3614
4118
|
if (status && status.ok === false) refreshed = false;
|
|
3615
4119
|
} catch {
|
|
3616
4120
|
refreshed = false;
|
|
@@ -4359,8 +4863,10 @@ function hardensWithoutBody(rule) {
|
|
|
4359
4863
|
// src/protect/firewall-log.js
|
|
4360
4864
|
var DEFAULT_API_BASE = "https://api.patchstack.com";
|
|
4361
4865
|
var STOP_BUDGET_MS2 = 5e3;
|
|
4866
|
+
var ATTEMPT_TIMEOUT_MS2 = 1e4;
|
|
4362
4867
|
var DEFAULT_FLUSH_MS2 = 1e3;
|
|
4363
4868
|
var MAX_BATCH2 = 50;
|
|
4869
|
+
var MAX_QUEUE2 = 500;
|
|
4364
4870
|
var TOKEN_SKEW_MS = 6e4;
|
|
4365
4871
|
function parseApiKey(apiKey) {
|
|
4366
4872
|
if (typeof apiKey !== "string" || apiKey.length === 0) return null;
|
|
@@ -4382,14 +4888,16 @@ function resolveApiBase(pulseOrManifestUrl) {
|
|
|
4382
4888
|
}
|
|
4383
4889
|
if (typeof pulseOrManifestUrl === "string" && pulseOrManifestUrl.length > 0) {
|
|
4384
4890
|
try {
|
|
4385
|
-
|
|
4891
|
+
const candidate = new URL(pulseOrManifestUrl).origin;
|
|
4892
|
+
if (isSafeOrigin(candidate)) return candidate;
|
|
4386
4893
|
} catch {
|
|
4387
4894
|
}
|
|
4388
4895
|
}
|
|
4389
4896
|
const endpoint = typeof process !== "undefined" ? process.env?.PATCHSTACK_ENDPOINT : void 0;
|
|
4390
4897
|
if (typeof endpoint === "string" && endpoint.length > 0) {
|
|
4391
4898
|
try {
|
|
4392
|
-
|
|
4899
|
+
const candidate = new URL(endpoint).origin;
|
|
4900
|
+
if (isSafeOrigin(candidate)) return candidate;
|
|
4393
4901
|
} catch {
|
|
4394
4902
|
}
|
|
4395
4903
|
}
|
|
@@ -4401,16 +4909,16 @@ function createFirewallLogReporter(opts) {
|
|
|
4401
4909
|
return { record() {
|
|
4402
4910
|
}, flush: () => Promise.resolve(), stop: () => Promise.resolve() };
|
|
4403
4911
|
}
|
|
4404
|
-
const apiBase = (opts.apiBase
|
|
4912
|
+
const apiBase = safeBaseUrl(opts.apiBase, DEFAULT_API_BASE, "block-log API").replace(/\/$/, "");
|
|
4405
4913
|
const fetchImpl = opts.fetchImpl ?? globalThis.fetch;
|
|
4406
4914
|
const flushMs = Number.isFinite(opts.flushMs) ? opts.flushMs : DEFAULT_FLUSH_MS2;
|
|
4407
4915
|
const sourceHost = typeof opts.sourceHost === "string" ? opts.sourceHost : "";
|
|
4408
4916
|
let queue = [];
|
|
4409
4917
|
let timer = null;
|
|
4410
4918
|
let stopped = false;
|
|
4411
|
-
|
|
4919
|
+
let inFlight = null;
|
|
4412
4920
|
let drainPromise = null;
|
|
4413
|
-
|
|
4921
|
+
let activeController = null;
|
|
4414
4922
|
let ended = false;
|
|
4415
4923
|
let cachedToken = null;
|
|
4416
4924
|
let tokenInflight = null;
|
|
@@ -4436,9 +4944,9 @@ function createFirewallLogReporter(opts) {
|
|
|
4436
4944
|
})
|
|
4437
4945
|
});
|
|
4438
4946
|
if (!res || !res.ok) return null;
|
|
4439
|
-
const body = await res
|
|
4947
|
+
const body = await readBoundedJson(res);
|
|
4440
4948
|
const token = body?.access_token;
|
|
4441
|
-
if (typeof token !== "string" || token.length === 0) return null;
|
|
4949
|
+
if (typeof token !== "string" || token.length === 0 || token.length > 8192 || /[\u0000-\u0020\u007f-\u009f]/.test(token)) return null;
|
|
4442
4950
|
const expiresIn = Number(body?.expires_in);
|
|
4443
4951
|
const ttlMs = Number.isFinite(expiresIn) && expiresIn > 0 ? expiresIn * 1e3 : 36e5;
|
|
4444
4952
|
cachedToken = { token, expiresAt: Date.now() + ttlMs };
|
|
@@ -4456,12 +4964,18 @@ function createFirewallLogReporter(opts) {
|
|
|
4456
4964
|
clearTimeout(timer);
|
|
4457
4965
|
timer = null;
|
|
4458
4966
|
}
|
|
4459
|
-
if (ended ||
|
|
4967
|
+
if (ended || typeof fetchImpl !== "function") return Promise.resolve();
|
|
4968
|
+
if (inFlight) return inFlight;
|
|
4969
|
+
if (queue.length === 0) return Promise.resolve();
|
|
4460
4970
|
const batch = queue.splice(0, MAX_BATCH2);
|
|
4971
|
+
const controller = typeof AbortController === "function" ? new AbortController() : null;
|
|
4972
|
+
activeController = controller;
|
|
4973
|
+
const attemptTimer = setTimeout(() => controller?.abort(), ATTEMPT_TIMEOUT_MS2);
|
|
4974
|
+
attemptTimer?.unref?.();
|
|
4461
4975
|
let entry;
|
|
4462
|
-
|
|
4976
|
+
entry = (async () => {
|
|
4463
4977
|
try {
|
|
4464
|
-
const token = await fetchAccessToken(
|
|
4978
|
+
const token = await fetchAccessToken(controller?.signal);
|
|
4465
4979
|
if (!token || ended) return;
|
|
4466
4980
|
const body = new URLSearchParams();
|
|
4467
4981
|
body.set("type", "firewall");
|
|
@@ -4476,19 +4990,24 @@ function createFirewallLogReporter(opts) {
|
|
|
4476
4990
|
...sourceHost ? { "Source-Host": sourceHost } : {}
|
|
4477
4991
|
},
|
|
4478
4992
|
body,
|
|
4479
|
-
// Both phases carry
|
|
4480
|
-
...
|
|
4993
|
+
// Both phases carry the attempt controller, so slow transports cannot accumulate work.
|
|
4994
|
+
...controller ? { signal: controller.signal } : {}
|
|
4481
4995
|
});
|
|
4482
4996
|
if (p && typeof p.then === "function") await p.catch(() => {
|
|
4483
4997
|
});
|
|
4484
4998
|
} catch {
|
|
4485
4999
|
} finally {
|
|
4486
|
-
|
|
5000
|
+
clearTimeout(attemptTimer);
|
|
5001
|
+
if (activeController === controller) activeController = null;
|
|
5002
|
+
if (inFlight === entry) inFlight = null;
|
|
5003
|
+
if (!stopped && !ended && queue.length > 0 && !timer) {
|
|
5004
|
+
timer = setTimeout(flush, queue.length >= MAX_BATCH2 ? 0 : flushMs);
|
|
5005
|
+
timer?.unref?.();
|
|
5006
|
+
}
|
|
4487
5007
|
}
|
|
4488
5008
|
})();
|
|
4489
|
-
|
|
4490
|
-
|
|
4491
|
-
return send;
|
|
5009
|
+
inFlight = entry;
|
|
5010
|
+
return entry;
|
|
4492
5011
|
};
|
|
4493
5012
|
return {
|
|
4494
5013
|
/**
|
|
@@ -4504,6 +5023,7 @@ function createFirewallLogReporter(opts) {
|
|
|
4504
5023
|
if (stopped) return;
|
|
4505
5024
|
const fid = event?.rule?.id;
|
|
4506
5025
|
if (fid === void 0 || fid === null || fid === "") return;
|
|
5026
|
+
if (queue.length >= MAX_QUEUE2) return;
|
|
4507
5027
|
queue.push({
|
|
4508
5028
|
fid,
|
|
4509
5029
|
method: event.method ?? null,
|
|
@@ -4535,9 +5055,10 @@ function createFirewallLogReporter(opts) {
|
|
|
4535
5055
|
}
|
|
4536
5056
|
const terminate = () => {
|
|
4537
5057
|
ended = true;
|
|
4538
|
-
|
|
5058
|
+
activeController?.abort();
|
|
5059
|
+
activeController = null;
|
|
4539
5060
|
queue = [];
|
|
4540
|
-
|
|
5061
|
+
inFlight = null;
|
|
4541
5062
|
};
|
|
4542
5063
|
let budget = null;
|
|
4543
5064
|
const spent = new Promise((resolve) => {
|
|
@@ -4549,8 +5070,8 @@ function createFirewallLogReporter(opts) {
|
|
|
4549
5070
|
});
|
|
4550
5071
|
const work = (async () => {
|
|
4551
5072
|
while (!ended) {
|
|
4552
|
-
if (
|
|
4553
|
-
await
|
|
5073
|
+
if (inFlight) {
|
|
5074
|
+
await inFlight;
|
|
4554
5075
|
continue;
|
|
4555
5076
|
}
|
|
4556
5077
|
if (queue.length === 0) break;
|
|
@@ -4574,10 +5095,107 @@ function telemetryEnabled() {
|
|
|
4574
5095
|
|
|
4575
5096
|
// src/protect/supabase-guard.js
|
|
4576
5097
|
var GUARD_PATH = "/_patchstack/guard";
|
|
4577
|
-
var
|
|
4578
|
-
|
|
5098
|
+
var DEFAULT_MAX_BODY_BYTES = 1024 * 1024;
|
|
5099
|
+
var DEFAULT_TIMEOUT_MS = 3e4;
|
|
5100
|
+
var REQUEST_HEADER_DENYLIST = /* @__PURE__ */ new Set([
|
|
5101
|
+
"connection",
|
|
5102
|
+
"cookie",
|
|
5103
|
+
"cookie2",
|
|
5104
|
+
"forwarded",
|
|
5105
|
+
"host",
|
|
5106
|
+
"keep-alive",
|
|
5107
|
+
"proxy-authenticate",
|
|
5108
|
+
"proxy-authorization",
|
|
5109
|
+
"te",
|
|
5110
|
+
"trailer",
|
|
5111
|
+
"transfer-encoding",
|
|
5112
|
+
"upgrade",
|
|
5113
|
+
"x-forwarded-for",
|
|
5114
|
+
"x-forwarded-host",
|
|
5115
|
+
"x-forwarded-port",
|
|
5116
|
+
"x-forwarded-proto",
|
|
5117
|
+
"x-ps-target",
|
|
5118
|
+
"x-real-ip"
|
|
5119
|
+
]);
|
|
5120
|
+
var RESPONSE_HEADER_DENYLIST = /* @__PURE__ */ new Set([
|
|
5121
|
+
"connection",
|
|
5122
|
+
"content-encoding",
|
|
5123
|
+
"content-length",
|
|
5124
|
+
"keep-alive",
|
|
5125
|
+
"proxy-authenticate",
|
|
5126
|
+
"proxy-authorization",
|
|
5127
|
+
"set-cookie",
|
|
5128
|
+
"set-cookie2",
|
|
5129
|
+
"te",
|
|
5130
|
+
"trailer",
|
|
5131
|
+
"transfer-encoding",
|
|
5132
|
+
"upgrade"
|
|
5133
|
+
]);
|
|
5134
|
+
var BodyTooLargeError = class extends Error {
|
|
5135
|
+
};
|
|
5136
|
+
async function boundedBody(request, maxBytes) {
|
|
5137
|
+
const stated = Number(request.headers.get("content-length"));
|
|
5138
|
+
if (Number.isFinite(stated) && stated > maxBytes) throw new BodyTooLargeError();
|
|
5139
|
+
if (!request.body) return void 0;
|
|
5140
|
+
const reader = request.body.getReader();
|
|
5141
|
+
const chunks = [];
|
|
5142
|
+
let total = 0;
|
|
5143
|
+
try {
|
|
5144
|
+
for (; ; ) {
|
|
5145
|
+
const { done, value } = await reader.read();
|
|
5146
|
+
if (done) break;
|
|
5147
|
+
total += value.byteLength;
|
|
5148
|
+
if (total > maxBytes) {
|
|
5149
|
+
void reader.cancel().catch(() => {
|
|
5150
|
+
});
|
|
5151
|
+
throw new BodyTooLargeError();
|
|
5152
|
+
}
|
|
5153
|
+
chunks.push(value);
|
|
5154
|
+
}
|
|
5155
|
+
} finally {
|
|
5156
|
+
reader.releaseLock();
|
|
5157
|
+
}
|
|
5158
|
+
const body = new Uint8Array(total);
|
|
5159
|
+
let offset = 0;
|
|
5160
|
+
for (const chunk of chunks) {
|
|
5161
|
+
body.set(chunk, offset);
|
|
5162
|
+
offset += chunk.byteLength;
|
|
5163
|
+
}
|
|
5164
|
+
return body;
|
|
5165
|
+
}
|
|
5166
|
+
function requestHeaders(input) {
|
|
5167
|
+
const output = new Headers();
|
|
5168
|
+
const connectionTokens = new Set(
|
|
5169
|
+
(input.get("connection") ?? "").split(",").map((value) => value.trim().toLowerCase()).filter(Boolean)
|
|
5170
|
+
);
|
|
5171
|
+
input.forEach((value, key) => {
|
|
5172
|
+
const lower = key.toLowerCase();
|
|
5173
|
+
if (!REQUEST_HEADER_DENYLIST.has(lower) && !connectionTokens.has(lower)) output.append(key, value);
|
|
5174
|
+
});
|
|
5175
|
+
return output;
|
|
5176
|
+
}
|
|
5177
|
+
function responseHeaders(input) {
|
|
5178
|
+
const output = new Headers();
|
|
5179
|
+
const connectionTokens = new Set(
|
|
5180
|
+
(input.get("connection") ?? "").split(",").map((value) => value.trim().toLowerCase()).filter(Boolean)
|
|
5181
|
+
);
|
|
5182
|
+
input.forEach((value, key) => {
|
|
5183
|
+
const lower = key.toLowerCase();
|
|
5184
|
+
if (!RESPONSE_HEADER_DENYLIST.has(lower) && !connectionTokens.has(lower)) output.append(key, value);
|
|
5185
|
+
});
|
|
5186
|
+
return output;
|
|
5187
|
+
}
|
|
5188
|
+
function createSupabaseGuard({
|
|
5189
|
+
protection,
|
|
5190
|
+
supabaseUrl,
|
|
5191
|
+
fetchImpl = fetch,
|
|
5192
|
+
maxBodyBytes = DEFAULT_MAX_BODY_BYTES,
|
|
5193
|
+
timeoutMs = DEFAULT_TIMEOUT_MS
|
|
5194
|
+
}) {
|
|
4579
5195
|
const guard = protection.fetchGuard();
|
|
4580
5196
|
const allowedOrigin = supabaseUrl ? new URL(supabaseUrl).origin : null;
|
|
5197
|
+
const bodyLimit = Number.isFinite(maxBodyBytes) && maxBodyBytes > 0 ? maxBodyBytes : DEFAULT_MAX_BODY_BYTES;
|
|
5198
|
+
const upstreamTimeout = Number.isFinite(timeoutMs) && timeoutMs > 0 ? timeoutMs : DEFAULT_TIMEOUT_MS;
|
|
4581
5199
|
return async function handleGuardRequest(request) {
|
|
4582
5200
|
const target = request.headers.get("x-ps-target");
|
|
4583
5201
|
if (!target) return new Response("patchstack: missing x-ps-target", { status: 400 });
|
|
@@ -4591,29 +5209,45 @@ function createSupabaseGuard({ protection, supabaseUrl, fetchImpl = fetch }) {
|
|
|
4591
5209
|
return new Response("patchstack: target not allowed", { status: 403 });
|
|
4592
5210
|
}
|
|
4593
5211
|
const hasBody = request.method !== "GET" && request.method !== "HEAD";
|
|
4594
|
-
|
|
5212
|
+
let body;
|
|
5213
|
+
try {
|
|
5214
|
+
body = hasBody ? await boundedBody(request, bodyLimit) : void 0;
|
|
5215
|
+
} catch (error) {
|
|
5216
|
+
if (error instanceof BodyTooLargeError) {
|
|
5217
|
+
return new Response("patchstack: request body too large", { status: 413 });
|
|
5218
|
+
}
|
|
5219
|
+
throw error;
|
|
5220
|
+
}
|
|
5221
|
+
const forwardHeaders = requestHeaders(request.headers);
|
|
4595
5222
|
const evalReq = new Request(targetUrl.toString(), {
|
|
4596
5223
|
method: request.method,
|
|
5224
|
+
// Inspect what the browser sent, including app-origin cookies; only the forwarded copy is
|
|
5225
|
+
// sanitized at the cross-origin boundary below.
|
|
4597
5226
|
headers: request.headers,
|
|
4598
|
-
body
|
|
5227
|
+
body
|
|
4599
5228
|
});
|
|
4600
5229
|
const blocked = await guard(evalReq);
|
|
4601
5230
|
if (blocked) return blocked;
|
|
4602
|
-
|
|
4603
|
-
|
|
4604
|
-
|
|
4605
|
-
|
|
4606
|
-
|
|
4607
|
-
|
|
4608
|
-
|
|
4609
|
-
|
|
4610
|
-
|
|
4611
|
-
|
|
4612
|
-
|
|
4613
|
-
|
|
5231
|
+
let upstream;
|
|
5232
|
+
try {
|
|
5233
|
+
upstream = await fetchImpl(targetUrl.toString(), {
|
|
5234
|
+
method: request.method,
|
|
5235
|
+
headers: forwardHeaders,
|
|
5236
|
+
body,
|
|
5237
|
+
redirect: "manual",
|
|
5238
|
+
signal: AbortSignal.timeout(upstreamTimeout)
|
|
5239
|
+
});
|
|
5240
|
+
} catch (error) {
|
|
5241
|
+
if (error?.name === "TimeoutError" || error?.name === "AbortError") {
|
|
5242
|
+
return new Response("patchstack: upstream timed out", { status: 504 });
|
|
5243
|
+
}
|
|
5244
|
+
throw error;
|
|
5245
|
+
}
|
|
5246
|
+
const forwarded = new Response(upstream.body, {
|
|
5247
|
+
status: upstream.status,
|
|
5248
|
+
statusText: upstream.statusText,
|
|
5249
|
+
headers: responseHeaders(upstream.headers)
|
|
4614
5250
|
});
|
|
4615
|
-
const buf = await upstream.arrayBuffer();
|
|
4616
|
-
const forwarded = new Response(buf, { status: upstream.status, statusText: upstream.statusText, headers: outHeaders });
|
|
4617
5251
|
return protection.screenResponse ? protection.screenResponse(forwarded) : forwarded;
|
|
4618
5252
|
};
|
|
4619
5253
|
}
|
|
@@ -4916,9 +5550,13 @@ async function createProtection(options = {}) {
|
|
|
4916
5550
|
for (const name of Object.keys(headers)) {
|
|
4917
5551
|
const value = headers[name];
|
|
4918
5552
|
if (typeof value === "string") {
|
|
4919
|
-
headers
|
|
5553
|
+
setOwn(headers, name, applyRedactors(value, spanRedactors, mask));
|
|
4920
5554
|
} else if (Array.isArray(value)) {
|
|
4921
|
-
|
|
5555
|
+
setOwn(
|
|
5556
|
+
headers,
|
|
5557
|
+
name,
|
|
5558
|
+
value.map((item) => typeof item === "string" ? applyRedactors(item, spanRedactors, mask) : item)
|
|
5559
|
+
);
|
|
4922
5560
|
}
|
|
4923
5561
|
}
|
|
4924
5562
|
}
|
|
@@ -4968,7 +5606,13 @@ async function createProtection(options = {}) {
|
|
|
4968
5606
|
let result;
|
|
4969
5607
|
let shaped;
|
|
4970
5608
|
try {
|
|
4971
|
-
shaped = await fromFetchRequest(request, {
|
|
5609
|
+
shaped = await fromFetchRequest(request, {
|
|
5610
|
+
trustedProxy: options.trustedProxy,
|
|
5611
|
+
maxBodyBytes: options.maxBodyBytes
|
|
5612
|
+
});
|
|
5613
|
+
if (shaped?._bodyInspectionSkip) {
|
|
5614
|
+
recordSkip("request", shaped._bodyInspectionSkip, { limit: options.maxBodyBytes ?? 1024 * 1024 });
|
|
5615
|
+
}
|
|
4972
5616
|
result = engine.evaluate(shaped);
|
|
4973
5617
|
} catch (err) {
|
|
4974
5618
|
notify(onError, err, "onError");
|
|
@@ -5058,7 +5702,7 @@ async function createProtection(options = {}) {
|
|
|
5058
5702
|
const out = {};
|
|
5059
5703
|
for (const [name, value] of writeHeadEntries(headers)) {
|
|
5060
5704
|
const key = name.toLowerCase();
|
|
5061
|
-
|
|
5705
|
+
appendOwn(out, key, value);
|
|
5062
5706
|
}
|
|
5063
5707
|
return out;
|
|
5064
5708
|
};
|
|
@@ -5455,7 +6099,7 @@ async function createProtection(options = {}) {
|
|
|
5455
6099
|
let reporter = null;
|
|
5456
6100
|
if (refreshable && options.siteUuid && options.reportManifest !== false && cwd) {
|
|
5457
6101
|
try {
|
|
5458
|
-
({ reportManifest: reporter } = await import("./refresh-manifest-
|
|
6102
|
+
({ reportManifest: reporter } = await import("./refresh-manifest-3YWIYENU.js"));
|
|
5459
6103
|
} catch (err) {
|
|
5460
6104
|
notify(onError, err, "onError");
|
|
5461
6105
|
}
|
|
@@ -5594,7 +6238,6 @@ async function readTextResponse(response, cap = DEFAULT_SCREEN_CAP) {
|
|
|
5594
6238
|
const reader = body.getReader();
|
|
5595
6239
|
const chunks = [];
|
|
5596
6240
|
let size = 0;
|
|
5597
|
-
let over = false;
|
|
5598
6241
|
let sniffed = !sniff;
|
|
5599
6242
|
try {
|
|
5600
6243
|
for (; ; ) {
|
|
@@ -5603,20 +6246,27 @@ async function readTextResponse(response, cap = DEFAULT_SCREEN_CAP) {
|
|
|
5603
6246
|
if (!value) continue;
|
|
5604
6247
|
if (!sniffed) {
|
|
5605
6248
|
sniffed = true;
|
|
5606
|
-
if (looksBinary(value))
|
|
6249
|
+
if (looksBinary(value)) {
|
|
6250
|
+
void reader.cancel().catch(() => {
|
|
6251
|
+
});
|
|
6252
|
+
return { skip: "binary-body" };
|
|
6253
|
+
}
|
|
5607
6254
|
}
|
|
5608
6255
|
size += value.byteLength;
|
|
5609
|
-
if (
|
|
5610
|
-
|
|
5611
|
-
|
|
5612
|
-
|
|
6256
|
+
if (size >= cap) {
|
|
6257
|
+
void reader.cancel().catch(() => {
|
|
6258
|
+
});
|
|
6259
|
+
return { skip: "body-cap" };
|
|
5613
6260
|
}
|
|
5614
6261
|
chunks.push(value);
|
|
5615
6262
|
}
|
|
5616
6263
|
} catch {
|
|
6264
|
+
void reader.cancel().catch(() => {
|
|
6265
|
+
});
|
|
5617
6266
|
return { skip: "read-failed" };
|
|
6267
|
+
} finally {
|
|
6268
|
+
reader.releaseLock();
|
|
5618
6269
|
}
|
|
5619
|
-
if (over) return { skip: "body-cap" };
|
|
5620
6270
|
try {
|
|
5621
6271
|
return { text: new TextDecoder().decode(concatBytes(chunks, size)) };
|
|
5622
6272
|
} catch {
|
|
@@ -5644,10 +6294,10 @@ function concatBytes(chunks, total) {
|
|
|
5644
6294
|
function headerObject(headers) {
|
|
5645
6295
|
const out = {};
|
|
5646
6296
|
headers?.forEach?.((v, k) => {
|
|
5647
|
-
out
|
|
6297
|
+
setOwn(out, k.toLowerCase(), v);
|
|
5648
6298
|
});
|
|
5649
6299
|
const setCookies = headers?.getSetCookie?.();
|
|
5650
|
-
if (setCookies && setCookies.length) out
|
|
6300
|
+
if (setCookies && setCookies.length) setOwn(out, "set-cookie", setCookies);
|
|
5651
6301
|
return out;
|
|
5652
6302
|
}
|
|
5653
6303
|
function hasSpanMutations(rule) {
|
|
@@ -5816,7 +6466,7 @@ function isHeaderMutation(action) {
|
|
|
5816
6466
|
}
|
|
5817
6467
|
function applyHeaderMutation(headers, rule) {
|
|
5818
6468
|
if (rule.action === "remove-header") {
|
|
5819
|
-
for (const name of rule.remove_headers ?? []) headers
|
|
6469
|
+
for (const name of rule.remove_headers ?? []) setOwn(headers, String(name).toLowerCase(), null);
|
|
5820
6470
|
return;
|
|
5821
6471
|
}
|
|
5822
6472
|
if (rule.action === "set-header") {
|
|
@@ -5825,7 +6475,7 @@ function applyHeaderMutation(headers, rule) {
|
|
|
5825
6475
|
const key = String(name).toLowerCase();
|
|
5826
6476
|
const present = headers[key] != null && headers[key] !== "";
|
|
5827
6477
|
if (ensure && present) continue;
|
|
5828
|
-
headers
|
|
6478
|
+
setOwn(headers, key, String(value));
|
|
5829
6479
|
}
|
|
5830
6480
|
return;
|
|
5831
6481
|
}
|