@patchstack/connect 0.3.18 → 0.3.20
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/AGENT-INSTALL.md +33 -18
- package/README.md +28 -11
- package/dist/cli.js +1204 -1071
- package/dist/cli.js.map +1 -1
- package/dist/protect/templates/express-guard.cjs +5 -1
- package/dist/protect/templates/express-guard.js +5 -1
- package/dist/protect/templates/express-guard.ts +5 -1
- package/dist/protect/templates/fastify-plugin.cjs +46 -0
- package/dist/protect/templates/fastify-plugin.js +45 -0
- package/dist/protect/templates/generic-guard.cjs +41 -0
- package/dist/protect/templates/generic-guard.js +42 -0
- package/dist/protect/templates/generic-guard.ts +5 -1
- package/dist/protect/templates/guard.ts +5 -1
- package/dist/protect/templates/nuxt-middleware.ts +43 -0
- package/dist/protect.cjs +1692 -92
- package/dist/protect.cjs.map +1 -1
- package/dist/protect.d.ts +24 -0
- package/dist/protect.js +501 -82
- package/dist/protect.js.map +1 -1
- package/dist/refresh-manifest-XBOGN446.js +1090 -0
- package/dist/refresh-manifest-XBOGN446.js.map +1 -0
- package/package.json +1 -1
package/dist/protect.js
CHANGED
|
@@ -13,7 +13,7 @@ var PatchstackRuleClient = class {
|
|
|
13
13
|
constructor({ token, baseUrl, cacheTtl, etag } = {}) {
|
|
14
14
|
this.#token = token ?? process.env.PATCHSTACK_WAF_TOKEN;
|
|
15
15
|
this.#baseUrl = baseUrl ?? process.env.PATCHSTACK_WAF_API_URL ?? DEFAULT_BASE_URL;
|
|
16
|
-
this.#cacheTtl = cacheTtl
|
|
16
|
+
this.#cacheTtl = Number.isFinite(cacheTtl) && cacheTtl > 0 ? cacheTtl : DEFAULT_CACHE_TTL;
|
|
17
17
|
this.#etag = etag ?? null;
|
|
18
18
|
if (!this.#token) {
|
|
19
19
|
throw new Error("Patchstack WAF token is required. Pass { token } or set PATCHSTACK_WAF_TOKEN env var.");
|
|
@@ -53,11 +53,14 @@ var PatchstackRuleClient = class {
|
|
|
53
53
|
};
|
|
54
54
|
}
|
|
55
55
|
const data = await response.json();
|
|
56
|
+
if (!data || !Array.isArray(data.firewall)) {
|
|
57
|
+
return { success: false, error: "unexpected response shape (no firewall array)", firewall: [], whitelists: [], whitelist_keys: {} };
|
|
58
|
+
}
|
|
56
59
|
const result = {
|
|
57
60
|
success: true,
|
|
58
61
|
notModified: false,
|
|
59
62
|
etag: response.headers?.get?.("etag") ?? null,
|
|
60
|
-
firewall:
|
|
63
|
+
firewall: data.firewall,
|
|
61
64
|
whitelists: Array.isArray(data.whitelists) ? data.whitelists : [],
|
|
62
65
|
whitelist_keys: data.whitelist_keys ?? {}
|
|
63
66
|
};
|
|
@@ -383,13 +386,14 @@ var RequestResolver = class {
|
|
|
383
386
|
if (typeof obj !== "object" || obj === null) {
|
|
384
387
|
return void 0;
|
|
385
388
|
}
|
|
386
|
-
|
|
389
|
+
const own = (o, k) => o !== null && typeof o === "object" && Object.prototype.hasOwnProperty.call(o, k);
|
|
390
|
+
if (own(obj, key)) {
|
|
387
391
|
return obj[key];
|
|
388
392
|
}
|
|
389
393
|
const parts = key.split(".");
|
|
390
394
|
let current = obj;
|
|
391
395
|
for (const part of parts) {
|
|
392
|
-
if (current
|
|
396
|
+
if (!own(current, part)) {
|
|
393
397
|
return void 0;
|
|
394
398
|
}
|
|
395
399
|
current = current[part];
|
|
@@ -602,11 +606,8 @@ function normalizeObject(value, options = {}) {
|
|
|
602
606
|
|
|
603
607
|
// src/protect/engine/engine.js
|
|
604
608
|
var REDOS_PATTERNS = [
|
|
605
|
-
/\(\
|
|
606
|
-
/\(\
|
|
607
|
-
/\(\w\+\)\*/,
|
|
608
|
-
/\(\w\*\)\*/,
|
|
609
|
-
/\(\w\|\w\)\+/
|
|
609
|
+
/\([^)]*[+*}][^)]*\)\s*[+*]/,
|
|
610
|
+
/\([^)]*\|[^)]*\)\s*[+*]/
|
|
610
611
|
];
|
|
611
612
|
function safeRegExp(pattern) {
|
|
612
613
|
if (!pattern) {
|
|
@@ -634,6 +635,25 @@ function ctypeResult(strValue, isClass, matchVal) {
|
|
|
634
635
|
const expected = typeof matchVal === "boolean" ? matchVal : true;
|
|
635
636
|
return isClass === expected;
|
|
636
637
|
}
|
|
638
|
+
function walkLeaves(node, segments, cb) {
|
|
639
|
+
if (node === null || typeof node !== "object") return;
|
|
640
|
+
if (Array.isArray(node)) {
|
|
641
|
+
for (const el of node) walkLeaves(el, segments, cb);
|
|
642
|
+
return;
|
|
643
|
+
}
|
|
644
|
+
const [head, ...rest] = segments;
|
|
645
|
+
if (head === void 0 || !Object.prototype.hasOwnProperty.call(node, head)) return;
|
|
646
|
+
if (rest.length === 0) {
|
|
647
|
+
const leaf = node[head];
|
|
648
|
+
if (Array.isArray(leaf)) {
|
|
649
|
+
for (let i = 0; i < leaf.length; i++) cb({ parent: leaf, key: i, value: leaf[i] });
|
|
650
|
+
} else {
|
|
651
|
+
cb({ parent: node, key: head, value: leaf });
|
|
652
|
+
}
|
|
653
|
+
} else {
|
|
654
|
+
walkLeaves(node[head], rest, cb);
|
|
655
|
+
}
|
|
656
|
+
}
|
|
637
657
|
function arrayKeyValue(value, matchObj) {
|
|
638
658
|
if (!matchObj || !matchObj.match || value === null || typeof value !== "object") {
|
|
639
659
|
return false;
|
|
@@ -641,23 +661,11 @@ function arrayKeyValue(value, matchObj) {
|
|
|
641
661
|
const keys = Array.isArray(matchObj.key) ? matchObj.key : [matchObj.key];
|
|
642
662
|
const sub = matchObj.match;
|
|
643
663
|
for (const key of keys) {
|
|
644
|
-
let
|
|
645
|
-
|
|
646
|
-
if (
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
}
|
|
650
|
-
node = node[part];
|
|
651
|
-
}
|
|
652
|
-
if (node === void 0) {
|
|
653
|
-
continue;
|
|
654
|
-
}
|
|
655
|
-
const candidates = Array.isArray(node) ? node : [node];
|
|
656
|
-
for (const candidate of candidates) {
|
|
657
|
-
if (matchValue(sub.type, candidate, sub.value, sub)) {
|
|
658
|
-
return true;
|
|
659
|
-
}
|
|
660
|
-
}
|
|
664
|
+
let matched = false;
|
|
665
|
+
walkLeaves(value, String(key).split("."), ({ value: leaf }) => {
|
|
666
|
+
if (!matched && matchValue(sub.type, leaf, sub.value, sub)) matched = true;
|
|
667
|
+
});
|
|
668
|
+
if (matched) return true;
|
|
661
669
|
}
|
|
662
670
|
return false;
|
|
663
671
|
}
|
|
@@ -679,16 +687,68 @@ function isInternalHost(hostname) {
|
|
|
679
687
|
if (host === "::1" || host === "::") return true;
|
|
680
688
|
if (host.startsWith("fe80:") || host.startsWith("fc") || host.startsWith("fd")) return true;
|
|
681
689
|
const v4 = host.match(/(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/);
|
|
682
|
-
if (v4)
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
if (
|
|
688
|
-
if (a === 192 && b === 168) return true;
|
|
690
|
+
if (v4 && isPrivateV4(Number(v4[1]), Number(v4[2]))) return true;
|
|
691
|
+
const mapped = host.match(/^::ffff:([0-9a-f]{1,4}):([0-9a-f]{1,4})$/);
|
|
692
|
+
if (mapped) {
|
|
693
|
+
const g1 = parseInt(mapped[1], 16);
|
|
694
|
+
const g2 = parseInt(mapped[2], 16);
|
|
695
|
+
if (isPrivateV4(g1 >> 8 & 255, g1 & 255)) return true;
|
|
689
696
|
}
|
|
690
697
|
return false;
|
|
691
698
|
}
|
|
699
|
+
function isPrivateV4(a, b) {
|
|
700
|
+
if (a === 127 || a === 10 || a === 0) return true;
|
|
701
|
+
if (a === 169 && b === 254) return true;
|
|
702
|
+
if (a === 172 && b >= 16 && b <= 31) return true;
|
|
703
|
+
if (a === 192 && b === 168) return true;
|
|
704
|
+
return false;
|
|
705
|
+
}
|
|
706
|
+
function ruleAppliesTo(when, resolver) {
|
|
707
|
+
try {
|
|
708
|
+
if (when.method) {
|
|
709
|
+
const methods = (Array.isArray(when.method) ? when.method : [when.method]).map((m) => String(m).toUpperCase());
|
|
710
|
+
const actual = String(resolver.resolve("server.REQUEST_METHOD")[0] ?? "GET").toUpperCase();
|
|
711
|
+
if (!methods.includes(actual)) return false;
|
|
712
|
+
}
|
|
713
|
+
if (when.path) {
|
|
714
|
+
const uri = String(resolver.resolve("server.REQUEST_URI")[0] ?? "/");
|
|
715
|
+
if (!pathMatches(String(when.path), uri.split("?")[0])) return false;
|
|
716
|
+
}
|
|
717
|
+
return true;
|
|
718
|
+
} catch {
|
|
719
|
+
return true;
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
function pathMatches(pattern, path) {
|
|
723
|
+
if (pattern.length > 2 && pattern.startsWith("/") && pattern.endsWith("/")) {
|
|
724
|
+
const re = safeRegExp(pattern);
|
|
725
|
+
return re ? re.test(path) : false;
|
|
726
|
+
}
|
|
727
|
+
if (pattern.includes("*")) {
|
|
728
|
+
const re = safeRegExp("/^" + pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*/g, "[^?]*") + "$/");
|
|
729
|
+
return re ? re.test(path) : false;
|
|
730
|
+
}
|
|
731
|
+
return path === pattern;
|
|
732
|
+
}
|
|
733
|
+
function isCrossOrigin(resolver) {
|
|
734
|
+
try {
|
|
735
|
+
const host = String(resolver.resolve("server.HTTP_HOST")[0] ?? "").toLowerCase();
|
|
736
|
+
if (!host) return false;
|
|
737
|
+
const src = resolver.resolve("server.HTTP_ORIGIN")[0] ?? resolver.resolve("server.HTTP_REFERER")[0];
|
|
738
|
+
if (!src) return false;
|
|
739
|
+
const srcHost = hostFromUrl(String(src));
|
|
740
|
+
return srcHost !== null && srcHost !== host;
|
|
741
|
+
} catch {
|
|
742
|
+
return false;
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
function hostFromUrl(value) {
|
|
746
|
+
try {
|
|
747
|
+
return new URL(value).host.toLowerCase();
|
|
748
|
+
} catch {
|
|
749
|
+
return null;
|
|
750
|
+
}
|
|
751
|
+
}
|
|
692
752
|
function matchValue(type, value, matchVal, matchObj) {
|
|
693
753
|
if (value === null || value === void 0) {
|
|
694
754
|
if (type === "isset") {
|
|
@@ -734,18 +794,19 @@ function matchValue(type, value, matchVal, matchObj) {
|
|
|
734
794
|
case "isset":
|
|
735
795
|
return true;
|
|
736
796
|
case "in_array": {
|
|
737
|
-
const arr = Array.isArray(matchVal) ? matchVal : [matchVal];
|
|
797
|
+
const arr = (Array.isArray(matchVal) ? matchVal : [matchVal]).map((x) => String(x));
|
|
738
798
|
return arr.includes(strValue);
|
|
739
799
|
}
|
|
740
800
|
case "not_in_array": {
|
|
741
|
-
const arr = Array.isArray(matchVal) ? matchVal : [matchVal];
|
|
801
|
+
const arr = (Array.isArray(matchVal) ? matchVal : [matchVal]).map((x) => String(x));
|
|
742
802
|
return !arr.includes(strValue);
|
|
743
803
|
}
|
|
744
804
|
case "array_in_array": {
|
|
745
805
|
if (!Array.isArray(value) || !Array.isArray(matchVal)) {
|
|
746
806
|
return false;
|
|
747
807
|
}
|
|
748
|
-
|
|
808
|
+
const target = matchVal.map((x) => String(x));
|
|
809
|
+
return value.some((v) => target.includes(String(v)));
|
|
749
810
|
}
|
|
750
811
|
case "hostname": {
|
|
751
812
|
try {
|
|
@@ -822,6 +883,9 @@ var RuleEngine = class {
|
|
|
822
883
|
}
|
|
823
884
|
for (const rule of this.#rules) {
|
|
824
885
|
try {
|
|
886
|
+
if (rule.when && !ruleAppliesTo(rule.when, resolver)) {
|
|
887
|
+
continue;
|
|
888
|
+
}
|
|
825
889
|
const conditions = rule.rule_v2;
|
|
826
890
|
if (!Array.isArray(conditions) || conditions.length === 0) {
|
|
827
891
|
continue;
|
|
@@ -868,6 +932,9 @@ var RuleEngine = class {
|
|
|
868
932
|
if (parameter === "rules" && Array.isArray(condition.rules)) {
|
|
869
933
|
return this.#evaluateRule(condition.rules, resolver);
|
|
870
934
|
}
|
|
935
|
+
if (match && match.type === "cross_origin") {
|
|
936
|
+
return isCrossOrigin(resolver);
|
|
937
|
+
}
|
|
871
938
|
const params = Array.isArray(parameter) ? parameter : [parameter];
|
|
872
939
|
const values = params.flatMap((p) => resolver.resolve(p));
|
|
873
940
|
if (values.length === 0) {
|
|
@@ -932,7 +999,7 @@ var PulseRuleClient = class {
|
|
|
932
999
|
constructor({ siteUuid, baseUrl, cacheTtl, etag } = {}) {
|
|
933
1000
|
this.#siteUuid = siteUuid ?? process.env.PATCHSTACK_SITE_UUID;
|
|
934
1001
|
this.#baseUrl = baseUrl ?? process.env.PATCHSTACK_PULSE_RULES_URL ?? DEFAULT_BASE_URL2;
|
|
935
|
-
this.#cacheTtl = cacheTtl
|
|
1002
|
+
this.#cacheTtl = Number.isFinite(cacheTtl) && cacheTtl > 0 ? cacheTtl : DEFAULT_CACHE_TTL2;
|
|
936
1003
|
this.#etag = etag ?? null;
|
|
937
1004
|
if (!this.#siteUuid) {
|
|
938
1005
|
throw new Error("Patchstack site UUID is required. Pass { siteUuid } or set PATCHSTACK_SITE_UUID.");
|
|
@@ -956,11 +1023,14 @@ var PulseRuleClient = class {
|
|
|
956
1023
|
return { success: false, error: `API returned ${response.status}`, firewall: [], whitelists: [], whitelist_keys: {} };
|
|
957
1024
|
}
|
|
958
1025
|
const data = await response.json();
|
|
1026
|
+
if (!data || !Array.isArray(data.firewall)) {
|
|
1027
|
+
return { success: false, error: "unexpected response shape (no firewall array)", firewall: [], whitelists: [], whitelist_keys: {} };
|
|
1028
|
+
}
|
|
959
1029
|
const result = {
|
|
960
1030
|
success: true,
|
|
961
1031
|
notModified: false,
|
|
962
1032
|
etag: response.headers?.get?.("etag") ?? null,
|
|
963
|
-
firewall:
|
|
1033
|
+
firewall: data.firewall,
|
|
964
1034
|
whitelists: Array.isArray(data.whitelists) ? data.whitelists : [],
|
|
965
1035
|
whitelist_keys: data.whitelist_keys ?? {}
|
|
966
1036
|
};
|
|
@@ -1062,6 +1132,29 @@ async function readCappedText(request, max) {
|
|
|
1062
1132
|
} catch {
|
|
1063
1133
|
return "";
|
|
1064
1134
|
}
|
|
1135
|
+
const body = clone.body;
|
|
1136
|
+
if (body && typeof body.getReader === "function") {
|
|
1137
|
+
const reader = body.getReader();
|
|
1138
|
+
const chunks = [];
|
|
1139
|
+
let buffered = 0;
|
|
1140
|
+
try {
|
|
1141
|
+
for (; ; ) {
|
|
1142
|
+
const { done, value } = await reader.read();
|
|
1143
|
+
if (done) break;
|
|
1144
|
+
if (!value || buffered >= max) continue;
|
|
1145
|
+
const take = Math.min(value.byteLength, max - buffered);
|
|
1146
|
+
chunks.push(take === value.byteLength ? value : value.subarray(0, take));
|
|
1147
|
+
buffered += take;
|
|
1148
|
+
}
|
|
1149
|
+
} catch {
|
|
1150
|
+
return "";
|
|
1151
|
+
}
|
|
1152
|
+
try {
|
|
1153
|
+
return new TextDecoder().decode(concatChunks(chunks, buffered));
|
|
1154
|
+
} catch {
|
|
1155
|
+
return "";
|
|
1156
|
+
}
|
|
1157
|
+
}
|
|
1065
1158
|
try {
|
|
1066
1159
|
const text = await clone.text();
|
|
1067
1160
|
return text.length > max ? text.slice(0, max) : text;
|
|
@@ -1069,6 +1162,15 @@ async function readCappedText(request, max) {
|
|
|
1069
1162
|
return "";
|
|
1070
1163
|
}
|
|
1071
1164
|
}
|
|
1165
|
+
function concatChunks(chunks, total) {
|
|
1166
|
+
const out = new Uint8Array(total);
|
|
1167
|
+
let offset = 0;
|
|
1168
|
+
for (const c of chunks) {
|
|
1169
|
+
out.set(c, offset);
|
|
1170
|
+
offset += c.byteLength;
|
|
1171
|
+
}
|
|
1172
|
+
return out;
|
|
1173
|
+
}
|
|
1072
1174
|
function parseMultipart(rawBody, boundary) {
|
|
1073
1175
|
const body = {};
|
|
1074
1176
|
const files = {};
|
|
@@ -1113,7 +1215,16 @@ function fromNodeRequest(req, rawBody = "") {
|
|
|
1113
1215
|
headers[key.toLowerCase()] = Array.isArray(value) ? value.join(", ") : value;
|
|
1114
1216
|
}
|
|
1115
1217
|
const host = headers.host || "localhost";
|
|
1116
|
-
|
|
1218
|
+
let url;
|
|
1219
|
+
try {
|
|
1220
|
+
url = new URL(req.url || "/", `http://${host}`);
|
|
1221
|
+
} catch {
|
|
1222
|
+
try {
|
|
1223
|
+
url = new URL(req.url || "/", "http://localhost");
|
|
1224
|
+
} catch {
|
|
1225
|
+
url = new URL("http://localhost/");
|
|
1226
|
+
}
|
|
1227
|
+
}
|
|
1117
1228
|
const query = {};
|
|
1118
1229
|
for (const [key, value] of url.searchParams) {
|
|
1119
1230
|
if (key in query) {
|
|
@@ -1124,6 +1235,7 @@ function fromNodeRequest(req, rawBody = "") {
|
|
|
1124
1235
|
}
|
|
1125
1236
|
const contentType = headers["content-type"] || "";
|
|
1126
1237
|
let body = {};
|
|
1238
|
+
let files;
|
|
1127
1239
|
if (rawBody) {
|
|
1128
1240
|
if (contentType.includes("application/json")) {
|
|
1129
1241
|
try {
|
|
@@ -1136,6 +1248,13 @@ function fromNodeRequest(req, rawBody = "") {
|
|
|
1136
1248
|
for (const [k, v] of new URLSearchParams(rawBody)) {
|
|
1137
1249
|
body[k] = k in body ? [].concat(body[k], v) : v;
|
|
1138
1250
|
}
|
|
1251
|
+
} else if (contentType.includes("multipart/form-data")) {
|
|
1252
|
+
const boundary = /boundary=("?)([^";]+)\1/i.exec(contentType)?.[2];
|
|
1253
|
+
if (boundary) {
|
|
1254
|
+
const parsed = parseMultipart(rawBody, boundary);
|
|
1255
|
+
body = parsed.body;
|
|
1256
|
+
files = parsed.files;
|
|
1257
|
+
}
|
|
1139
1258
|
}
|
|
1140
1259
|
}
|
|
1141
1260
|
const uri = url.pathname + url.search;
|
|
@@ -1146,6 +1265,7 @@ function fromNodeRequest(req, rawBody = "") {
|
|
|
1146
1265
|
originalUrl: uri,
|
|
1147
1266
|
query,
|
|
1148
1267
|
body,
|
|
1268
|
+
files,
|
|
1149
1269
|
headers,
|
|
1150
1270
|
ip: forwarded.split(",")[0].trim() || req.socket && req.socket.remoteAddress || "",
|
|
1151
1271
|
cookies: parseCookies2(headers.cookie),
|
|
@@ -1169,15 +1289,43 @@ function parseCookies2(header) {
|
|
|
1169
1289
|
}
|
|
1170
1290
|
|
|
1171
1291
|
// src/protect/egress.js
|
|
1172
|
-
async function installEgressGuard({ shouldBlock, onBlock } = {}) {
|
|
1292
|
+
async function installEgressGuard({ shouldBlock, onBlock, dnsScreen = true, lookup, allowHosts } = {}) {
|
|
1173
1293
|
const restores = [];
|
|
1174
1294
|
if (typeof shouldBlock !== "function") return () => {
|
|
1175
1295
|
};
|
|
1296
|
+
const exempt = new Set((allowHosts ?? []).map((h) => String(h).toLowerCase()));
|
|
1176
1297
|
const block = (url, host, method) => {
|
|
1177
1298
|
if (!shouldBlock(url, host, method)) return false;
|
|
1178
1299
|
onBlock?.({ url, host, method });
|
|
1179
1300
|
return true;
|
|
1180
1301
|
};
|
|
1302
|
+
let screen = null;
|
|
1303
|
+
if (dnsScreen) {
|
|
1304
|
+
try {
|
|
1305
|
+
const resolveLookup = lookup ?? (await import("dns")).lookup;
|
|
1306
|
+
const { isIP } = await import("net");
|
|
1307
|
+
if (typeof resolveLookup === "function" && typeof isIP === "function") {
|
|
1308
|
+
screen = { lookup: resolveLookup, isIP, isExempt: (h) => exempt.has(String(h).toLowerCase()) };
|
|
1309
|
+
}
|
|
1310
|
+
} catch {
|
|
1311
|
+
screen = null;
|
|
1312
|
+
}
|
|
1313
|
+
}
|
|
1314
|
+
const resolvesToDisallowed = (url, host, method) => new Promise((resolve) => {
|
|
1315
|
+
if (!screen || !host || screen.isIP(host) !== 0 || screen.isExempt(host)) return resolve(false);
|
|
1316
|
+
try {
|
|
1317
|
+
screen.lookup(host, { all: true }, (err, addresses) => {
|
|
1318
|
+
if (err || !Array.isArray(addresses)) return resolve(false);
|
|
1319
|
+
for (const a of addresses) {
|
|
1320
|
+
const ip = a && typeof a === "object" ? a.address : a;
|
|
1321
|
+
if (ip && block(url, ip, method)) return resolve(true);
|
|
1322
|
+
}
|
|
1323
|
+
resolve(false);
|
|
1324
|
+
});
|
|
1325
|
+
} catch {
|
|
1326
|
+
resolve(false);
|
|
1327
|
+
}
|
|
1328
|
+
});
|
|
1181
1329
|
const originalFetch = globalThis.fetch;
|
|
1182
1330
|
if (typeof originalFetch === "function" && !originalFetch.__patchstackGuarded) {
|
|
1183
1331
|
const guarded = async (input, init) => {
|
|
@@ -1189,7 +1337,7 @@ async function installEgressGuard({ shouldBlock, onBlock } = {}) {
|
|
|
1189
1337
|
host = null;
|
|
1190
1338
|
}
|
|
1191
1339
|
const method = init && init.method || input && input.method || "GET";
|
|
1192
|
-
if (block(url, host, method)) {
|
|
1340
|
+
if (block(url, host, method) || await resolvesToDisallowed(url, host, method)) {
|
|
1193
1341
|
throw new Error(`Patchstack blocked an outbound request to a disallowed address: ${host ?? url}`);
|
|
1194
1342
|
}
|
|
1195
1343
|
return originalFetch(input, init);
|
|
@@ -1203,7 +1351,7 @@ async function installEgressGuard({ shouldBlock, onBlock } = {}) {
|
|
|
1203
1351
|
for (const moduleName of ["node:http", "node:https"]) {
|
|
1204
1352
|
try {
|
|
1205
1353
|
const mod = await import(moduleName);
|
|
1206
|
-
const restore = patchHttpModule(mod.default ?? mod, block);
|
|
1354
|
+
const restore = patchHttpModule(mod.default ?? mod, block, screen);
|
|
1207
1355
|
if (restore) restores.push(restore);
|
|
1208
1356
|
} catch {
|
|
1209
1357
|
}
|
|
@@ -1241,7 +1389,7 @@ async function installEgressGuard({ shouldBlock, onBlock } = {}) {
|
|
|
1241
1389
|
}
|
|
1242
1390
|
};
|
|
1243
1391
|
}
|
|
1244
|
-
function patchHttpModule(http, block) {
|
|
1392
|
+
function patchHttpModule(http, block, screen) {
|
|
1245
1393
|
if (!http || typeof http.request !== "function" || http.__patchstackGuarded) return null;
|
|
1246
1394
|
const originalRequest = http.request;
|
|
1247
1395
|
const originalGet = http.get;
|
|
@@ -1250,14 +1398,25 @@ function patchHttpModule(http, block) {
|
|
|
1250
1398
|
if (target && block(target.url, target.host, target.method)) {
|
|
1251
1399
|
throw new Error(`Patchstack blocked an outbound request to a disallowed address: ${target.host ?? target.url}`);
|
|
1252
1400
|
}
|
|
1401
|
+
if (target && screen && target.host && screen.isIP(target.host) === 0 && !screen.isExempt(target.host)) {
|
|
1402
|
+
try {
|
|
1403
|
+
args = withScreeningLookup(args, target, block, screen.lookup);
|
|
1404
|
+
} catch {
|
|
1405
|
+
}
|
|
1406
|
+
}
|
|
1253
1407
|
return original.apply(this, args);
|
|
1254
1408
|
};
|
|
1255
|
-
|
|
1256
|
-
|
|
1409
|
+
const guardedRequest = wrap(originalRequest);
|
|
1410
|
+
http.request = guardedRequest;
|
|
1411
|
+
let guardedGet;
|
|
1412
|
+
if (typeof originalGet === "function") {
|
|
1413
|
+
guardedGet = wrap(originalGet);
|
|
1414
|
+
http.get = guardedGet;
|
|
1415
|
+
}
|
|
1257
1416
|
http.__patchstackGuarded = true;
|
|
1258
1417
|
return () => {
|
|
1259
|
-
http.request = originalRequest;
|
|
1260
|
-
if (
|
|
1418
|
+
if (http.request === guardedRequest) http.request = originalRequest;
|
|
1419
|
+
if (guardedGet && http.get === guardedGet) http.get = originalGet;
|
|
1261
1420
|
delete http.__patchstackGuarded;
|
|
1262
1421
|
};
|
|
1263
1422
|
}
|
|
@@ -1270,7 +1429,7 @@ function extractHttpTarget(args) {
|
|
|
1270
1429
|
return { url: url.href, host: url.hostname, method: opts && opts.method || "GET" };
|
|
1271
1430
|
}
|
|
1272
1431
|
if (first && typeof first === "object") {
|
|
1273
|
-
const host = normalizeHost(first.hostname || first.host);
|
|
1432
|
+
const host = normalizeHost(first.hostname || first.host) || "localhost";
|
|
1274
1433
|
const protocol = first.protocol || "http:";
|
|
1275
1434
|
const port = first.port ? `:${first.port}` : "";
|
|
1276
1435
|
const path = first.path || "/";
|
|
@@ -1287,6 +1446,61 @@ function normalizeHost(raw) {
|
|
|
1287
1446
|
if ((host.match(/:/g) || []).length > 1) return host;
|
|
1288
1447
|
return host.split(":")[0];
|
|
1289
1448
|
}
|
|
1449
|
+
function screenResolved(addresses, target, block) {
|
|
1450
|
+
for (const a of addresses || []) {
|
|
1451
|
+
const ip = a && typeof a === "object" ? a.address : a;
|
|
1452
|
+
if (ip && block(target.url, ip, target.method)) return ip;
|
|
1453
|
+
}
|
|
1454
|
+
return null;
|
|
1455
|
+
}
|
|
1456
|
+
function withScreeningLookup(args, target, block, lookup) {
|
|
1457
|
+
const screeningLookup = (hostname, options, callback) => {
|
|
1458
|
+
let opts = options;
|
|
1459
|
+
let cb = callback;
|
|
1460
|
+
if (typeof opts === "function") {
|
|
1461
|
+
cb = opts;
|
|
1462
|
+
opts = {};
|
|
1463
|
+
}
|
|
1464
|
+
if (!opts || typeof opts !== "object") opts = {};
|
|
1465
|
+
try {
|
|
1466
|
+
lookup(hostname, { ...opts, all: true }, (err, addresses) => {
|
|
1467
|
+
if (err) return cb(err);
|
|
1468
|
+
const list = Array.isArray(addresses) ? addresses : [];
|
|
1469
|
+
const blocked = screenResolved(list, target, block);
|
|
1470
|
+
if (blocked) {
|
|
1471
|
+
return cb(new Error(`Patchstack blocked an outbound request to a disallowed address: ${target.host} resolved to ${blocked}`));
|
|
1472
|
+
}
|
|
1473
|
+
if (opts.all) return cb(null, list);
|
|
1474
|
+
const first = list[0];
|
|
1475
|
+
if (!first) return cb(new Error(`Patchstack: could not resolve ${hostname}`));
|
|
1476
|
+
return cb(null, first.address, first.family);
|
|
1477
|
+
});
|
|
1478
|
+
} catch {
|
|
1479
|
+
try {
|
|
1480
|
+
lookup(hostname, opts, cb);
|
|
1481
|
+
} catch {
|
|
1482
|
+
cb(new Error(`Patchstack: lookup failed for ${hostname}`));
|
|
1483
|
+
}
|
|
1484
|
+
}
|
|
1485
|
+
};
|
|
1486
|
+
return injectLookupOption(args, screeningLookup);
|
|
1487
|
+
}
|
|
1488
|
+
function injectLookupOption(args, lookup) {
|
|
1489
|
+
const first = args[0];
|
|
1490
|
+
if (first && typeof first === "object" && !(first instanceof URL)) {
|
|
1491
|
+
return [{ ...first, lookup }, ...args.slice(1)];
|
|
1492
|
+
}
|
|
1493
|
+
const rest = args.slice(1);
|
|
1494
|
+
const optIdx = rest.findIndex((a) => a && typeof a === "object" && !(a instanceof URL));
|
|
1495
|
+
if (optIdx !== -1) {
|
|
1496
|
+
const next = [...rest];
|
|
1497
|
+
next[optIdx] = { ...rest[optIdx], lookup };
|
|
1498
|
+
return [first, ...next];
|
|
1499
|
+
}
|
|
1500
|
+
const cbIdx = rest.findIndex((a) => typeof a === "function");
|
|
1501
|
+
if (cbIdx === -1) return [first, { lookup }, ...rest];
|
|
1502
|
+
return [first, ...rest.slice(0, cbIdx), { lookup }, ...rest.slice(cbIdx)];
|
|
1503
|
+
}
|
|
1290
1504
|
|
|
1291
1505
|
// src/protect/defaults.js
|
|
1292
1506
|
var DEFAULT_RESPONSE_RULES = [
|
|
@@ -1530,22 +1744,33 @@ async function createProtection(options = {}) {
|
|
|
1530
1744
|
const onError = options.onError;
|
|
1531
1745
|
const onDetect = options.onDetect ?? defaultOnDetect;
|
|
1532
1746
|
const bundle = await resolveRules(options);
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1747
|
+
let requestRules;
|
|
1748
|
+
let responseRules;
|
|
1749
|
+
let egressRules;
|
|
1750
|
+
let screenCap;
|
|
1751
|
+
let engine;
|
|
1752
|
+
let responseRuleSet;
|
|
1753
|
+
let egressEngine;
|
|
1754
|
+
const applyBundle = (delivered) => {
|
|
1755
|
+
const incoming = delivered.firewall ?? [];
|
|
1756
|
+
requestRules = byPhase(incoming, "request");
|
|
1757
|
+
responseRules = [...options.responseRules ?? DEFAULT_RESPONSE_RULES, ...byPhase(incoming, "response")];
|
|
1758
|
+
screenCap = responseScreenCap(responseRules);
|
|
1759
|
+
egressRules = [...options.egressRules ?? DEFAULT_EGRESS_RULES, ...byPhase(incoming, "egress")];
|
|
1760
|
+
engine = new RuleEngine({
|
|
1761
|
+
firewall: requestRules,
|
|
1762
|
+
whitelists: delivered.whitelists,
|
|
1763
|
+
whitelist_keys: delivered.whitelist_keys,
|
|
1764
|
+
onError
|
|
1765
|
+
});
|
|
1766
|
+
responseRuleSet = responseRules.map((rule) => ({
|
|
1767
|
+
rule,
|
|
1768
|
+
engine: new RuleEngine({ firewall: [rule], onError }),
|
|
1769
|
+
redactors: rule.action === "redact" || rule.action === "encode" ? extractRedactors(rule) : null
|
|
1770
|
+
}));
|
|
1771
|
+
egressEngine = new RuleEngine({ firewall: egressRules, onError });
|
|
1772
|
+
};
|
|
1773
|
+
applyBundle(bundle);
|
|
1549
1774
|
const maskFn = typeof options.maskWith === "function" ? options.maskWith : () => typeof options.maskWith === "string" ? options.maskWith : "[REDACTED]";
|
|
1550
1775
|
const decide = (phase, result, block, allow2) => {
|
|
1551
1776
|
if (!result || !result.blocked) return allow2();
|
|
@@ -1579,20 +1804,26 @@ async function createProtection(options = {}) {
|
|
|
1579
1804
|
const headers = { ...meta.headers || {} };
|
|
1580
1805
|
for (const { rule, redactors } of redactions) {
|
|
1581
1806
|
const mask = maskFn(rule.category);
|
|
1582
|
-
|
|
1807
|
+
const transform = rule.action === "encode" ? htmlEscape : null;
|
|
1808
|
+
const pathRedactors = redactors.filter((r) => r.jsonPath);
|
|
1809
|
+
const spanRedactors = redactors.filter((r) => !r.jsonPath);
|
|
1810
|
+
if (pathRedactors.length) body = applyPathRedactors(body, pathRedactors, mask, screenCap, transform);
|
|
1811
|
+
if (!spanRedactors.length) continue;
|
|
1812
|
+
body = applyRedactors(body, spanRedactors, mask, transform);
|
|
1813
|
+
if (transform) continue;
|
|
1583
1814
|
for (const name of Object.keys(headers)) {
|
|
1584
1815
|
const value = headers[name];
|
|
1585
1816
|
if (typeof value === "string") {
|
|
1586
|
-
headers[name] = applyRedactors(value,
|
|
1817
|
+
headers[name] = applyRedactors(value, spanRedactors, mask);
|
|
1587
1818
|
} else if (Array.isArray(value)) {
|
|
1588
|
-
headers[name] = value.map((item) => typeof item === "string" ? applyRedactors(item,
|
|
1819
|
+
headers[name] = value.map((item) => typeof item === "string" ? applyRedactors(item, spanRedactors, mask) : item);
|
|
1589
1820
|
}
|
|
1590
1821
|
}
|
|
1591
1822
|
}
|
|
1592
1823
|
return { verdict: "redact", body, headers };
|
|
1593
1824
|
};
|
|
1594
1825
|
const screenResp = async (response) => {
|
|
1595
|
-
const text = await readTextResponse(response);
|
|
1826
|
+
const text = await readTextResponse(response, screenCap);
|
|
1596
1827
|
if (text == null) return response;
|
|
1597
1828
|
const r = screenText(text, { status: response.status, headers: headerObject(response.headers) });
|
|
1598
1829
|
if (r.verdict === "block") return leakResponse();
|
|
@@ -1605,12 +1836,15 @@ async function createProtection(options = {}) {
|
|
|
1605
1836
|
const chunks = [];
|
|
1606
1837
|
let size = 0;
|
|
1607
1838
|
let overflow = false;
|
|
1608
|
-
const MAX =
|
|
1839
|
+
const MAX = screenCap;
|
|
1609
1840
|
const collect = (chunk, enc) => {
|
|
1610
1841
|
if (chunk == null) return;
|
|
1611
1842
|
const buf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk, typeof enc === "string" ? enc : "utf8");
|
|
1612
1843
|
size += buf.length;
|
|
1613
1844
|
if (size > MAX) {
|
|
1845
|
+
for (const c of chunks) origWrite(c);
|
|
1846
|
+
chunks.length = 0;
|
|
1847
|
+
origWrite(buf);
|
|
1614
1848
|
overflow = true;
|
|
1615
1849
|
return;
|
|
1616
1850
|
}
|
|
@@ -1637,6 +1871,7 @@ async function createProtection(options = {}) {
|
|
|
1637
1871
|
return origEnd(cb);
|
|
1638
1872
|
}
|
|
1639
1873
|
collect(chunk, enc);
|
|
1874
|
+
if (overflow) return origEnd(cb);
|
|
1640
1875
|
const text = Buffer.concat(chunks).toString("utf8");
|
|
1641
1876
|
let ct = res.getHeader ? res.getHeader("content-type") : void 0;
|
|
1642
1877
|
if (Array.isArray(ct)) ct = ct[0];
|
|
@@ -1668,6 +1903,7 @@ async function createProtection(options = {}) {
|
|
|
1668
1903
|
if (r.headers && res.setHeader) {
|
|
1669
1904
|
const current = res.getHeaders ? res.getHeaders() : {};
|
|
1670
1905
|
for (const [name, value] of Object.entries(r.headers)) {
|
|
1906
|
+
if (name.toLowerCase() === "content-length") continue;
|
|
1671
1907
|
if (Array.isArray(value)) {
|
|
1672
1908
|
try {
|
|
1673
1909
|
res.setHeader(name, value);
|
|
@@ -1703,7 +1939,9 @@ async function createProtection(options = {}) {
|
|
|
1703
1939
|
};
|
|
1704
1940
|
const protection = {
|
|
1705
1941
|
mode,
|
|
1706
|
-
rules
|
|
1942
|
+
get rules() {
|
|
1943
|
+
return { request: requestRules, response: responseRules, egress: egressRules };
|
|
1944
|
+
},
|
|
1707
1945
|
// Screen a fetch Response through the response-phase rules (redact/block). Used by
|
|
1708
1946
|
// .fetch(), and by the Supabase guard on its forwarded upstream response.
|
|
1709
1947
|
screenResponse: (response) => screenResp(response),
|
|
@@ -1814,27 +2052,101 @@ async function createProtection(options = {}) {
|
|
|
1814
2052
|
}
|
|
1815
2053
|
};
|
|
1816
2054
|
if (options.egress) {
|
|
1817
|
-
protection.uninstallEgress = await installEgressGuard({
|
|
2055
|
+
protection.uninstallEgress = await installEgressGuard({
|
|
2056
|
+
shouldBlock: egressShouldBlock,
|
|
2057
|
+
onBlock: options.onEgressBlock,
|
|
2058
|
+
dnsScreen: options.screenDns !== false,
|
|
2059
|
+
allowHosts: options.allowHosts
|
|
2060
|
+
});
|
|
2061
|
+
}
|
|
2062
|
+
if (options.refreshMs > 0 && (options.siteUuid || options.token)) {
|
|
2063
|
+
const cwd = options.cwd ?? (typeof process !== "undefined" ? process.cwd() : void 0);
|
|
2064
|
+
let reportManifest = null;
|
|
2065
|
+
if (options.siteUuid && options.reportManifest !== false && cwd) {
|
|
2066
|
+
try {
|
|
2067
|
+
({ reportManifest } = await import("./refresh-manifest-XBOGN446.js"));
|
|
2068
|
+
} catch (err) {
|
|
2069
|
+
onError?.(err);
|
|
2070
|
+
}
|
|
2071
|
+
}
|
|
2072
|
+
const timer = setInterval(() => {
|
|
2073
|
+
(async () => {
|
|
2074
|
+
if (reportManifest) {
|
|
2075
|
+
try {
|
|
2076
|
+
await reportManifest(cwd);
|
|
2077
|
+
} catch (err) {
|
|
2078
|
+
onError?.(err);
|
|
2079
|
+
}
|
|
2080
|
+
}
|
|
2081
|
+
applyBundle(await resolveRules(options));
|
|
2082
|
+
})().catch((err) => onError?.(err));
|
|
2083
|
+
}, options.refreshMs);
|
|
2084
|
+
timer.unref?.();
|
|
2085
|
+
protection.stopRefresh = () => clearInterval(timer);
|
|
1818
2086
|
}
|
|
1819
2087
|
return protection;
|
|
1820
2088
|
}
|
|
1821
2089
|
function byPhase(rules, phase) {
|
|
1822
2090
|
return (rules ?? []).filter((r) => (r.phase ?? "request") === phase);
|
|
1823
2091
|
}
|
|
1824
|
-
async function readTextResponse(response) {
|
|
2092
|
+
async function readTextResponse(response, cap = DEFAULT_SCREEN_CAP) {
|
|
1825
2093
|
if (!response || typeof response.clone !== "function") return null;
|
|
1826
2094
|
const ct = (response.headers?.get?.("content-type") || "").toLowerCase();
|
|
1827
2095
|
const isText = ct === "" || /(json|text|xml|html|javascript|csv|yaml|x-www-form-urlencoded)/.test(ct);
|
|
1828
2096
|
if (!isText) return null;
|
|
1829
2097
|
const len = Number(response.headers?.get?.("content-length") || 0);
|
|
1830
|
-
if (len && len >
|
|
2098
|
+
if (len && len > cap) return null;
|
|
2099
|
+
let clone;
|
|
1831
2100
|
try {
|
|
1832
|
-
|
|
1833
|
-
|
|
2101
|
+
clone = response.clone();
|
|
2102
|
+
} catch {
|
|
2103
|
+
return null;
|
|
2104
|
+
}
|
|
2105
|
+
const body = clone.body;
|
|
2106
|
+
if (body && typeof body.getReader === "function") {
|
|
2107
|
+
const reader = body.getReader();
|
|
2108
|
+
const chunks = [];
|
|
2109
|
+
let size = 0;
|
|
2110
|
+
let over = false;
|
|
2111
|
+
try {
|
|
2112
|
+
for (; ; ) {
|
|
2113
|
+
const { done, value } = await reader.read();
|
|
2114
|
+
if (done) break;
|
|
2115
|
+
if (!value) continue;
|
|
2116
|
+
size += value.byteLength;
|
|
2117
|
+
if (over) continue;
|
|
2118
|
+
if (size > cap) {
|
|
2119
|
+
over = true;
|
|
2120
|
+
continue;
|
|
2121
|
+
}
|
|
2122
|
+
chunks.push(value);
|
|
2123
|
+
}
|
|
2124
|
+
} catch {
|
|
2125
|
+
return null;
|
|
2126
|
+
}
|
|
2127
|
+
if (over) return null;
|
|
2128
|
+
try {
|
|
2129
|
+
return new TextDecoder().decode(concatBytes(chunks, size));
|
|
2130
|
+
} catch {
|
|
2131
|
+
return null;
|
|
2132
|
+
}
|
|
2133
|
+
}
|
|
2134
|
+
try {
|
|
2135
|
+
const text = await clone.text();
|
|
2136
|
+
return text.length > cap ? null : text;
|
|
1834
2137
|
} catch {
|
|
1835
2138
|
return null;
|
|
1836
2139
|
}
|
|
1837
2140
|
}
|
|
2141
|
+
function concatBytes(chunks, total) {
|
|
2142
|
+
const out = new Uint8Array(total);
|
|
2143
|
+
let offset = 0;
|
|
2144
|
+
for (const c of chunks) {
|
|
2145
|
+
out.set(c, offset);
|
|
2146
|
+
offset += c.byteLength;
|
|
2147
|
+
}
|
|
2148
|
+
return out;
|
|
2149
|
+
}
|
|
1838
2150
|
function headerObject(headers) {
|
|
1839
2151
|
const out = {};
|
|
1840
2152
|
headers?.forEach?.((v, k) => {
|
|
@@ -1852,30 +2164,137 @@ function extractRedactors(rule) {
|
|
|
1852
2164
|
const m = c.match;
|
|
1853
2165
|
if (!m) continue;
|
|
1854
2166
|
if (m.type === "regex" && typeof m.value === "string") {
|
|
1855
|
-
const
|
|
1856
|
-
if (
|
|
1857
|
-
const flags =
|
|
2167
|
+
const safe = safeRegExp(m.value);
|
|
2168
|
+
if (safe) {
|
|
2169
|
+
const flags = safe.flags.includes("g") ? safe.flags : safe.flags + "g";
|
|
1858
2170
|
try {
|
|
1859
|
-
out.push({ re: new RegExp(
|
|
2171
|
+
out.push({ re: new RegExp(safe.source, flags) });
|
|
1860
2172
|
} catch {
|
|
1861
2173
|
}
|
|
1862
2174
|
}
|
|
1863
2175
|
} else if ((m.type === "contains" || m.type === "stripos") && m.value != null) {
|
|
1864
2176
|
out.push({ literal: String(m.value) });
|
|
2177
|
+
} else if (m.type === "array_key_value" && m.match && isBodyParam(c.parameter)) {
|
|
2178
|
+
const keys = Array.isArray(m.key) ? m.key : [m.key];
|
|
2179
|
+
for (const key of keys) {
|
|
2180
|
+
out.push({ jsonPath: String(key).split("."), condition: m.match });
|
|
2181
|
+
}
|
|
1865
2182
|
}
|
|
1866
2183
|
}
|
|
1867
2184
|
};
|
|
1868
2185
|
walk(rule.rule_v2);
|
|
1869
2186
|
return out;
|
|
1870
2187
|
}
|
|
1871
|
-
function
|
|
2188
|
+
function htmlEscape(str) {
|
|
2189
|
+
return String(str).replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[c]);
|
|
2190
|
+
}
|
|
2191
|
+
function applyRedactors(body, redactors, mask, transform) {
|
|
1872
2192
|
let out = body;
|
|
1873
2193
|
for (const r of redactors) {
|
|
1874
|
-
if (r.re) out = out.replace(r.re, mask);
|
|
1875
|
-
else if (r.literal) out = out.split(r.literal).join(mask);
|
|
2194
|
+
if (r.re) out = out.replace(r.re, transform ? (m) => transform(m) : mask);
|
|
2195
|
+
else if (r.literal) out = out.split(r.literal).join(transform ? transform(r.literal) : mask);
|
|
1876
2196
|
}
|
|
1877
2197
|
return out;
|
|
1878
2198
|
}
|
|
2199
|
+
function isBodyParam(parameter) {
|
|
2200
|
+
return parameter == null || parameter === "response.body" || parameter === "raw" || parameter === "response.raw";
|
|
2201
|
+
}
|
|
2202
|
+
function conditionPredicate(condition) {
|
|
2203
|
+
if (!condition || !condition.type) return () => true;
|
|
2204
|
+
return (value) => {
|
|
2205
|
+
try {
|
|
2206
|
+
return matchValue(condition.type, value, condition.value, condition);
|
|
2207
|
+
} catch {
|
|
2208
|
+
return false;
|
|
2209
|
+
}
|
|
2210
|
+
};
|
|
2211
|
+
}
|
|
2212
|
+
var DEFAULT_SCREEN_CAP = 512 * 1024;
|
|
2213
|
+
function responseScreenCap(rules) {
|
|
2214
|
+
let cap = DEFAULT_SCREEN_CAP;
|
|
2215
|
+
for (const r of rules ?? []) {
|
|
2216
|
+
if (r && r.bypass_limit === true) return Infinity;
|
|
2217
|
+
const n = Number(r && r.max_bytes);
|
|
2218
|
+
if (Number.isFinite(n) && n > cap) cap = n;
|
|
2219
|
+
}
|
|
2220
|
+
return cap;
|
|
2221
|
+
}
|
|
2222
|
+
function applyPathRedactors(text, pathRedactors, mask, cap, transform) {
|
|
2223
|
+
if (!pathRedactors.length || typeof text !== "string" || text.length > cap) return text;
|
|
2224
|
+
const head = text.trimStart()[0];
|
|
2225
|
+
if (head !== "{" && head !== "[") return text;
|
|
2226
|
+
const preserved = preserveBigInts(text);
|
|
2227
|
+
let obj;
|
|
2228
|
+
try {
|
|
2229
|
+
obj = JSON.parse(preserved);
|
|
2230
|
+
} catch {
|
|
2231
|
+
return text;
|
|
2232
|
+
}
|
|
2233
|
+
let changed = false;
|
|
2234
|
+
for (const r of pathRedactors) {
|
|
2235
|
+
const pred = conditionPredicate(r.condition);
|
|
2236
|
+
walkLeaves(obj, r.jsonPath, (loc) => {
|
|
2237
|
+
try {
|
|
2238
|
+
if (pred(loc.value)) {
|
|
2239
|
+
loc.parent[loc.key] = transform ? transform(String(loc.value)) : mask;
|
|
2240
|
+
changed = true;
|
|
2241
|
+
}
|
|
2242
|
+
} catch {
|
|
2243
|
+
}
|
|
2244
|
+
});
|
|
2245
|
+
}
|
|
2246
|
+
return changed ? restoreBigInts(JSON.stringify(obj)) : text;
|
|
2247
|
+
}
|
|
2248
|
+
var BIGINT_OPEN = "__PSBIGINT_9c2f__";
|
|
2249
|
+
var BIGINT_CLOSE = "__DNEGIB__";
|
|
2250
|
+
function preserveBigInts(text) {
|
|
2251
|
+
let out = "";
|
|
2252
|
+
let inStr = false;
|
|
2253
|
+
for (let i = 0; i < text.length; ) {
|
|
2254
|
+
const ch = text[i];
|
|
2255
|
+
if (inStr) {
|
|
2256
|
+
out += ch;
|
|
2257
|
+
if (ch === "\\") {
|
|
2258
|
+
out += text[i + 1] ?? "";
|
|
2259
|
+
i += 2;
|
|
2260
|
+
continue;
|
|
2261
|
+
}
|
|
2262
|
+
if (ch === '"') inStr = false;
|
|
2263
|
+
i++;
|
|
2264
|
+
continue;
|
|
2265
|
+
}
|
|
2266
|
+
if (ch === '"') {
|
|
2267
|
+
inStr = true;
|
|
2268
|
+
out += ch;
|
|
2269
|
+
i++;
|
|
2270
|
+
continue;
|
|
2271
|
+
}
|
|
2272
|
+
if (ch === "-" || ch >= "0" && ch <= "9") {
|
|
2273
|
+
let j = ch === "-" ? i + 1 : i;
|
|
2274
|
+
let digits = 0;
|
|
2275
|
+
while (j < text.length && text[j] >= "0" && text[j] <= "9") {
|
|
2276
|
+
digits++;
|
|
2277
|
+
j++;
|
|
2278
|
+
}
|
|
2279
|
+
const next = text[j];
|
|
2280
|
+
const isIntToken = digits > 0 && next !== "." && next !== "e" && next !== "E";
|
|
2281
|
+
if (isIntToken && digits >= 16) {
|
|
2282
|
+
out += `"${BIGINT_OPEN}${text.slice(i, j)}${BIGINT_CLOSE}"`;
|
|
2283
|
+
} else {
|
|
2284
|
+
out += text.slice(i, j || i + 1);
|
|
2285
|
+
}
|
|
2286
|
+
i = j > i ? j : i + 1;
|
|
2287
|
+
continue;
|
|
2288
|
+
}
|
|
2289
|
+
out += ch;
|
|
2290
|
+
i++;
|
|
2291
|
+
}
|
|
2292
|
+
return out;
|
|
2293
|
+
}
|
|
2294
|
+
function restoreBigInts(text) {
|
|
2295
|
+
if (!text.includes(BIGINT_OPEN)) return text;
|
|
2296
|
+
return text.replace(new RegExp(`"${BIGINT_OPEN}(-?\\d+)${BIGINT_CLOSE}"`, "g"), "$1");
|
|
2297
|
+
}
|
|
1879
2298
|
function rebuildResponse(response, body, redactedHeaders) {
|
|
1880
2299
|
const headers = new Headers(response.headers);
|
|
1881
2300
|
headers.delete("content-length");
|