@patchstack/connect 0.3.30 → 0.3.32
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 +180 -20
- package/README.md +17 -3
- package/dist/{chunk-LLKP5EJS.js → chunk-3G2I6QL6.js} +1 -1
- package/dist/chunk-3G2I6QL6.js.map +1 -0
- package/dist/cli.js +309 -130
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +48 -18
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +58 -3
- package/dist/index.d.ts +58 -3
- package/dist/index.js +48 -18
- package/dist/index.js.map +1 -1
- package/dist/protect.cjs +2749 -1373
- package/dist/protect.cjs.map +1 -1
- package/dist/protect.d.cts +316 -0
- package/dist/protect.d.ts +134 -18
- package/dist/protect.edge.js +2776 -1412
- package/dist/protect.edge.js.map +3 -4
- package/dist/protect.js +2697 -1355
- package/dist/protect.js.map +1 -1
- package/dist/{refresh-manifest-ZSP76JWQ.js → refresh-manifest-SWCPQ52Z.js} +49 -19
- package/dist/refresh-manifest-SWCPQ52Z.js.map +1 -0
- package/package.json +44 -11
- package/dist/chunk-LLKP5EJS.js.map +0 -1
- package/dist/refresh-manifest-ZSP76JWQ.js.map +0 -1
package/dist/protect.cjs
CHANGED
|
@@ -5,8 +5,13 @@ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
|
5
5
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
6
|
var __getProtoOf = Object.getPrototypeOf;
|
|
7
7
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
-
var __esm = (fn, res) => function __init() {
|
|
9
|
-
|
|
8
|
+
var __esm = (fn, res, err) => function __init() {
|
|
9
|
+
if (err) throw err[0];
|
|
10
|
+
try {
|
|
11
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
12
|
+
} catch (e) {
|
|
13
|
+
throw err = [e], e;
|
|
14
|
+
}
|
|
10
15
|
};
|
|
11
16
|
var __export = (target, all) => {
|
|
12
17
|
for (var name in all)
|
|
@@ -682,7 +687,7 @@ async function walkNodeModules(cwd) {
|
|
|
682
687
|
seen.set(await (0, import_promises5.realpath)(cwd), -1);
|
|
683
688
|
} catch {
|
|
684
689
|
}
|
|
685
|
-
return dedupeByShallowest(await walk(root, 0, seen));
|
|
690
|
+
return dedupeByShallowest(await walk(root, 0, seen, cwd));
|
|
686
691
|
}
|
|
687
692
|
function dedupeByShallowest(found) {
|
|
688
693
|
const best = /* @__PURE__ */ new Map();
|
|
@@ -692,7 +697,7 @@ function dedupeByShallowest(found) {
|
|
|
692
697
|
}
|
|
693
698
|
return [...best.values()].map(({ entry }) => entry);
|
|
694
699
|
}
|
|
695
|
-
async function walk(dir, depth, seen) {
|
|
700
|
+
async function walk(dir, depth, seen, root) {
|
|
696
701
|
let names;
|
|
697
702
|
try {
|
|
698
703
|
names = await (0, import_promises5.readdir)(dir);
|
|
@@ -704,7 +709,7 @@ async function walk(dir, depth, seen) {
|
|
|
704
709
|
const fullPath = import_node_path4.default.join(dir, name);
|
|
705
710
|
const resolved = await claim(fullPath, seen, depth);
|
|
706
711
|
if (resolved === null) return [];
|
|
707
|
-
if (PACKAGE_STORE_DIRS.has(name)) return walk(fullPath, depth, seen);
|
|
712
|
+
if (PACKAGE_STORE_DIRS.has(name)) return walk(fullPath, depth, seen, root);
|
|
708
713
|
if (name.startsWith("@")) {
|
|
709
714
|
let subNames;
|
|
710
715
|
try {
|
|
@@ -717,16 +722,16 @@ async function walk(dir, depth, seen) {
|
|
|
717
722
|
const scopedDir = import_node_path4.default.join(fullPath, sub);
|
|
718
723
|
const scopedReal = await claim(scopedDir, seen, depth);
|
|
719
724
|
if (scopedReal === null) return [];
|
|
720
|
-
return packageAndNested(scopedDir, scopedReal, depth, seen);
|
|
725
|
+
return packageAndNested(scopedDir, scopedReal, depth, seen, root);
|
|
721
726
|
});
|
|
722
727
|
return scoped.flat();
|
|
723
728
|
}
|
|
724
|
-
return packageAndNested(fullPath, resolved, depth, seen);
|
|
729
|
+
return packageAndNested(fullPath, resolved, depth, seen, root);
|
|
725
730
|
});
|
|
726
731
|
return perEntry.flat();
|
|
727
732
|
}
|
|
728
|
-
async function packageAndNested(pkgDir, resolved, depth, seen) {
|
|
729
|
-
const [own, nested] = await Promise.all([readPackage(pkgDir, depth), walkNested(pkgDir, depth, seen)]);
|
|
733
|
+
async function packageAndNested(pkgDir, resolved, depth, seen, root) {
|
|
734
|
+
const [own, nested] = await Promise.all([readPackage(pkgDir, depth, root), walkNested(pkgDir, depth, seen, root)]);
|
|
730
735
|
return own === null ? nested : [{ entry: own, realpath: resolved, depth }, ...nested];
|
|
731
736
|
}
|
|
732
737
|
async function mapLimit(items, fn) {
|
|
@@ -741,7 +746,10 @@ async function mapLimit(items, fn) {
|
|
|
741
746
|
await Promise.all(workers);
|
|
742
747
|
return results;
|
|
743
748
|
}
|
|
744
|
-
|
|
749
|
+
function installLocation(root, pkgDir, impl = import_node_path4.default) {
|
|
750
|
+
return impl.relative(root, pkgDir).split(impl.sep).join("/");
|
|
751
|
+
}
|
|
752
|
+
async function readPackage(pkgDir, depth, root) {
|
|
745
753
|
let raw;
|
|
746
754
|
try {
|
|
747
755
|
raw = await (0, import_promises5.readFile)(import_node_path4.default.join(pkgDir, "package.json"), "utf8");
|
|
@@ -760,14 +768,14 @@ async function readPackage(pkgDir, depth) {
|
|
|
760
768
|
if (typeof parsed.version !== "string" || parsed.version.length === 0) {
|
|
761
769
|
return null;
|
|
762
770
|
}
|
|
763
|
-
return { name: parsed.name, version: parsed.version, direct: depth === 0 };
|
|
771
|
+
return { name: parsed.name, version: parsed.version, path: installLocation(root, pkgDir), direct: depth === 0 };
|
|
764
772
|
}
|
|
765
|
-
async function walkNested(pkgDir, depth, seen) {
|
|
773
|
+
async function walkNested(pkgDir, depth, seen, root) {
|
|
766
774
|
const nested = import_node_path4.default.join(pkgDir, "node_modules");
|
|
767
775
|
if (await claim(nested, seen, depth + 1) === null) {
|
|
768
776
|
return [];
|
|
769
777
|
}
|
|
770
|
-
return walk(nested, depth + 1, seen);
|
|
778
|
+
return walk(nested, depth + 1, seen, root);
|
|
771
779
|
}
|
|
772
780
|
async function claim(dir, seen, depth) {
|
|
773
781
|
let resolved;
|
|
@@ -1453,21 +1461,33 @@ var init_parsers = __esm({
|
|
|
1453
1461
|
});
|
|
1454
1462
|
|
|
1455
1463
|
// src/normalize.ts
|
|
1456
|
-
function buildWirePayload(manifest) {
|
|
1464
|
+
function buildWirePayload(manifest, options = {}) {
|
|
1465
|
+
const withPaths = options.installPaths === true;
|
|
1457
1466
|
const seen = /* @__PURE__ */ new Map();
|
|
1458
1467
|
const wirePackages = [];
|
|
1468
|
+
const byIdentity = /* @__PURE__ */ new Map();
|
|
1469
|
+
let entriesWithoutPath = 0;
|
|
1459
1470
|
for (const entry of manifest.packages) {
|
|
1471
|
+
const identity = `${entry.name}@${entry.version}`;
|
|
1472
|
+
const location = withPaths && entry.path !== void 0 && entry.path !== "" ? entry.path : void 0;
|
|
1473
|
+
if (location === void 0) entriesWithoutPath++;
|
|
1474
|
+
const existing = byIdentity.get(identity);
|
|
1475
|
+
if (existing) {
|
|
1476
|
+
if (location !== void 0) addPath(existing, location);
|
|
1477
|
+
continue;
|
|
1478
|
+
}
|
|
1460
1479
|
const versions = seen.get(entry.name);
|
|
1461
1480
|
if (versions) {
|
|
1462
|
-
if (versions.has(entry.version)) {
|
|
1463
|
-
continue;
|
|
1464
|
-
}
|
|
1465
1481
|
versions.add(entry.version);
|
|
1466
1482
|
} else {
|
|
1467
1483
|
seen.set(entry.name, /* @__PURE__ */ new Set([entry.version]));
|
|
1468
1484
|
}
|
|
1469
|
-
|
|
1485
|
+
const wire = { name: entry.name, version: entry.version };
|
|
1486
|
+
if (location !== void 0) addPath(wire, location);
|
|
1487
|
+
byIdentity.set(identity, wire);
|
|
1488
|
+
wirePackages.push(wire);
|
|
1470
1489
|
}
|
|
1490
|
+
for (const wire of wirePackages) wire.paths?.sort();
|
|
1471
1491
|
wirePackages.sort((a, b) => {
|
|
1472
1492
|
if (a.name === b.name) {
|
|
1473
1493
|
return compareVersions(a.version, b.version);
|
|
@@ -1481,7 +1501,18 @@ function buildWirePayload(manifest) {
|
|
|
1481
1501
|
}
|
|
1482
1502
|
}
|
|
1483
1503
|
return {
|
|
1484
|
-
payload: {
|
|
1504
|
+
payload: {
|
|
1505
|
+
ecosystem: manifest.ecosystem,
|
|
1506
|
+
packages: wirePackages,
|
|
1507
|
+
// Every entry had a location, so a missing path anywhere means the package is not installed there.
|
|
1508
|
+
// One entry without one forfeits that for the whole payload: a consumer reading a short `paths` has
|
|
1509
|
+
// no way to tell which entry was the incomplete one.
|
|
1510
|
+
//
|
|
1511
|
+
// No separate check for "locations were not requested": with them off every entry counts as without
|
|
1512
|
+
// one, so this is already false — which is the default, and the safe direction. An explicit
|
|
1513
|
+
// `withPaths &&` here was a guard no test could distinguish from its absence.
|
|
1514
|
+
installPathsComplete: entriesWithoutPath === 0 && manifest.packages.length > 0
|
|
1515
|
+
},
|
|
1485
1516
|
stats: {
|
|
1486
1517
|
uniqueNames: seen.size,
|
|
1487
1518
|
duplicateNames,
|
|
@@ -1489,6 +1520,10 @@ function buildWirePayload(manifest) {
|
|
|
1489
1520
|
}
|
|
1490
1521
|
};
|
|
1491
1522
|
}
|
|
1523
|
+
function addPath(wire, installPath) {
|
|
1524
|
+
if (wire.paths === void 0) wire.paths = [installPath];
|
|
1525
|
+
else if (!wire.paths.includes(installPath)) wire.paths.push(installPath);
|
|
1526
|
+
}
|
|
1492
1527
|
function compareVersions(a, b) {
|
|
1493
1528
|
if (a === b) {
|
|
1494
1529
|
return 0;
|
|
@@ -1594,6 +1629,220 @@ __export(runtime_exports, {
|
|
|
1594
1629
|
});
|
|
1595
1630
|
module.exports = __toCommonJS(runtime_exports);
|
|
1596
1631
|
|
|
1632
|
+
// src/protect/client-ip.js
|
|
1633
|
+
var ZONE = /^[A-Za-z0-9._-]{1,64}$/;
|
|
1634
|
+
var POLICY_FIELDS = Object.freeze(["peers", "hops", "header", "isTrusted"]);
|
|
1635
|
+
var IP_SOURCES = Object.freeze(["runtime", "trusted-proxy", "unavailable"]);
|
|
1636
|
+
var DEFAULT_FORWARDED_HEADER = "x-forwarded-for";
|
|
1637
|
+
function parseIpv4(value) {
|
|
1638
|
+
const parts = value.split(".");
|
|
1639
|
+
if (parts.length !== 4) return null;
|
|
1640
|
+
const octets = [];
|
|
1641
|
+
for (const part of parts) {
|
|
1642
|
+
if (!/^\d{1,3}$/.test(part)) return null;
|
|
1643
|
+
if (part.length > 1 && part.startsWith("0")) return null;
|
|
1644
|
+
const n = Number(part);
|
|
1645
|
+
if (n > 255) return null;
|
|
1646
|
+
octets.push(n);
|
|
1647
|
+
}
|
|
1648
|
+
return octets;
|
|
1649
|
+
}
|
|
1650
|
+
function parseIpv6(value) {
|
|
1651
|
+
if (value === "" || value.includes(":::")) return null;
|
|
1652
|
+
let head = value;
|
|
1653
|
+
let embedded = null;
|
|
1654
|
+
const lastColon = head.lastIndexOf(":");
|
|
1655
|
+
const tail = lastColon === -1 ? "" : head.slice(lastColon + 1);
|
|
1656
|
+
if (tail.includes(".")) {
|
|
1657
|
+
embedded = parseIpv4(tail);
|
|
1658
|
+
if (embedded === null) return null;
|
|
1659
|
+
head = head.slice(0, lastColon + 1) + "0:0";
|
|
1660
|
+
}
|
|
1661
|
+
const halves = head.split("::");
|
|
1662
|
+
if (halves.length > 2) return null;
|
|
1663
|
+
const readGroups = (text) => {
|
|
1664
|
+
if (text === "") return [];
|
|
1665
|
+
const groups2 = [];
|
|
1666
|
+
for (const g of text.split(":")) {
|
|
1667
|
+
if (!/^[0-9a-fA-F]{1,4}$/.test(g)) return null;
|
|
1668
|
+
groups2.push(Number.parseInt(g, 16));
|
|
1669
|
+
}
|
|
1670
|
+
return groups2;
|
|
1671
|
+
};
|
|
1672
|
+
const left = readGroups(halves[0]);
|
|
1673
|
+
const right = halves.length === 2 ? readGroups(halves[1]) : [];
|
|
1674
|
+
if (left === null || right === null) return null;
|
|
1675
|
+
let groups;
|
|
1676
|
+
if (halves.length === 2) {
|
|
1677
|
+
const fill = 8 - (left.length + right.length);
|
|
1678
|
+
if (fill < 1) return null;
|
|
1679
|
+
groups = [...left, ...new Array(fill).fill(0), ...right];
|
|
1680
|
+
} else {
|
|
1681
|
+
groups = left;
|
|
1682
|
+
}
|
|
1683
|
+
if (groups.length !== 8) return null;
|
|
1684
|
+
if (embedded !== null) {
|
|
1685
|
+
groups[6] = embedded[0] << 8 | embedded[1];
|
|
1686
|
+
groups[7] = embedded[2] << 8 | embedded[3];
|
|
1687
|
+
}
|
|
1688
|
+
return groups;
|
|
1689
|
+
}
|
|
1690
|
+
function toNumeric(value) {
|
|
1691
|
+
const text = String(value).trim();
|
|
1692
|
+
if (text === "") return null;
|
|
1693
|
+
const zoneParts = text.split("%");
|
|
1694
|
+
if (zoneParts.length > 2) return null;
|
|
1695
|
+
const addr = zoneParts[0];
|
|
1696
|
+
const zone = zoneParts.length === 2 ? zoneParts[1] : null;
|
|
1697
|
+
if (zone !== null && (!ZONE.test(zone) || !addr.includes(":"))) return null;
|
|
1698
|
+
const v4 = parseIpv4(addr);
|
|
1699
|
+
if (v4 !== null) {
|
|
1700
|
+
return {
|
|
1701
|
+
bits: 32,
|
|
1702
|
+
value: v4.reduce((acc, octet) => acc << 8n | BigInt(octet), 0n),
|
|
1703
|
+
canonical: v4.join(".")
|
|
1704
|
+
};
|
|
1705
|
+
}
|
|
1706
|
+
if (!addr.includes(":")) return null;
|
|
1707
|
+
const v6 = parseIpv6(addr);
|
|
1708
|
+
if (v6 === null) return null;
|
|
1709
|
+
const mapped = v6.slice(0, 5).every((g) => g === 0) && v6[5] === 65535;
|
|
1710
|
+
if (mapped) {
|
|
1711
|
+
if (zone !== null) return null;
|
|
1712
|
+
const octets = [v6[6] >> 8, v6[6] & 255, v6[7] >> 8, v6[7] & 255];
|
|
1713
|
+
return { bits: 32, value: BigInt(v6[6]) << 16n | BigInt(v6[7]), canonical: octets.join(".") };
|
|
1714
|
+
}
|
|
1715
|
+
return {
|
|
1716
|
+
bits: 128,
|
|
1717
|
+
value: v6.reduce((acc, group) => acc << 16n | BigInt(group), 0n),
|
|
1718
|
+
// The zone survives: without it a link-local address does not identify an interface.
|
|
1719
|
+
canonical: zone === null ? canonicalIpv6(v6) : `${canonicalIpv6(v6)}%${zone}`
|
|
1720
|
+
};
|
|
1721
|
+
}
|
|
1722
|
+
function canonicalIpv6(groups) {
|
|
1723
|
+
let bestStart = -1;
|
|
1724
|
+
let bestLength = 0;
|
|
1725
|
+
let runStart = -1;
|
|
1726
|
+
for (let i = 0; i <= groups.length; i++) {
|
|
1727
|
+
if (i < groups.length && groups[i] === 0) {
|
|
1728
|
+
if (runStart === -1) runStart = i;
|
|
1729
|
+
} else if (runStart !== -1) {
|
|
1730
|
+
const length = i - runStart;
|
|
1731
|
+
if (length > bestLength) {
|
|
1732
|
+
bestLength = length;
|
|
1733
|
+
bestStart = runStart;
|
|
1734
|
+
}
|
|
1735
|
+
runStart = -1;
|
|
1736
|
+
}
|
|
1737
|
+
}
|
|
1738
|
+
const hex = groups.map((g) => g.toString(16));
|
|
1739
|
+
if (bestLength < 2) return hex.join(":");
|
|
1740
|
+
const head = hex.slice(0, bestStart).join(":");
|
|
1741
|
+
const tail = hex.slice(bestStart + bestLength).join(":");
|
|
1742
|
+
return `${head}::${tail}`;
|
|
1743
|
+
}
|
|
1744
|
+
function canonicalIp(value) {
|
|
1745
|
+
return toNumeric(value)?.canonical ?? null;
|
|
1746
|
+
}
|
|
1747
|
+
function parseCidr(entry) {
|
|
1748
|
+
if (typeof entry !== "string") return null;
|
|
1749
|
+
if (entry.includes("%")) return null;
|
|
1750
|
+
const parts = entry.trim().split("/");
|
|
1751
|
+
if (parts.length > 2) return null;
|
|
1752
|
+
const numeric = toNumeric(parts[0]);
|
|
1753
|
+
if (numeric === null) return null;
|
|
1754
|
+
let length = numeric.bits;
|
|
1755
|
+
if (parts.length === 2) {
|
|
1756
|
+
if (!/^\d{1,3}$/.test(parts[1])) return null;
|
|
1757
|
+
length = Number(parts[1]);
|
|
1758
|
+
if (length > numeric.bits) return null;
|
|
1759
|
+
}
|
|
1760
|
+
const shift = BigInt(numeric.bits - length);
|
|
1761
|
+
return { bits: numeric.bits, network: numeric.value >> shift, shift };
|
|
1762
|
+
}
|
|
1763
|
+
function readTrustPolicy(trustedProxy) {
|
|
1764
|
+
if (trustedProxy === null || typeof trustedProxy !== "object" || Array.isArray(trustedProxy)) return null;
|
|
1765
|
+
const has = (field) => Object.hasOwn(trustedProxy, field);
|
|
1766
|
+
for (const key of Object.keys(trustedProxy)) {
|
|
1767
|
+
if (!POLICY_FIELDS.includes(key)) return null;
|
|
1768
|
+
}
|
|
1769
|
+
if (has("header") && (typeof trustedProxy.header !== "string" || trustedProxy.header.trim() === "")) {
|
|
1770
|
+
return null;
|
|
1771
|
+
}
|
|
1772
|
+
if (has("hops") && (!Number.isInteger(trustedProxy.hops) || trustedProxy.hops < 1)) return null;
|
|
1773
|
+
if (has("isTrusted") && typeof trustedProxy.isTrusted !== "function") return null;
|
|
1774
|
+
const header = has("header") ? trustedProxy.header.trim().toLowerCase() : DEFAULT_FORWARDED_HEADER;
|
|
1775
|
+
const cidrs = [];
|
|
1776
|
+
if (has("peers")) {
|
|
1777
|
+
if (!Array.isArray(trustedProxy.peers) || trustedProxy.peers.length === 0) return null;
|
|
1778
|
+
for (const entry of trustedProxy.peers) {
|
|
1779
|
+
const parsed = parseCidr(entry);
|
|
1780
|
+
if (parsed === null) return null;
|
|
1781
|
+
cidrs.push(parsed);
|
|
1782
|
+
}
|
|
1783
|
+
}
|
|
1784
|
+
const hops = has("hops") ? trustedProxy.hops : null;
|
|
1785
|
+
const predicate = has("isTrusted") ? trustedProxy.isTrusted : null;
|
|
1786
|
+
if (cidrs.length === 0 && hops === null && predicate === null) return null;
|
|
1787
|
+
return { header, cidrs, hops, predicate };
|
|
1788
|
+
}
|
|
1789
|
+
function isTrustedPeer(policy, value) {
|
|
1790
|
+
const numeric = toNumeric(value);
|
|
1791
|
+
if (numeric === null) return false;
|
|
1792
|
+
for (const cidr of policy.cidrs) {
|
|
1793
|
+
if (cidr.bits === numeric.bits && numeric.value >> cidr.shift === cidr.network) return true;
|
|
1794
|
+
}
|
|
1795
|
+
if (policy.predicate !== null) {
|
|
1796
|
+
try {
|
|
1797
|
+
if (policy.predicate(String(value)) === true) return true;
|
|
1798
|
+
} catch {
|
|
1799
|
+
return false;
|
|
1800
|
+
}
|
|
1801
|
+
}
|
|
1802
|
+
return false;
|
|
1803
|
+
}
|
|
1804
|
+
var UNSPECIFIED = /* @__PURE__ */ new Set(["0.0.0.0", "::"]);
|
|
1805
|
+
function identifyingIp(value) {
|
|
1806
|
+
const canonical = canonicalIp(value);
|
|
1807
|
+
return canonical === null || UNSPECIFIED.has(canonical) ? null : canonical;
|
|
1808
|
+
}
|
|
1809
|
+
function chainFrom(headers, header) {
|
|
1810
|
+
if (headers === null || typeof headers !== "object" || !Object.hasOwn(headers, header)) return [];
|
|
1811
|
+
const raw = headers[header];
|
|
1812
|
+
const value = Array.isArray(raw) ? raw.join(",") : raw;
|
|
1813
|
+
if (typeof value !== "string" || value.trim() === "") return [];
|
|
1814
|
+
return value.split(",").map((entry) => entry.trim()).filter((entry) => entry !== "");
|
|
1815
|
+
}
|
|
1816
|
+
function resolveClientIp(input) {
|
|
1817
|
+
const headers = input.headers ?? {};
|
|
1818
|
+
const peer = identifyingIp(input.peer);
|
|
1819
|
+
const policy = readTrustPolicy(input.trustedProxy);
|
|
1820
|
+
if (peer === null) return { ip: null, source: "unavailable" };
|
|
1821
|
+
if (policy === null) return { ip: peer, source: "runtime" };
|
|
1822
|
+
const gatedByAddress = policy.cidrs.length > 0 || policy.predicate !== null;
|
|
1823
|
+
const peerTrusted = gatedByAddress ? isTrustedPeer(policy, peer) : policy.hops !== null;
|
|
1824
|
+
if (!peerTrusted) return { ip: peer, source: "runtime" };
|
|
1825
|
+
const chain = chainFrom(headers, policy.header);
|
|
1826
|
+
if (chain.length === 0) return { ip: peer, source: "runtime" };
|
|
1827
|
+
if (policy.hops !== null) {
|
|
1828
|
+
const index = chain.length - policy.hops;
|
|
1829
|
+
const candidate = index >= 0 ? chain[index] : void 0;
|
|
1830
|
+
const canonical = identifyingIp(candidate);
|
|
1831
|
+
return canonical === null ? { ip: peer, source: "runtime" } : { ip: canonical, source: "trusted-proxy" };
|
|
1832
|
+
}
|
|
1833
|
+
for (let i = chain.length - 1; i >= 0; i--) {
|
|
1834
|
+
const canonical = identifyingIp(chain[i]);
|
|
1835
|
+
if (canonical === null) return { ip: peer, source: "runtime" };
|
|
1836
|
+
if (!isTrustedPeer(policy, canonical)) return { ip: canonical, source: "trusted-proxy" };
|
|
1837
|
+
}
|
|
1838
|
+
return { ip: peer, source: "runtime" };
|
|
1839
|
+
}
|
|
1840
|
+
function clientIpFields(resolved) {
|
|
1841
|
+
const established = resolved?.source === "runtime" || resolved?.source === "trusted-proxy";
|
|
1842
|
+
const address = identifyingIp(resolved?.ip);
|
|
1843
|
+
return established && address !== null ? { client_ip: address, client_ip_source: resolved.source } : { client_ip_source: "unavailable" };
|
|
1844
|
+
}
|
|
1845
|
+
|
|
1597
1846
|
// src/protect/safe-origin.js
|
|
1598
1847
|
function isSafeOrigin(value) {
|
|
1599
1848
|
try {
|
|
@@ -1950,7 +2199,7 @@ var RequestResolver = class {
|
|
|
1950
2199
|
return req.headers?.host ? [req.headers.host] : [];
|
|
1951
2200
|
case "REMOTE_ADDR":
|
|
1952
2201
|
case "ip":
|
|
1953
|
-
return [req.ip
|
|
2202
|
+
return [typeof req.ip === "string" ? req.ip : ""];
|
|
1954
2203
|
case "CONTENT_TYPE":
|
|
1955
2204
|
return req.headers?.["content-type"] ? [req.headers["content-type"]] : [];
|
|
1956
2205
|
case "CONTENT_LENGTH":
|
|
@@ -2291,14 +2540,29 @@ function serializeForRawDetection(body, visited = /* @__PURE__ */ new Set(), isR
|
|
|
2291
2540
|
});
|
|
2292
2541
|
return "{" + parts.join(",") + "}";
|
|
2293
2542
|
}
|
|
2543
|
+
var INHERITED_ACCESSORS = /* @__PURE__ */ new Set(["headers", "query"]);
|
|
2544
|
+
function requestField(req, key) {
|
|
2545
|
+
if (req === null || typeof req !== "object") return void 0;
|
|
2546
|
+
if (Object.hasOwn(req, key)) return req[key];
|
|
2547
|
+
if (!INHERITED_ACCESSORS.has(key)) return void 0;
|
|
2548
|
+
let holder = Object.getPrototypeOf(req);
|
|
2549
|
+
while (holder !== null && !Object.hasOwn(holder, key)) holder = Object.getPrototypeOf(holder);
|
|
2550
|
+
if (holder === null || holder === Object.prototype) return void 0;
|
|
2551
|
+
const descriptor = Object.getOwnPropertyDescriptor(holder, key);
|
|
2552
|
+
if (typeof descriptor?.get !== "function") return void 0;
|
|
2553
|
+
return req[key];
|
|
2554
|
+
}
|
|
2294
2555
|
function normalizeRequest(req, options = {}) {
|
|
2295
|
-
const
|
|
2556
|
+
const ownRaw = Object.hasOwn(req ?? {}, "_rawBody") && typeof req._rawBody === "string";
|
|
2557
|
+
const body = requestField(req, "body");
|
|
2558
|
+
const url = requestField(req, "url");
|
|
2559
|
+
const rawBody = ownRaw ? req._rawBody : serializeForRawDetection(body ?? null);
|
|
2296
2560
|
return {
|
|
2297
|
-
query: normalizeObject(req
|
|
2298
|
-
body: normalizeObject(
|
|
2299
|
-
headers: normalizeObject(req
|
|
2300
|
-
url: normalize(
|
|
2301
|
-
originalUrl: normalize(req
|
|
2561
|
+
query: normalizeObject(requestField(req, "query") || {}, options),
|
|
2562
|
+
body: normalizeObject(body || {}, options),
|
|
2563
|
+
headers: normalizeObject(requestField(req, "headers") || {}, options),
|
|
2564
|
+
url: normalize(url || "", options),
|
|
2565
|
+
originalUrl: normalize(requestField(req, "originalUrl") || url || "", options),
|
|
2302
2566
|
_rawBody: rawBody
|
|
2303
2567
|
};
|
|
2304
2568
|
}
|
|
@@ -2553,6 +2817,30 @@ function expandIPv6(host) {
|
|
|
2553
2817
|
}
|
|
2554
2818
|
return out;
|
|
2555
2819
|
}
|
|
2820
|
+
var JWT_CANDIDATE = /(?<![A-Za-z0-9_-])(?<![A-Za-z0-9_-]\.)eyJ[A-Za-z0-9_-]{2,2048}\.[A-Za-z0-9_-]{2,8192}\.[A-Za-z0-9_-]{0,2048}(?![A-Za-z0-9_-])(?!\.[A-Za-z0-9_-])/g;
|
|
2821
|
+
function base64UrlToBase64(segment) {
|
|
2822
|
+
const padded = segment.length % 4 === 0 ? segment : segment + "=".repeat(4 - segment.length % 4);
|
|
2823
|
+
return padded.replace(/-/g, "+").replace(/_/g, "/");
|
|
2824
|
+
}
|
|
2825
|
+
function jwtClaimSpans(text, claim2, value) {
|
|
2826
|
+
const found = /* @__PURE__ */ new Set();
|
|
2827
|
+
if (typeof text !== "string" || typeof claim2 !== "string" || claim2 === "") return [];
|
|
2828
|
+
const wanted = String(value);
|
|
2829
|
+
for (const [token] of text.matchAll(JWT_CANDIDATE)) {
|
|
2830
|
+
const parts = token.split(".");
|
|
2831
|
+
let payload;
|
|
2832
|
+
try {
|
|
2833
|
+
payload = JSON.parse(base64DecodeUtf8(base64UrlToBase64(parts[1])));
|
|
2834
|
+
} catch {
|
|
2835
|
+
continue;
|
|
2836
|
+
}
|
|
2837
|
+
if (payload === null || typeof payload !== "object" || Array.isArray(payload)) continue;
|
|
2838
|
+
if (!Object.prototype.hasOwnProperty.call(payload, claim2)) continue;
|
|
2839
|
+
const actual = payload[claim2];
|
|
2840
|
+
if (typeof actual === "string" && actual === wanted) found.add(token);
|
|
2841
|
+
}
|
|
2842
|
+
return [...found];
|
|
2843
|
+
}
|
|
2556
2844
|
var warnedScopes = /* @__PURE__ */ new Set();
|
|
2557
2845
|
function warnUnrecognisedScope(when) {
|
|
2558
2846
|
const key = Object.keys(when).sort().join(",");
|
|
@@ -2581,16 +2869,30 @@ function ruleAppliesTo(when, resolver) {
|
|
|
2581
2869
|
return true;
|
|
2582
2870
|
}
|
|
2583
2871
|
}
|
|
2872
|
+
function foldPathForScope(value) {
|
|
2873
|
+
const out = String(value).toLowerCase();
|
|
2874
|
+
return out.endsWith("/") ? out.slice(0, -1) : out;
|
|
2875
|
+
}
|
|
2876
|
+
var REGEX_SYNTAX = /[\\^$*+?()[\]{}|]/;
|
|
2877
|
+
var REGEX_FORM = /^\/(.+?)\/([gimsuy]*)$/s;
|
|
2878
|
+
function isRegexForm(pattern) {
|
|
2879
|
+
const parts = REGEX_FORM.exec(pattern);
|
|
2880
|
+
return parts !== null && REGEX_SYNTAX.test(parts[1]);
|
|
2881
|
+
}
|
|
2584
2882
|
function pathMatches(pattern, path7) {
|
|
2585
|
-
if (
|
|
2883
|
+
if (isRegexForm(pattern)) {
|
|
2586
2884
|
const re = safeRegExp(pattern);
|
|
2587
2885
|
return re ? re.test(path7) : false;
|
|
2588
2886
|
}
|
|
2589
|
-
|
|
2590
|
-
|
|
2591
|
-
|
|
2887
|
+
const target = foldPathForScope(path7);
|
|
2888
|
+
const folded = foldPathForScope(pattern);
|
|
2889
|
+
if (folded.includes("*")) {
|
|
2890
|
+
const trailing = folded.endsWith("/*");
|
|
2891
|
+
const body = (trailing ? folded.slice(0, -2) : folded).replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*/g, "[^?]*");
|
|
2892
|
+
const re = safeRegExp("/^" + body + (trailing ? "(?:/[^?]*)?" : "") + "$/");
|
|
2893
|
+
return re ? re.test(target) : false;
|
|
2592
2894
|
}
|
|
2593
|
-
return
|
|
2895
|
+
return target === folded;
|
|
2594
2896
|
}
|
|
2595
2897
|
function normalizeDefaultPort(host) {
|
|
2596
2898
|
return String(host).toLowerCase().replace(/:(?:80|443)$/, "");
|
|
@@ -2743,6 +3045,8 @@ function matchValue(type, value, matchVal, matchObj) {
|
|
|
2743
3045
|
const stripped = strValue.replace(/[ _\-,]/g, "");
|
|
2744
3046
|
return ctypeResult(strValue, /^[\w$-]*$/.test(stripped), matchVal);
|
|
2745
3047
|
}
|
|
3048
|
+
case "jwt_claim_equals":
|
|
3049
|
+
return jwtClaimSpans(strValue, matchObj?.claim, matchVal).length > 0;
|
|
2746
3050
|
case "array_key_value":
|
|
2747
3051
|
return arrayKeyValue(value, matchObj);
|
|
2748
3052
|
// file_contains (uploaded-file content scanning) is NOT WordPress-specific, but is
|
|
@@ -2820,7 +3124,12 @@ var RuleEngine = class {
|
|
|
2820
3124
|
return {
|
|
2821
3125
|
blocked: true,
|
|
2822
3126
|
rule,
|
|
2823
|
-
message: rule.message ?? `Blocked by Patchstack WAF rule: ${rule.title ?? rule.id}
|
|
3127
|
+
message: rule.message ?? `Blocked by Patchstack WAF rule: ${rule.title ?? rule.id}`,
|
|
3128
|
+
// The resolver this match was decided by, for whatever needs to report on it. A consumer
|
|
3129
|
+
// that re-read the request instead would be reading it a second time: a getter, a stream or
|
|
3130
|
+
// anything else that answers once can give a different value, and evidence that disagrees
|
|
3131
|
+
// with the match it belongs to is worse than none.
|
|
3132
|
+
resolver
|
|
2824
3133
|
};
|
|
2825
3134
|
}
|
|
2826
3135
|
} catch (err) {
|
|
@@ -2915,1325 +3224,1625 @@ var RuleEngine = class {
|
|
|
2915
3224
|
}
|
|
2916
3225
|
};
|
|
2917
3226
|
|
|
2918
|
-
// src/protect/
|
|
2919
|
-
|
|
2920
|
-
|
|
2921
|
-
|
|
2922
|
-
|
|
2923
|
-
|
|
2924
|
-
|
|
2925
|
-
|
|
2926
|
-
|
|
2927
|
-
|
|
2928
|
-
|
|
2929
|
-
|
|
2930
|
-
|
|
2931
|
-
|
|
2932
|
-
|
|
2933
|
-
|
|
2934
|
-
|
|
2935
|
-
|
|
2936
|
-
|
|
2937
|
-
|
|
2938
|
-
|
|
2939
|
-
|
|
2940
|
-
|
|
2941
|
-
|
|
2942
|
-
|
|
2943
|
-
|
|
3227
|
+
// src/protect/rules/contract.js
|
|
3228
|
+
var SOURCES = Object.freeze({
|
|
3229
|
+
raw: Object.freeze({ keyed: false }),
|
|
3230
|
+
all: Object.freeze({ keyed: false }),
|
|
3231
|
+
false: Object.freeze({ keyed: false }),
|
|
3232
|
+
rules: Object.freeze({ keyed: false, group: true }),
|
|
3233
|
+
get: Object.freeze({ keyed: true, keys: "any", wildcard: true }),
|
|
3234
|
+
post: Object.freeze({ keyed: true, keys: "any", wildcard: true }),
|
|
3235
|
+
request: Object.freeze({ keyed: true, keys: "any", wildcard: true }),
|
|
3236
|
+
cookie: Object.freeze({ keyed: true, keys: "any", wildcard: true }),
|
|
3237
|
+
// A bare `files.<name>` resolves to the filename; an optional trailing attribute selects another part
|
|
3238
|
+
// of the upload. Optional, not required — filename-scoped rules use the bare form.
|
|
3239
|
+
files: Object.freeze({
|
|
3240
|
+
keyed: true,
|
|
3241
|
+
keys: "any",
|
|
3242
|
+
wildcard: true,
|
|
3243
|
+
optional_key_suffixes: Object.freeze(["content", "filename", "type"])
|
|
3244
|
+
}),
|
|
3245
|
+
server: Object.freeze({
|
|
3246
|
+
keyed: true,
|
|
3247
|
+
keys: Object.freeze([
|
|
3248
|
+
"REQUEST_URI",
|
|
3249
|
+
"REQUEST_METHOD",
|
|
3250
|
+
"HTTP_USER_AGENT",
|
|
3251
|
+
"HTTP_REFERER",
|
|
3252
|
+
"HTTP_HOST",
|
|
3253
|
+
"REMOTE_ADDR",
|
|
3254
|
+
"ip",
|
|
3255
|
+
"CONTENT_TYPE",
|
|
3256
|
+
"CONTENT_LENGTH"
|
|
3257
|
+
]),
|
|
3258
|
+
key_prefixes: Object.freeze(["HTTP_"])
|
|
3259
|
+
}),
|
|
3260
|
+
response: Object.freeze({
|
|
3261
|
+
keyed: true,
|
|
3262
|
+
keys: Object.freeze(["status", "body", "headers"]),
|
|
3263
|
+
key_prefixes: Object.freeze(["header."])
|
|
3264
|
+
}),
|
|
3265
|
+
egress: Object.freeze({ keyed: true, keys: Object.freeze(["url", "host", "method"]) })
|
|
3266
|
+
});
|
|
3267
|
+
var MATCH_TYPES = Object.freeze([
|
|
3268
|
+
"array_in_array",
|
|
3269
|
+
"array_key_value",
|
|
3270
|
+
"contains",
|
|
3271
|
+
"cors_reflected",
|
|
3272
|
+
"cross_origin",
|
|
3273
|
+
"ctype_alnum",
|
|
3274
|
+
"ctype_digit",
|
|
3275
|
+
"ctype_special",
|
|
3276
|
+
"equals",
|
|
3277
|
+
"equals_strict",
|
|
3278
|
+
"hostname",
|
|
3279
|
+
"in_array",
|
|
3280
|
+
"inline_js_xss",
|
|
3281
|
+
"inline_xss",
|
|
3282
|
+
"internal_host",
|
|
3283
|
+
"is_numeric",
|
|
3284
|
+
"isset",
|
|
3285
|
+
"jwt_claim_equals",
|
|
3286
|
+
"less_than",
|
|
3287
|
+
"more_than",
|
|
3288
|
+
"not_contains",
|
|
3289
|
+
"not_in_array",
|
|
3290
|
+
"off_origin",
|
|
3291
|
+
"quotes",
|
|
3292
|
+
"regex",
|
|
3293
|
+
"stripos"
|
|
3294
|
+
]);
|
|
3295
|
+
var PARAMETERLESS_MATCH_TYPES = Object.freeze(["cross_origin", "off_origin", "cors_reflected"]);
|
|
3296
|
+
var OPERANDLESS_MATCH_TYPES = Object.freeze([
|
|
3297
|
+
"isset",
|
|
3298
|
+
"quotes",
|
|
3299
|
+
"is_numeric",
|
|
3300
|
+
"ctype_alnum",
|
|
3301
|
+
"ctype_digit",
|
|
3302
|
+
"ctype_special",
|
|
3303
|
+
"inline_xss",
|
|
3304
|
+
"inline_js_xss",
|
|
3305
|
+
"internal_host",
|
|
3306
|
+
"cross_origin",
|
|
3307
|
+
"off_origin",
|
|
3308
|
+
"cors_reflected"
|
|
3309
|
+
]);
|
|
3310
|
+
var OPERAND_SHAPES = Object.freeze([
|
|
3311
|
+
"positive-number",
|
|
3312
|
+
"boolean",
|
|
3313
|
+
"non-empty-string",
|
|
3314
|
+
"regex-literal",
|
|
3315
|
+
"string-or-string-list",
|
|
3316
|
+
"scalar",
|
|
3317
|
+
"string",
|
|
3318
|
+
"numeric",
|
|
3319
|
+
"scalar-or-non-empty-list",
|
|
3320
|
+
"non-empty-list",
|
|
3321
|
+
"object",
|
|
3322
|
+
"non-empty-object",
|
|
3323
|
+
"non-empty-string-list",
|
|
3324
|
+
"header-name-map",
|
|
3325
|
+
"header-name-list",
|
|
3326
|
+
"cookie-flags"
|
|
3327
|
+
]);
|
|
3328
|
+
var COOKIE_FLAGS = Object.freeze(["httpOnly", "secure", "sameSite"]);
|
|
3329
|
+
var SAME_SITE_VALUES = Object.freeze(["Strict", "Lax", "None"]);
|
|
3330
|
+
var HEADER_NAME = /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/;
|
|
3331
|
+
var REGEX_LITERAL = /^\/(.+?)\/([gimsuy]*)$/s;
|
|
3332
|
+
var MATCH_OPERANDS = Object.freeze({
|
|
3333
|
+
array_key_value: Object.freeze({
|
|
3334
|
+
required: Object.freeze(["key", "match"]),
|
|
3335
|
+
shapes: Object.freeze({ key: "string-or-string-list", match: "object" })
|
|
3336
|
+
}),
|
|
3337
|
+
// Decides on a JWT's DECODED payload rather than its shape, and yields the matching token spans so
|
|
3338
|
+
// `redact` masks only those tokens. `claim` names an own, top-level claim; `value` is compared with
|
|
3339
|
+
// exact string equality. Anything not positively identified — undecodable, not JSON, not an object,
|
|
3340
|
+
// oversized, claim missing, claim not a string, or a different value — is a no-match.
|
|
3341
|
+
jwt_claim_equals: Object.freeze({
|
|
3342
|
+
required: Object.freeze(["claim", "value"]),
|
|
3343
|
+
shapes: Object.freeze({ claim: "non-empty-string", value: "non-empty-string" })
|
|
3344
|
+
}),
|
|
3345
|
+
regex: Object.freeze({ required: Object.freeze(["value"]), shapes: Object.freeze({ value: "regex-literal" }) }),
|
|
3346
|
+
contains: Object.freeze({ required: Object.freeze(["value"]), shapes: Object.freeze({ value: "non-empty-string" }) }),
|
|
3347
|
+
stripos: Object.freeze({ required: Object.freeze(["value"]), shapes: Object.freeze({ value: "non-empty-string" }) }),
|
|
3348
|
+
not_contains: Object.freeze({ required: Object.freeze(["value"]), shapes: Object.freeze({ value: "non-empty-string" }) }),
|
|
3349
|
+
// A hostname is compared to `new URL(value).hostname`, so an empty one only ever equals the empty
|
|
3350
|
+
// hostname of an opaque-scheme URL (`file:`, `mailto:`, `data:`) — never a host, and a false positive
|
|
3351
|
+
// on those. The other three are the list-membership family: an empty list makes `in_array` and
|
|
3352
|
+
// `array_in_array` inert and `not_in_array` true for every value.
|
|
3353
|
+
hostname: Object.freeze({ required: Object.freeze(["value"]), shapes: Object.freeze({ value: "non-empty-string" }) }),
|
|
3354
|
+
in_array: Object.freeze({ required: Object.freeze(["value"]), shapes: Object.freeze({ value: "scalar-or-non-empty-list" }) }),
|
|
3355
|
+
not_in_array: Object.freeze({ required: Object.freeze(["value"]), shapes: Object.freeze({ value: "scalar-or-non-empty-list" }) }),
|
|
3356
|
+
array_in_array: Object.freeze({ required: Object.freeze(["value"]), shapes: Object.freeze({ value: "non-empty-list" }) }),
|
|
3357
|
+
// The comparison family. `equals` coerces, so any scalar can match. `equals_strict` does not, and its
|
|
3358
|
+
// subject is always a string by the time it is compared — so a number operand is unconditionally
|
|
3359
|
+
// inert, which is the trap the engine's own note about `'1' !== 1` describes.
|
|
3360
|
+
equals: Object.freeze({ required: Object.freeze(["value"]), shapes: Object.freeze({ value: "scalar" }) }),
|
|
3361
|
+
equals_strict: Object.freeze({ required: Object.freeze(["value"]), shapes: Object.freeze({ value: "string" }) }),
|
|
3362
|
+
less_than: Object.freeze({ required: Object.freeze(["value"]), shapes: Object.freeze({ value: "numeric" }) }),
|
|
3363
|
+
more_than: Object.freeze({ required: Object.freeze(["value"]), shapes: Object.freeze({ value: "numeric" }) })
|
|
3364
|
+
});
|
|
3365
|
+
function operandShapeProblem(shape, value) {
|
|
3366
|
+
const isFilledString = (v) => typeof v === "string" && v.trim() !== "";
|
|
3367
|
+
const isScalar = (v) => typeof v === "string" || typeof v === "number" || typeof v === "boolean";
|
|
3368
|
+
const isHeaderName = (v) => typeof v === "string" && HEADER_NAME.test(v);
|
|
3369
|
+
switch (shape) {
|
|
3370
|
+
case "positive-number":
|
|
3371
|
+
return typeof value === "number" && Number.isFinite(value) && value > 0 ? null : "must be a finite positive number";
|
|
3372
|
+
case "boolean":
|
|
3373
|
+
return typeof value === "boolean" ? null : "must be true or false";
|
|
3374
|
+
case "non-empty-string":
|
|
3375
|
+
return isFilledString(value) ? null : "must be a non-empty string";
|
|
3376
|
+
case "regex-literal":
|
|
3377
|
+
if (!isFilledString(value)) return "must be a non-empty string";
|
|
3378
|
+
return REGEX_LITERAL.test(value) ? null : "must be a delimited pattern, as in /pattern/i";
|
|
3379
|
+
case "string-or-string-list":
|
|
3380
|
+
if (isFilledString(value)) return null;
|
|
3381
|
+
if (Array.isArray(value) && value.length > 0 && value.every(isFilledString)) return null;
|
|
3382
|
+
return "must be a non-empty string, or a non-empty list of them";
|
|
3383
|
+
case "scalar":
|
|
3384
|
+
return isScalar(value) ? null : "must be a string, number or boolean";
|
|
3385
|
+
case "string":
|
|
3386
|
+
return typeof value === "string" ? null : "must be a string (this comparison is type-strict)";
|
|
3387
|
+
case "numeric":
|
|
3388
|
+
return isScalar(value) && Number.isFinite(Number(value)) ? null : "must be a number";
|
|
3389
|
+
case "scalar-or-non-empty-list":
|
|
3390
|
+
if (isScalar(value)) return null;
|
|
3391
|
+
if (Array.isArray(value) && value.length > 0 && value.every(isScalar)) return null;
|
|
3392
|
+
return "must be a scalar, or a non-empty list of them";
|
|
3393
|
+
case "non-empty-list":
|
|
3394
|
+
return Array.isArray(value) && value.length > 0 && value.every(isScalar) ? null : "must be a non-empty list of scalars";
|
|
3395
|
+
case "header-name-map": {
|
|
3396
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) return "must be an object";
|
|
3397
|
+
const names = Object.keys(value);
|
|
3398
|
+
if (names.length === 0) return "must be an object with at least one entry";
|
|
3399
|
+
const bad = names.find((name) => !isHeaderName(name));
|
|
3400
|
+
return bad === void 0 ? null : `names an invalid HTTP header, ${JSON.stringify(bad)}`;
|
|
2944
3401
|
}
|
|
2945
|
-
|
|
2946
|
-
|
|
2947
|
-
|
|
2948
|
-
|
|
2949
|
-
return this.#cache;
|
|
3402
|
+
case "header-name-list": {
|
|
3403
|
+
if (!Array.isArray(value) || value.length === 0) return "must be a non-empty list of HTTP header names";
|
|
3404
|
+
const bad = value.find((name) => !isHeaderName(name));
|
|
3405
|
+
return bad === void 0 ? null : `names an invalid HTTP header, ${JSON.stringify(bad)}`;
|
|
2950
3406
|
}
|
|
2951
|
-
|
|
2952
|
-
|
|
2953
|
-
const
|
|
2954
|
-
|
|
2955
|
-
|
|
2956
|
-
|
|
2957
|
-
|
|
2958
|
-
|
|
2959
|
-
headers["X-Patchstack-Detections"] = "enabled";
|
|
3407
|
+
case "cookie-flags": {
|
|
3408
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) return "must be an object";
|
|
3409
|
+
const unknown = Object.keys(value).filter((key) => !COOKIE_FLAGS.includes(key));
|
|
3410
|
+
if (unknown.length > 0) return `has no flag named ${unknown.map((k) => JSON.stringify(k)).join(", ")}`;
|
|
3411
|
+
for (const flag of ["httpOnly", "secure"]) {
|
|
3412
|
+
if (value[flag] !== void 0 && typeof value[flag] !== "boolean") {
|
|
3413
|
+
return `flag "${flag}" must be true or false`;
|
|
3414
|
+
}
|
|
2960
3415
|
}
|
|
2961
|
-
|
|
2962
|
-
const
|
|
2963
|
-
if (
|
|
2964
|
-
|
|
2965
|
-
|
|
2966
|
-
|
|
2967
|
-
if (!response.ok) {
|
|
2968
|
-
return { success: false, error: `API returned ${response.status}`, firewall: [], whitelists: [], whitelist_keys: {} };
|
|
3416
|
+
const sameSite = value.sameSite;
|
|
3417
|
+
const sameSiteOff = sameSite === false;
|
|
3418
|
+
if (sameSite !== void 0 && !sameSiteOff) {
|
|
3419
|
+
if (typeof sameSite !== "string" || !SAME_SITE_VALUES.some((v) => v.toLowerCase() === sameSite.toLowerCase())) {
|
|
3420
|
+
return `flag "sameSite" must be false, or one of ${SAME_SITE_VALUES.join(", ")}`;
|
|
3421
|
+
}
|
|
2969
3422
|
}
|
|
2970
|
-
|
|
2971
|
-
|
|
2972
|
-
return { success: false, error: "unexpected response shape (no firewall array)", firewall: [], whitelists: [], whitelist_keys: {} };
|
|
3423
|
+
if (value.httpOnly === false && value.secure === false && sameSiteOff) {
|
|
3424
|
+
return "turns off every flag, so it would harden nothing";
|
|
2973
3425
|
}
|
|
2974
|
-
|
|
2975
|
-
success: true,
|
|
2976
|
-
notModified: false,
|
|
2977
|
-
etag: response.headers?.get?.("etag") ?? null,
|
|
2978
|
-
firewall: data.firewall,
|
|
2979
|
-
whitelists: Array.isArray(data.whitelists) ? data.whitelists : [],
|
|
2980
|
-
whitelist_keys: data.whitelist_keys ?? {},
|
|
2981
|
-
...enforcementField(data)
|
|
2982
|
-
};
|
|
2983
|
-
this.#cache = result;
|
|
2984
|
-
this.#etag = result.etag;
|
|
2985
|
-
this.#touch(now);
|
|
2986
|
-
return result;
|
|
2987
|
-
} catch (err) {
|
|
2988
|
-
return {
|
|
2989
|
-
success: false,
|
|
2990
|
-
error: err.name === "TimeoutError" ? "Request timed out" : err.message,
|
|
2991
|
-
firewall: [],
|
|
2992
|
-
whitelists: [],
|
|
2993
|
-
whitelist_keys: {}
|
|
2994
|
-
};
|
|
3426
|
+
return null;
|
|
2995
3427
|
}
|
|
3428
|
+
case "object":
|
|
3429
|
+
return value !== null && typeof value === "object" && !Array.isArray(value) ? null : "must be an object";
|
|
3430
|
+
case "non-empty-object":
|
|
3431
|
+
return value !== null && typeof value === "object" && !Array.isArray(value) && Object.keys(value).length > 0 ? null : "must be an object with at least one entry";
|
|
3432
|
+
case "non-empty-string-list":
|
|
3433
|
+
return Array.isArray(value) && value.length > 0 && value.every(isFilledString) ? null : "must be a non-empty list of non-empty strings";
|
|
3434
|
+
default:
|
|
3435
|
+
return null;
|
|
2996
3436
|
}
|
|
2997
|
-
|
|
2998
|
-
|
|
2999
|
-
|
|
3000
|
-
|
|
3001
|
-
|
|
3002
|
-
|
|
3003
|
-
|
|
3004
|
-
|
|
3437
|
+
}
|
|
3438
|
+
var MUTATIONS = Object.freeze([
|
|
3439
|
+
"base64_decode",
|
|
3440
|
+
"getArrayValues",
|
|
3441
|
+
"htmlentitydecode",
|
|
3442
|
+
"intval",
|
|
3443
|
+
"json_decode",
|
|
3444
|
+
"json_encode",
|
|
3445
|
+
"urldecode"
|
|
3446
|
+
]);
|
|
3447
|
+
var PHASES = Object.freeze(["request", "response", "egress"]);
|
|
3448
|
+
var ACTIONS = Object.freeze([
|
|
3449
|
+
"block",
|
|
3450
|
+
"redact",
|
|
3451
|
+
"encode",
|
|
3452
|
+
"set-header",
|
|
3453
|
+
"remove-header",
|
|
3454
|
+
"harden-cookie"
|
|
3455
|
+
]);
|
|
3456
|
+
var RULE_PROPERTY_SHAPES = Object.freeze({
|
|
3457
|
+
max_bytes: "positive-number",
|
|
3458
|
+
bypass_limit: "boolean",
|
|
3459
|
+
ensure: "boolean",
|
|
3460
|
+
prefilter: "non-empty-string-list"
|
|
3461
|
+
});
|
|
3462
|
+
var ENFORCEMENT_VALUES = Object.freeze(["dry-run"]);
|
|
3463
|
+
var WHEN_KEYS = Object.freeze(["path", "method"]);
|
|
3464
|
+
var RULE_PROPERTIES = Object.freeze([
|
|
3465
|
+
"id",
|
|
3466
|
+
"rule_id",
|
|
3467
|
+
"title",
|
|
3468
|
+
"category",
|
|
3469
|
+
"phase",
|
|
3470
|
+
"action",
|
|
3471
|
+
"rule_v2",
|
|
3472
|
+
"when",
|
|
3473
|
+
"message",
|
|
3474
|
+
"enforcement",
|
|
3475
|
+
"source_revision",
|
|
3476
|
+
"prefilter",
|
|
3477
|
+
"max_bytes",
|
|
3478
|
+
"bypass_limit",
|
|
3479
|
+
"set_headers",
|
|
3480
|
+
"remove_headers",
|
|
3481
|
+
"cookie_flags",
|
|
3482
|
+
"ensure",
|
|
3483
|
+
"capture"
|
|
3484
|
+
]);
|
|
3485
|
+
var CAPTURE_VERSION = 1;
|
|
3486
|
+
var CAPTURE_KEYS = Object.freeze(["version", "raw_chars"]);
|
|
3487
|
+
var CAPTURE_RAW_CHARS_MAX = 512;
|
|
3488
|
+
var NULL_EXEMPT_PROPERTIES = Object.freeze(["capture"]);
|
|
3489
|
+
function captureProblem(capture) {
|
|
3490
|
+
if (capture === void 0 || capture === null) return null;
|
|
3491
|
+
if (typeof capture !== "object" || Array.isArray(capture)) return "capture must be an object";
|
|
3492
|
+
for (const key of Object.keys(capture)) {
|
|
3493
|
+
if (!CAPTURE_KEYS.includes(key)) return `capture has no "${key}" \u2014 it takes ${CAPTURE_KEYS.join(", ")}`;
|
|
3494
|
+
}
|
|
3495
|
+
if (!Object.hasOwn(capture, "version")) return "capture must name the version it was written against";
|
|
3496
|
+
if (capture.version !== CAPTURE_VERSION) {
|
|
3497
|
+
return `capture version ${String(capture.version)} is not one this guard understands`;
|
|
3498
|
+
}
|
|
3499
|
+
if (!Object.hasOwn(capture, "raw_chars")) return "capture must say what it is opting into";
|
|
3500
|
+
const chars = capture.raw_chars;
|
|
3501
|
+
if (typeof chars !== "number" || !Number.isInteger(chars) || chars <= 0) {
|
|
3502
|
+
return "capture.raw_chars must be a positive whole number of characters";
|
|
3005
3503
|
}
|
|
3006
|
-
|
|
3007
|
-
function enforcementField(data) {
|
|
3008
|
-
if (!data || typeof data !== "object") return {};
|
|
3009
|
-
const v = data.enforcement ?? data.mode;
|
|
3010
|
-
return v === "block" || v === "dry-run" ? { enforcement: v } : {};
|
|
3504
|
+
return null;
|
|
3011
3505
|
}
|
|
3012
|
-
|
|
3013
|
-
|
|
3014
|
-
|
|
3015
|
-
|
|
3016
|
-
|
|
3017
|
-
|
|
3018
|
-
|
|
3019
|
-
|
|
3020
|
-
|
|
3021
|
-
|
|
3022
|
-
|
|
3023
|
-
|
|
3024
|
-
|
|
3025
|
-
|
|
3026
|
-
|
|
3027
|
-
|
|
3028
|
-
|
|
3029
|
-
|
|
3506
|
+
var ACTION_PROPERTIES = Object.freeze({
|
|
3507
|
+
block: Object.freeze({ required: Object.freeze([]), defaulted: Object.freeze([]), phases: Object.freeze(["request", "response", "egress"]) }),
|
|
3508
|
+
redact: Object.freeze({ required: Object.freeze([]), defaulted: Object.freeze([]), phases: Object.freeze(["response"]) }),
|
|
3509
|
+
encode: Object.freeze({ required: Object.freeze([]), defaulted: Object.freeze([]), phases: Object.freeze(["response"]) }),
|
|
3510
|
+
"set-header": Object.freeze({
|
|
3511
|
+
required: Object.freeze(["set_headers"]),
|
|
3512
|
+
defaulted: Object.freeze(["ensure"]),
|
|
3513
|
+
phases: Object.freeze(["response"]),
|
|
3514
|
+
shapes: Object.freeze({ set_headers: "header-name-map" })
|
|
3515
|
+
}),
|
|
3516
|
+
"remove-header": Object.freeze({
|
|
3517
|
+
required: Object.freeze(["remove_headers"]),
|
|
3518
|
+
defaulted: Object.freeze([]),
|
|
3519
|
+
phases: Object.freeze(["response"]),
|
|
3520
|
+
shapes: Object.freeze({ remove_headers: "header-name-list" })
|
|
3521
|
+
}),
|
|
3522
|
+
"harden-cookie": Object.freeze({
|
|
3523
|
+
required: Object.freeze([]),
|
|
3524
|
+
defaulted: Object.freeze(["cookie_flags"]),
|
|
3525
|
+
phases: Object.freeze(["response"]),
|
|
3526
|
+
shapes: Object.freeze({ cookie_flags: "cookie-flags" })
|
|
3527
|
+
})
|
|
3528
|
+
});
|
|
3529
|
+
var LIMITS = Object.freeze({
|
|
3530
|
+
maxRules: 5e3,
|
|
3531
|
+
maxWhitelists: 2e3,
|
|
3532
|
+
maxConditionsPerRule: 250,
|
|
3533
|
+
maxNestingDepth: 12,
|
|
3534
|
+
maxRegexLength: 1e3,
|
|
3535
|
+
maxValueLength: 8192
|
|
3536
|
+
});
|
|
3537
|
+
var GROUP_PARAMETER = "rules";
|
|
3538
|
+
function isGroup(condition) {
|
|
3539
|
+
return condition?.parameter === GROUP_PARAMETER && Array.isArray(condition?.rules);
|
|
3540
|
+
}
|
|
3541
|
+
function conditionShapeProblem(condition) {
|
|
3542
|
+
if (!condition || typeof condition !== "object") return "condition is not an object";
|
|
3543
|
+
const named = condition.parameter === GROUP_PARAMETER;
|
|
3544
|
+
const nested = Array.isArray(condition.rules);
|
|
3545
|
+
if (named && !nested) {
|
|
3546
|
+
return `parameter "${GROUP_PARAMETER}" is a group and needs a "rules" array`;
|
|
3030
3547
|
}
|
|
3031
|
-
|
|
3032
|
-
|
|
3033
|
-
rawBody = await readCappedText(request, maxBodyBytes);
|
|
3548
|
+
if (nested && !named) {
|
|
3549
|
+
return `condition carries nested rules but its parameter is ${JSON.stringify(condition.parameter)}; a group must be {"parameter":"${GROUP_PARAMETER}"}`;
|
|
3034
3550
|
}
|
|
3035
|
-
|
|
3036
|
-
|
|
3037
|
-
|
|
3038
|
-
|
|
3039
|
-
const parsed = parseBody(rawBody, contentType);
|
|
3040
|
-
body = parsed.body;
|
|
3041
|
-
files = parsed.files;
|
|
3551
|
+
if (named && nested) {
|
|
3552
|
+
if (condition.match !== void 0) return "a group carries no match of its own; the engine ignores it";
|
|
3553
|
+
if (condition.mutations !== void 0) return "a group carries no mutations of its own; the engine ignores them";
|
|
3554
|
+
if (condition.rules.length === 0) return "group has no conditions (would never match)";
|
|
3042
3555
|
}
|
|
3043
|
-
|
|
3044
|
-
const forwarded = headers["cf-connecting-ip"] || headers["x-forwarded-for"] || headers["x-real-ip"] || "";
|
|
3045
|
-
return {
|
|
3046
|
-
method,
|
|
3047
|
-
url: uri,
|
|
3048
|
-
originalUrl: uri,
|
|
3049
|
-
query,
|
|
3050
|
-
body,
|
|
3051
|
-
files,
|
|
3052
|
-
headers,
|
|
3053
|
-
ip: forwarded.split(",")[0].trim(),
|
|
3054
|
-
cookies: parseCookies(headers.cookie),
|
|
3055
|
-
// Verbatim body text: preserves literal keys (e.g. `__proto__`) that JSON.stringify
|
|
3056
|
-
// drops, so prototype-pollution rules on `raw` are robust.
|
|
3057
|
-
_rawBody: rawBody
|
|
3058
|
-
};
|
|
3556
|
+
return null;
|
|
3059
3557
|
}
|
|
3060
|
-
|
|
3061
|
-
|
|
3062
|
-
|
|
3063
|
-
|
|
3064
|
-
|
|
3065
|
-
|
|
3066
|
-
|
|
3067
|
-
|
|
3068
|
-
if (body && typeof body.getReader === "function") {
|
|
3069
|
-
const reader = body.getReader();
|
|
3070
|
-
const chunks = [];
|
|
3071
|
-
let buffered = 0;
|
|
3072
|
-
try {
|
|
3073
|
-
for (; ; ) {
|
|
3074
|
-
const { done, value } = await reader.read();
|
|
3075
|
-
if (done) break;
|
|
3076
|
-
if (!value || buffered >= max) continue;
|
|
3077
|
-
const take = Math.min(value.byteLength, max - buffered);
|
|
3078
|
-
chunks.push(take === value.byteLength ? value : value.subarray(0, take));
|
|
3079
|
-
buffered += take;
|
|
3080
|
-
}
|
|
3081
|
-
} catch {
|
|
3082
|
-
return "";
|
|
3083
|
-
}
|
|
3084
|
-
try {
|
|
3085
|
-
return new TextDecoder().decode(concatChunks(chunks, buffered));
|
|
3086
|
-
} catch {
|
|
3087
|
-
return "";
|
|
3558
|
+
function parameterProblem(parameter) {
|
|
3559
|
+
if (parameter === void 0 || parameter === null) return null;
|
|
3560
|
+
if (Array.isArray(parameter)) {
|
|
3561
|
+
if (parameter.length === 0) return "parameter list is empty";
|
|
3562
|
+
for (const member of parameter) {
|
|
3563
|
+
if (Array.isArray(member)) return "a parameter list holds parameters, not more lists";
|
|
3564
|
+
const problem = parameterProblem(member);
|
|
3565
|
+
if (problem !== null) return problem;
|
|
3088
3566
|
}
|
|
3567
|
+
return null;
|
|
3089
3568
|
}
|
|
3090
|
-
|
|
3091
|
-
|
|
3092
|
-
return
|
|
3093
|
-
} catch {
|
|
3094
|
-
return "";
|
|
3569
|
+
if (typeof parameter !== "string" || parameter === "") return "parameter must be a non-empty string";
|
|
3570
|
+
if (parameter === GROUP_PARAMETER) {
|
|
3571
|
+
return `"${GROUP_PARAMETER}" is the group source and is only valid as a whole group, not as a parameter`;
|
|
3095
3572
|
}
|
|
3096
|
-
|
|
3097
|
-
|
|
3098
|
-
const
|
|
3099
|
-
|
|
3100
|
-
|
|
3101
|
-
|
|
3102
|
-
offset += c.byteLength;
|
|
3573
|
+
const dot = parameter.indexOf(".");
|
|
3574
|
+
const name = dot === -1 ? parameter : parameter.slice(0, dot);
|
|
3575
|
+
const source = SOURCES[name];
|
|
3576
|
+
if (source === void 0) return `unknown parameter source "${name}"`;
|
|
3577
|
+
if (dot === -1) {
|
|
3578
|
+
return source.keyed === true ? `source "${name}" needs a key, e.g. "${name}.fieldname"` : null;
|
|
3103
3579
|
}
|
|
3104
|
-
return
|
|
3580
|
+
if (source.keyed !== true) return `source "${name}" takes no key`;
|
|
3581
|
+
const key = parameter.slice(dot + 1);
|
|
3582
|
+
if (key === "") return `source "${name}" needs a key after the dot`;
|
|
3583
|
+
return keyProblem(name, source, key);
|
|
3105
3584
|
}
|
|
3106
|
-
function
|
|
3107
|
-
|
|
3108
|
-
|
|
3109
|
-
const isForm = ct.includes("application/x-www-form-urlencoded");
|
|
3110
|
-
const isMultipart = ct.includes("multipart/form-data");
|
|
3111
|
-
const isAmbiguous = ct === "" || ct.startsWith("text/plain") || ct.includes("csp-report") || ct.includes("/json");
|
|
3112
|
-
if (isMultipart) {
|
|
3113
|
-
const boundary = /boundary=("?)([^";]+)\1/i.exec(contentType)?.[2];
|
|
3114
|
-
if (boundary) return parseMultipart(rawBody, boundary);
|
|
3115
|
-
return { body: {}, files: void 0 };
|
|
3585
|
+
function keyProblem(name, source, key) {
|
|
3586
|
+
if (key.endsWith("*") && source.wildcard !== true) {
|
|
3587
|
+
return `"${name}" does not fan out over a wildcard key`;
|
|
3116
3588
|
}
|
|
3117
|
-
|
|
3118
|
-
|
|
3119
|
-
|
|
3120
|
-
|
|
3589
|
+
const bare = key.endsWith("*") ? key.slice(0, -1) : key;
|
|
3590
|
+
if (source.keys === "any") return null;
|
|
3591
|
+
if (Array.isArray(source.keys) && source.keys.includes(bare)) return null;
|
|
3592
|
+
for (const prefix of source.key_prefixes ?? []) {
|
|
3593
|
+
if (bare.startsWith(prefix) && bare.length > prefix.length) return null;
|
|
3121
3594
|
}
|
|
3122
|
-
|
|
3123
|
-
|
|
3124
|
-
|
|
3125
|
-
|
|
3126
|
-
|
|
3595
|
+
const accepted = [
|
|
3596
|
+
...Array.isArray(source.keys) ? source.keys : [],
|
|
3597
|
+
...(source.key_prefixes ?? []).map((p) => `${p}\u2026`)
|
|
3598
|
+
].join(", ");
|
|
3599
|
+
return `"${name}" has no key "${key}" (expected one of: ${accepted})`;
|
|
3600
|
+
}
|
|
3601
|
+
function matchProblem(match, parameter) {
|
|
3602
|
+
if (!match || typeof match !== "object") return "condition has no match object";
|
|
3603
|
+
if (typeof match.type !== "string" || match.type === "") return "match.type must be a non-empty string";
|
|
3604
|
+
if (!MATCH_TYPES.includes(match.type)) return `unknown match type "${match.type}"`;
|
|
3605
|
+
const parameterless = PARAMETERLESS_MATCH_TYPES.includes(match.type);
|
|
3606
|
+
if (!parameterless && (parameter === void 0 || parameter === null)) {
|
|
3607
|
+
return `match type "${match.type}" needs a parameter`;
|
|
3608
|
+
}
|
|
3609
|
+
if (parameterless && parameter !== void 0 && parameter !== null) {
|
|
3610
|
+
return `match type "${match.type}" reads the whole request and takes no parameter`;
|
|
3611
|
+
}
|
|
3612
|
+
const operands = MATCH_OPERANDS[match.type];
|
|
3613
|
+
if (operands) {
|
|
3614
|
+
for (const required of operands.required) {
|
|
3615
|
+
if (match[required] === void 0 || match[required] === null) {
|
|
3616
|
+
return `match type "${match.type}" needs "${required}"`;
|
|
3617
|
+
}
|
|
3618
|
+
}
|
|
3619
|
+
for (const [operand, shape] of Object.entries(operands.shapes ?? {})) {
|
|
3620
|
+
if (match[operand] === void 0) continue;
|
|
3621
|
+
const problem = operandShapeProblem(shape, match[operand]);
|
|
3622
|
+
if (problem) return `match type "${match.type}" operand "${operand}" ${problem}`;
|
|
3127
3623
|
}
|
|
3624
|
+
} else if (!OPERANDLESS_MATCH_TYPES.includes(match.type) && match.value === void 0) {
|
|
3625
|
+
return `match type "${match.type}" needs a value`;
|
|
3128
3626
|
}
|
|
3129
|
-
return
|
|
3627
|
+
return null;
|
|
3130
3628
|
}
|
|
3131
|
-
function
|
|
3132
|
-
|
|
3133
|
-
|
|
3134
|
-
for (const
|
|
3135
|
-
|
|
3136
|
-
|
|
3137
|
-
const rawHeaders = part.slice(0, sep.index);
|
|
3138
|
-
const disposition = /content-disposition:[^\r\n]*/i.exec(rawHeaders)?.[0];
|
|
3139
|
-
if (!disposition) continue;
|
|
3140
|
-
const name = /name="([^"]*)"/i.exec(disposition)?.[1];
|
|
3141
|
-
if (name == null) continue;
|
|
3142
|
-
const content = part.slice(sep.index + sep[0].length).replace(/\r?\n$/, "");
|
|
3143
|
-
const filename = /filename="([^"]*)"/i.exec(disposition)?.[1];
|
|
3144
|
-
if (filename !== void 0) {
|
|
3145
|
-
const partType = /content-type:\s*([^\r\n;]+)/i.exec(rawHeaders)?.[1]?.trim() || "";
|
|
3146
|
-
const file = { filename, type: partType, content };
|
|
3147
|
-
files[name] = name in files ? [].concat(files[name], file) : file;
|
|
3148
|
-
} else {
|
|
3149
|
-
body[name] = name in body ? [].concat(body[name], content) : content;
|
|
3629
|
+
function mutationsProblem(mutations) {
|
|
3630
|
+
if (mutations === void 0 || mutations === null) return null;
|
|
3631
|
+
if (!Array.isArray(mutations)) return "mutations must be an array";
|
|
3632
|
+
for (const mutation of mutations) {
|
|
3633
|
+
if (typeof mutation !== "string" || !MUTATIONS.includes(mutation)) {
|
|
3634
|
+
return `unknown mutation "${String(mutation)}" \u2014 applied as a no-op, so the condition would test the untransformed value`;
|
|
3150
3635
|
}
|
|
3151
3636
|
}
|
|
3152
|
-
return
|
|
3637
|
+
return null;
|
|
3153
3638
|
}
|
|
3154
|
-
function
|
|
3155
|
-
|
|
3156
|
-
if (
|
|
3157
|
-
|
|
3639
|
+
function whenProblem(when) {
|
|
3640
|
+
if (when === void 0 || when === null) return null;
|
|
3641
|
+
if (typeof when !== "object" || Array.isArray(when)) return "when must be an object";
|
|
3642
|
+
const keys = Object.keys(when);
|
|
3643
|
+
if (keys.length === 0) return "when is empty \u2014 remove it, or name a path or method";
|
|
3644
|
+
const unknown = keys.filter((key) => !WHEN_KEYS.includes(key));
|
|
3645
|
+
if (unknown.length > 0) {
|
|
3646
|
+
return `when names no supported key (${unknown.join(", ")}) \u2014 the engine understands ${WHEN_KEYS.join(" and ")}, and an unrecognised scope is ignored, so the rule would apply to every request`;
|
|
3158
3647
|
}
|
|
3159
|
-
|
|
3160
|
-
|
|
3161
|
-
|
|
3162
|
-
|
|
3648
|
+
if ("path" in when && !isNonEmptyString(when.path)) {
|
|
3649
|
+
return "when.path must be a non-empty string";
|
|
3650
|
+
}
|
|
3651
|
+
if ("method" in when) {
|
|
3652
|
+
const methods = Array.isArray(when.method) ? when.method : [when.method];
|
|
3653
|
+
if (methods.length === 0) return "when.method is an empty list, so the rule would match no method";
|
|
3654
|
+
if (!methods.every(isNonEmptyString)) return "when.method must be a non-empty string, or a list of them";
|
|
3655
|
+
}
|
|
3656
|
+
return null;
|
|
3657
|
+
}
|
|
3658
|
+
function isNonEmptyString(value) {
|
|
3659
|
+
return typeof value === "string" && value.trim() !== "";
|
|
3660
|
+
}
|
|
3661
|
+
function nullPropertyProblem(rule) {
|
|
3662
|
+
if (!rule || typeof rule !== "object") return null;
|
|
3663
|
+
for (const property of RULE_PROPERTIES) {
|
|
3664
|
+
if (NULL_EXEMPT_PROPERTIES.includes(property)) continue;
|
|
3665
|
+
if (property in rule && rule[property] === null) {
|
|
3666
|
+
return `"${property}" is present but null; omit it to mean the default, since a null is a value that was meant to be something and is not`;
|
|
3163
3667
|
}
|
|
3164
|
-
cookies[pair.slice(0, idx).trim()] = pair.slice(idx + 1).trim();
|
|
3165
3668
|
}
|
|
3166
|
-
return
|
|
3669
|
+
return null;
|
|
3167
3670
|
}
|
|
3168
|
-
|
|
3169
|
-
|
|
3170
|
-
|
|
3171
|
-
|
|
3172
|
-
|
|
3173
|
-
|
|
3174
|
-
headers[key.toLowerCase()] = Array.isArray(value) ? value.join(", ") : value;
|
|
3671
|
+
function rulePropertyProblem(rule) {
|
|
3672
|
+
if (!rule || typeof rule !== "object") return null;
|
|
3673
|
+
for (const [property, shape] of Object.entries(RULE_PROPERTY_SHAPES)) {
|
|
3674
|
+
if (rule[property] === void 0 || rule[property] === null) continue;
|
|
3675
|
+
const problem = operandShapeProblem(shape, rule[property]);
|
|
3676
|
+
if (problem) return `"${property}" ${problem}`;
|
|
3175
3677
|
}
|
|
3176
|
-
|
|
3177
|
-
|
|
3178
|
-
|
|
3179
|
-
|
|
3180
|
-
|
|
3181
|
-
|
|
3182
|
-
|
|
3183
|
-
|
|
3184
|
-
|
|
3678
|
+
return null;
|
|
3679
|
+
}
|
|
3680
|
+
function actionProblem(action, rule) {
|
|
3681
|
+
if (action === void 0 || action === null) return null;
|
|
3682
|
+
if (!ACTIONS.includes(action)) return `unknown action "${action}"`;
|
|
3683
|
+
const spec = ACTION_PROPERTIES[action];
|
|
3684
|
+
for (const required of spec?.required ?? []) {
|
|
3685
|
+
if (rule?.[required] === void 0 || rule?.[required] === null) {
|
|
3686
|
+
return `action "${action}" needs "${required}"`;
|
|
3185
3687
|
}
|
|
3186
3688
|
}
|
|
3187
|
-
const
|
|
3188
|
-
|
|
3189
|
-
|
|
3190
|
-
|
|
3191
|
-
|
|
3192
|
-
|
|
3689
|
+
for (const [property, shape] of Object.entries(spec?.shapes ?? {})) {
|
|
3690
|
+
if (rule?.[property] === void 0 || rule?.[property] === null) continue;
|
|
3691
|
+
const problem = operandShapeProblem(shape, rule[property]);
|
|
3692
|
+
if (problem) return `action "${action}" property "${property}" ${problem}`;
|
|
3693
|
+
}
|
|
3694
|
+
const phase = rule?.phase ?? "request";
|
|
3695
|
+
if (spec !== void 0 && !spec.phases.includes(phase)) {
|
|
3696
|
+
return `action "${action}" is carried out on the ${spec.phases.join("/")} phase; on "${phase}" the rule blocks instead`;
|
|
3697
|
+
}
|
|
3698
|
+
return null;
|
|
3699
|
+
}
|
|
3700
|
+
|
|
3701
|
+
// src/protect/rules/validate.js
|
|
3702
|
+
var LIMITS2 = LIMITS;
|
|
3703
|
+
var PHASES2 = new Set(PHASES);
|
|
3704
|
+
var ACTIONS2 = new Set(ACTIONS);
|
|
3705
|
+
function validateBundle(bundle, opts = {}) {
|
|
3706
|
+
const rejected = [];
|
|
3707
|
+
const inFirewall = Array.isArray(bundle?.firewall) ? bundle.firewall : [];
|
|
3708
|
+
const inWhitelists = Array.isArray(bundle?.whitelists) ? bundle.whitelists : [];
|
|
3709
|
+
const firewall = [];
|
|
3710
|
+
for (const rule of inFirewall) {
|
|
3711
|
+
if (firewall.length >= LIMITS2.maxRules) {
|
|
3712
|
+
rejected.push({ id: idOf(rule), reason: `bundle exceeds maxRules (${LIMITS2.maxRules})` });
|
|
3713
|
+
continue;
|
|
3193
3714
|
}
|
|
3715
|
+
const reason = enforceableRuleProblem(rule);
|
|
3716
|
+
if (reason) rejected.push({ id: idOf(rule), reason });
|
|
3717
|
+
else firewall.push(rule);
|
|
3194
3718
|
}
|
|
3195
|
-
const
|
|
3196
|
-
|
|
3197
|
-
|
|
3198
|
-
|
|
3199
|
-
|
|
3200
|
-
|
|
3201
|
-
|
|
3719
|
+
const whitelists = [];
|
|
3720
|
+
for (const wl of inWhitelists) {
|
|
3721
|
+
if (whitelists.length >= LIMITS2.maxWhitelists) {
|
|
3722
|
+
rejected.push({ id: idOf(wl), reason: `bundle exceeds maxWhitelists (${LIMITS2.maxWhitelists})` });
|
|
3723
|
+
continue;
|
|
3724
|
+
}
|
|
3725
|
+
if (!opts.allowGlobalWhitelists && wl && Array.isArray(wl.rule_v2) && !wl.rule_id) {
|
|
3726
|
+
rejected.push({ id: idOf(wl), reason: "whitelist has no rule_id (would suppress every rule); set allowGlobalWhitelists to permit" });
|
|
3727
|
+
continue;
|
|
3728
|
+
}
|
|
3729
|
+
const reason = conditionsProblem(wl?.rule_v2);
|
|
3730
|
+
if (reason) rejected.push({ id: idOf(wl), reason: `whitelist: ${reason}` });
|
|
3731
|
+
else whitelists.push(wl);
|
|
3202
3732
|
}
|
|
3203
|
-
const uri = url.pathname + url.search;
|
|
3204
|
-
const forwarded = headers["cf-connecting-ip"] || headers["x-forwarded-for"] || headers["x-real-ip"] || "";
|
|
3205
3733
|
return {
|
|
3206
|
-
|
|
3207
|
-
|
|
3208
|
-
originalUrl: uri,
|
|
3209
|
-
query,
|
|
3210
|
-
body,
|
|
3211
|
-
files,
|
|
3212
|
-
headers,
|
|
3213
|
-
ip: forwarded.split(",")[0].trim() || req.socket && req.socket.remoteAddress || "",
|
|
3214
|
-
cookies: parseCookies2(headers.cookie),
|
|
3215
|
-
// Verbatim body text: preserves literal keys (e.g. `__proto__`) that JSON.stringify drops.
|
|
3216
|
-
_rawBody: rawBody
|
|
3734
|
+
bundle: { ...bundle, firewall, whitelists },
|
|
3735
|
+
rejected
|
|
3217
3736
|
};
|
|
3218
3737
|
}
|
|
3219
|
-
function
|
|
3220
|
-
const
|
|
3221
|
-
|
|
3222
|
-
|
|
3738
|
+
function idOf(rule) {
|
|
3739
|
+
const id = rule?.id ?? rule?.rule_id;
|
|
3740
|
+
return id === void 0 || id === null ? "(unidentified)" : String(id);
|
|
3741
|
+
}
|
|
3742
|
+
function enforceableRuleProblem(rule) {
|
|
3743
|
+
if (!rule || typeof rule !== "object") return "not an object";
|
|
3744
|
+
const nullReason = nullPropertyProblem(rule);
|
|
3745
|
+
if (nullReason) return nullReason;
|
|
3746
|
+
if (rule.phase !== void 0 && !PHASES2.has(rule.phase)) return `unknown phase "${rule.phase}"`;
|
|
3747
|
+
const actionReason = actionProblem(rule.action, rule);
|
|
3748
|
+
if (actionReason) return actionReason;
|
|
3749
|
+
const scopeReason = whenProblem(rule.when);
|
|
3750
|
+
if (scopeReason) return scopeReason;
|
|
3751
|
+
const propertyReason = rulePropertyProblem(rule);
|
|
3752
|
+
if (propertyReason) return propertyReason;
|
|
3753
|
+
return conditionsProblem(rule.rule_v2);
|
|
3754
|
+
}
|
|
3755
|
+
function conditionsProblem(conditions, depth = 0) {
|
|
3756
|
+
if (!Array.isArray(conditions)) return "rule_v2 must be an array of conditions";
|
|
3757
|
+
if (conditions.length === 0) return "rule_v2 is empty (would never match)";
|
|
3758
|
+
if (depth > LIMITS2.maxNestingDepth) return `nesting deeper than ${LIMITS2.maxNestingDepth}`;
|
|
3759
|
+
if (conditions.length > LIMITS2.maxConditionsPerRule) {
|
|
3760
|
+
return `more than ${LIMITS2.maxConditionsPerRule} conditions`;
|
|
3223
3761
|
}
|
|
3224
|
-
for (const
|
|
3225
|
-
const
|
|
3226
|
-
if (
|
|
3762
|
+
for (const c of conditions) {
|
|
3763
|
+
const shapeReason = conditionShapeProblem(c);
|
|
3764
|
+
if (shapeReason) return shapeReason;
|
|
3765
|
+
if (isGroup(c)) {
|
|
3766
|
+
const nested = conditionsProblem(c.rules, depth + 1);
|
|
3767
|
+
if (nested) return nested;
|
|
3227
3768
|
continue;
|
|
3228
3769
|
}
|
|
3229
|
-
|
|
3770
|
+
const parameterReason = parameterProblem(c.parameter);
|
|
3771
|
+
if (parameterReason) return parameterReason;
|
|
3772
|
+
const mutationReason = mutationsProblem(c.mutations);
|
|
3773
|
+
if (mutationReason) return mutationReason;
|
|
3774
|
+
const matchReason = matchProblem(c.match, c.parameter);
|
|
3775
|
+
if (matchReason) return matchReason;
|
|
3776
|
+
const m = c.match;
|
|
3777
|
+
if (m.type === "regex") {
|
|
3778
|
+
if (typeof m.value !== "string") return "regex match.value must be a string";
|
|
3779
|
+
if (m.value.length > LIMITS2.maxRegexLength) return `regex longer than ${LIMITS2.maxRegexLength} chars`;
|
|
3780
|
+
} else if (typeof m.value === "string" && m.value.length > LIMITS2.maxValueLength) {
|
|
3781
|
+
return `match.value longer than ${LIMITS2.maxValueLength} chars`;
|
|
3782
|
+
}
|
|
3783
|
+
if (m.match) {
|
|
3784
|
+
const nested = conditionsProblem([{ parameter: c.parameter, match: m.match }], depth + 1);
|
|
3785
|
+
if (nested) return nested;
|
|
3786
|
+
}
|
|
3230
3787
|
}
|
|
3231
|
-
return
|
|
3788
|
+
return null;
|
|
3232
3789
|
}
|
|
3233
3790
|
|
|
3234
|
-
// src/protect/
|
|
3235
|
-
|
|
3236
|
-
|
|
3237
|
-
|
|
3238
|
-
|
|
3239
|
-
|
|
3240
|
-
|
|
3241
|
-
|
|
3242
|
-
|
|
3243
|
-
|
|
3791
|
+
// src/protect/capture-plan.js
|
|
3792
|
+
var NEVER_CAPTURABLE = /* @__PURE__ */ new Set(["response"]);
|
|
3793
|
+
var CAPTURE_LIMITS = Object.freeze({
|
|
3794
|
+
/** Named and prefix values in one event, together. Raw has its own allowance. */
|
|
3795
|
+
capturedValues: 10,
|
|
3796
|
+
/** Characters of any single captured value. */
|
|
3797
|
+
valueChars: 512,
|
|
3798
|
+
/** Resolved values one prefix permission may contribute. */
|
|
3799
|
+
prefixValues: 5
|
|
3800
|
+
});
|
|
3801
|
+
function parametersOf(rule) {
|
|
3802
|
+
const out = /* @__PURE__ */ new Set();
|
|
3803
|
+
const add = (parameter) => {
|
|
3804
|
+
if (typeof parameter === "string" && parameter !== "rules") out.add(parameter);
|
|
3244
3805
|
};
|
|
3245
|
-
|
|
3246
|
-
|
|
3247
|
-
|
|
3248
|
-
|
|
3249
|
-
const { isIP } = await import("net");
|
|
3250
|
-
if (typeof resolveLookup === "function" && typeof isIP === "function") {
|
|
3251
|
-
screen = { lookup: resolveLookup, isIP, isExempt: (h) => exempt.has(String(h).toLowerCase()) };
|
|
3252
|
-
}
|
|
3253
|
-
} catch {
|
|
3254
|
-
screen = null;
|
|
3806
|
+
const collect = (parameter) => {
|
|
3807
|
+
if (Array.isArray(parameter)) {
|
|
3808
|
+
for (const member of parameter) add(member);
|
|
3809
|
+
return;
|
|
3255
3810
|
}
|
|
3256
|
-
|
|
3257
|
-
|
|
3258
|
-
const
|
|
3259
|
-
if (!
|
|
3260
|
-
|
|
3261
|
-
|
|
3811
|
+
add(parameter);
|
|
3812
|
+
};
|
|
3813
|
+
const walk2 = (conditions, depth) => {
|
|
3814
|
+
if (!Array.isArray(conditions) || depth > 20) return;
|
|
3815
|
+
for (const condition of conditions) {
|
|
3816
|
+
if (!condition || typeof condition !== "object") continue;
|
|
3817
|
+
collect(condition.parameter);
|
|
3818
|
+
if (Array.isArray(condition.rules)) walk2(condition.rules, depth + 1);
|
|
3262
3819
|
}
|
|
3263
|
-
|
|
3264
|
-
|
|
3265
|
-
|
|
3266
|
-
|
|
3267
|
-
|
|
3268
|
-
|
|
3269
|
-
|
|
3270
|
-
|
|
3271
|
-
|
|
3272
|
-
|
|
3273
|
-
|
|
3274
|
-
|
|
3275
|
-
|
|
3276
|
-
|
|
3277
|
-
|
|
3278
|
-
|
|
3820
|
+
};
|
|
3821
|
+
walk2(rule?.rule_v2, 0);
|
|
3822
|
+
return [...out];
|
|
3823
|
+
}
|
|
3824
|
+
function rawOptIn(rule) {
|
|
3825
|
+
if (!rule || typeof rule !== "object" || !Object.hasOwn(rule, "capture")) return null;
|
|
3826
|
+
const capture = rule.capture;
|
|
3827
|
+
if (capture === null || typeof capture !== "object") return null;
|
|
3828
|
+
if (captureProblem(capture) !== null) return null;
|
|
3829
|
+
return { chars: Math.min(capture.raw_chars, CAPTURE_RAW_CHARS_MAX) };
|
|
3830
|
+
}
|
|
3831
|
+
var NOTHING = Object.freeze({
|
|
3832
|
+
named: Object.freeze([]),
|
|
3833
|
+
prefixes: Object.freeze([]),
|
|
3834
|
+
raw: null,
|
|
3835
|
+
limits: CAPTURE_LIMITS
|
|
3836
|
+
});
|
|
3837
|
+
function derivePlan(rule) {
|
|
3838
|
+
const named = /* @__PURE__ */ new Set();
|
|
3839
|
+
const prefixes = /* @__PURE__ */ new Set();
|
|
3840
|
+
if (enforceableRuleProblem(rule) !== null) return NOTHING;
|
|
3841
|
+
const parameters = parametersOf(rule).filter((parameter) => parameterProblem(parameter) === null);
|
|
3842
|
+
for (const parameter of parameters) {
|
|
3843
|
+
const dot = parameter.indexOf(".");
|
|
3844
|
+
if (dot === -1) continue;
|
|
3845
|
+
const source = parameter.slice(0, dot);
|
|
3846
|
+
const key = parameter.slice(dot + 1);
|
|
3847
|
+
if (SOURCES[source]?.keyed !== true || NEVER_CAPTURABLE.has(source)) continue;
|
|
3848
|
+
if (key.endsWith("*")) {
|
|
3849
|
+
const prefix = key.slice(0, -1);
|
|
3850
|
+
if (prefix !== "") prefixes.add(`${source}.${prefix}`);
|
|
3851
|
+
continue;
|
|
3279
3852
|
}
|
|
3853
|
+
named.add(`${source}.${key}`);
|
|
3854
|
+
}
|
|
3855
|
+
const raw = rawOptIn(rule);
|
|
3856
|
+
return Object.freeze({
|
|
3857
|
+
named: Object.freeze([...named].sort()),
|
|
3858
|
+
prefixes: Object.freeze([...prefixes].sort()),
|
|
3859
|
+
raw: raw === null ? null : Object.freeze(raw),
|
|
3860
|
+
limits: CAPTURE_LIMITS
|
|
3280
3861
|
});
|
|
3281
|
-
|
|
3282
|
-
|
|
3283
|
-
|
|
3284
|
-
|
|
3285
|
-
|
|
3286
|
-
|
|
3287
|
-
|
|
3288
|
-
|
|
3289
|
-
|
|
3290
|
-
|
|
3291
|
-
|
|
3292
|
-
|
|
3293
|
-
|
|
3294
|
-
|
|
3295
|
-
|
|
3296
|
-
|
|
3297
|
-
|
|
3298
|
-
|
|
3299
|
-
|
|
3300
|
-
|
|
3301
|
-
|
|
3302
|
-
|
|
3303
|
-
|
|
3304
|
-
|
|
3305
|
-
|
|
3306
|
-
|
|
3307
|
-
|
|
3308
|
-
|
|
3309
|
-
|
|
3310
|
-
|
|
3311
|
-
|
|
3312
|
-
|
|
3313
|
-
hop === 0 ? cur : new Request(url, { method, headers, body, redirect: "manual", signal })
|
|
3314
|
-
);
|
|
3315
|
-
const location = REDIRECT_STATUSES.has(resp.status) ? resp.headers.get("location") : null;
|
|
3316
|
-
if (!location) return resp;
|
|
3317
|
-
if (hop >= MAX_REDIRECTS) throw new Error("Patchstack blocked an outbound request: too many redirects");
|
|
3318
|
-
const next = new URL(location, url).href;
|
|
3319
|
-
if (resp.status === 303 || (resp.status === 301 || resp.status === 302) && method === "POST") {
|
|
3320
|
-
method = "GET";
|
|
3321
|
-
body = void 0;
|
|
3322
|
-
headers.delete("content-type");
|
|
3323
|
-
headers.delete("content-length");
|
|
3324
|
-
}
|
|
3325
|
-
if (new URL(next).origin !== new URL(url).origin) {
|
|
3326
|
-
headers.delete("authorization");
|
|
3327
|
-
headers.delete("cookie");
|
|
3328
|
-
}
|
|
3329
|
-
await screenUrl(next, method);
|
|
3330
|
-
url = next;
|
|
3862
|
+
}
|
|
3863
|
+
function permitsAnything(plan) {
|
|
3864
|
+
return plan.named.length > 0 || plan.prefixes.length > 0 || plan.raw !== null;
|
|
3865
|
+
}
|
|
3866
|
+
var LANES = Object.freeze([2166136261, 16777619, 2654435769, 2246822507]);
|
|
3867
|
+
function planReference(plan) {
|
|
3868
|
+
const limits = plan?.limits ?? {};
|
|
3869
|
+
const canonical = JSON.stringify([
|
|
3870
|
+
[...plan?.named ?? []].sort(),
|
|
3871
|
+
[...plan?.prefixes ?? []].sort(),
|
|
3872
|
+
plan?.raw?.chars ?? 0,
|
|
3873
|
+
Object.entries(limits).map(([key, value]) => [key, value]).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0)
|
|
3874
|
+
]);
|
|
3875
|
+
const digest = LANES.map((base) => {
|
|
3876
|
+
let hash = base >>> 0;
|
|
3877
|
+
for (let i = 0; i < canonical.length; i++) {
|
|
3878
|
+
hash ^= canonical.charCodeAt(i);
|
|
3879
|
+
hash = Math.imul(hash, 16777619) >>> 0;
|
|
3880
|
+
}
|
|
3881
|
+
return hash.toString(16).padStart(8, "0");
|
|
3882
|
+
}).join("");
|
|
3883
|
+
return `cp2-${digest}`;
|
|
3884
|
+
}
|
|
3885
|
+
function createPlanCache() {
|
|
3886
|
+
const byRule = /* @__PURE__ */ new WeakMap();
|
|
3887
|
+
let derivations = 0;
|
|
3888
|
+
return {
|
|
3889
|
+
for(rule) {
|
|
3890
|
+
if (rule === null || typeof rule !== "object") {
|
|
3891
|
+
const plan2 = derivePlan(rule);
|
|
3892
|
+
derivations += 1;
|
|
3893
|
+
return Object.freeze({ plan: plan2, reference: planReference(plan2) });
|
|
3331
3894
|
}
|
|
3332
|
-
|
|
3333
|
-
|
|
3334
|
-
|
|
3335
|
-
|
|
3336
|
-
|
|
3337
|
-
|
|
3338
|
-
|
|
3339
|
-
|
|
3895
|
+
const hit = byRule.get(rule);
|
|
3896
|
+
if (hit) return hit;
|
|
3897
|
+
const plan = derivePlan(rule);
|
|
3898
|
+
derivations += 1;
|
|
3899
|
+
const entry = Object.freeze({ plan, reference: planReference(plan) });
|
|
3900
|
+
byRule.set(rule, entry);
|
|
3901
|
+
return entry;
|
|
3902
|
+
},
|
|
3903
|
+
/** How many plans have been derived — the observable difference between a hit and a miss. */
|
|
3904
|
+
get derivations() {
|
|
3905
|
+
return derivations;
|
|
3906
|
+
}
|
|
3907
|
+
};
|
|
3908
|
+
}
|
|
3909
|
+
function captureValues(plan, resolver) {
|
|
3910
|
+
const nothing = { values: [], omitted: 0, unsupported: 0, failed: 0, unavailable: false, raw: null };
|
|
3911
|
+
if (!plan || !permitsAnything(plan)) return nothing;
|
|
3912
|
+
if (!resolver || typeof resolver.resolve !== "function") return { ...nothing, unavailable: true };
|
|
3913
|
+
const limits = plan.limits ?? CAPTURE_LIMITS;
|
|
3914
|
+
const values = [];
|
|
3915
|
+
let omitted = 0;
|
|
3916
|
+
let unsupported = 0;
|
|
3917
|
+
let failed = 0;
|
|
3918
|
+
let attempted = 0;
|
|
3919
|
+
const read = (parameter) => {
|
|
3920
|
+
attempted += 1;
|
|
3340
3921
|
try {
|
|
3341
|
-
const
|
|
3342
|
-
|
|
3343
|
-
if (restore) restores.push(restore);
|
|
3922
|
+
const resolved = resolver.resolve(parameter);
|
|
3923
|
+
return Array.isArray(resolved) ? resolved : [];
|
|
3344
3924
|
} catch {
|
|
3925
|
+
failed += 1;
|
|
3926
|
+
return null;
|
|
3345
3927
|
}
|
|
3346
|
-
}
|
|
3347
|
-
|
|
3348
|
-
|
|
3349
|
-
|
|
3350
|
-
|
|
3351
|
-
|
|
3352
|
-
}
|
|
3928
|
+
};
|
|
3929
|
+
const classify = (value) => {
|
|
3930
|
+
const text = asText(value);
|
|
3931
|
+
if (text === null) {
|
|
3932
|
+
unsupported += 1;
|
|
3933
|
+
return null;
|
|
3353
3934
|
}
|
|
3935
|
+
return text;
|
|
3354
3936
|
};
|
|
3355
|
-
|
|
3356
|
-
|
|
3357
|
-
|
|
3358
|
-
|
|
3359
|
-
const originalGet = http.get;
|
|
3360
|
-
const wrap = (original) => function(...args) {
|
|
3361
|
-
const target = extractHttpTarget(args);
|
|
3362
|
-
if (target && block(target.url, target.host, target.method)) {
|
|
3363
|
-
throw new Error(`Patchstack blocked an outbound request to a disallowed address: ${target.host ?? target.url}`);
|
|
3937
|
+
const record = (parameter, text) => {
|
|
3938
|
+
if (values.length >= limits.capturedValues) {
|
|
3939
|
+
omitted += 1;
|
|
3940
|
+
return false;
|
|
3364
3941
|
}
|
|
3365
|
-
|
|
3366
|
-
|
|
3367
|
-
|
|
3368
|
-
|
|
3942
|
+
const capped = text.length > limits.valueChars;
|
|
3943
|
+
values.push({
|
|
3944
|
+
parameter,
|
|
3945
|
+
value: capped ? text.slice(0, limits.valueChars) : text,
|
|
3946
|
+
...capped ? { truncated: true } : {}
|
|
3947
|
+
});
|
|
3948
|
+
return true;
|
|
3949
|
+
};
|
|
3950
|
+
for (const parameter of plan.named) {
|
|
3951
|
+
for (const value of read(parameter) ?? []) {
|
|
3952
|
+
const text = classify(value);
|
|
3953
|
+
if (text !== null) record(parameter, text);
|
|
3954
|
+
}
|
|
3955
|
+
}
|
|
3956
|
+
for (const prefix of plan.prefixes) {
|
|
3957
|
+
const pattern = `${prefix}*`;
|
|
3958
|
+
let taken = 0;
|
|
3959
|
+
for (const value of read(pattern) ?? []) {
|
|
3960
|
+
const text = classify(value);
|
|
3961
|
+
if (text === null) continue;
|
|
3962
|
+
if (taken >= limits.prefixValues) {
|
|
3963
|
+
omitted += 1;
|
|
3964
|
+
continue;
|
|
3369
3965
|
}
|
|
3966
|
+
if (record(pattern, text)) taken += 1;
|
|
3370
3967
|
}
|
|
3371
|
-
return original.apply(this, args);
|
|
3372
|
-
};
|
|
3373
|
-
const guardedRequest = wrap(originalRequest);
|
|
3374
|
-
http.request = guardedRequest;
|
|
3375
|
-
let guardedGet;
|
|
3376
|
-
if (typeof originalGet === "function") {
|
|
3377
|
-
guardedGet = wrap(originalGet);
|
|
3378
|
-
http.get = guardedGet;
|
|
3379
3968
|
}
|
|
3380
|
-
|
|
3381
|
-
|
|
3382
|
-
|
|
3383
|
-
if (
|
|
3384
|
-
|
|
3385
|
-
|
|
3386
|
-
}
|
|
3387
|
-
function extractHttpTarget(args) {
|
|
3388
|
-
const first = args[0];
|
|
3389
|
-
try {
|
|
3390
|
-
if (typeof first === "string" || first instanceof URL) {
|
|
3391
|
-
const url = new URL(String(first));
|
|
3392
|
-
const opts = args.find((a) => a && typeof a === "object" && !(a instanceof URL));
|
|
3393
|
-
return { url: url.href, host: url.hostname, method: opts && opts.method || "GET" };
|
|
3394
|
-
}
|
|
3395
|
-
if (first && typeof first === "object") {
|
|
3396
|
-
const host = normalizeHost(first.hostname || first.host) || "localhost";
|
|
3397
|
-
const protocol = first.protocol || "http:";
|
|
3398
|
-
const port = first.port ? `:${first.port}` : "";
|
|
3399
|
-
const path7 = first.path || "/";
|
|
3400
|
-
return { url: `${protocol}//${host}${port}${path7}`, host, method: first.method || "GET" };
|
|
3969
|
+
let raw = null;
|
|
3970
|
+
if (plan.raw !== null) {
|
|
3971
|
+
const text = asText((read("raw") ?? [])[0]);
|
|
3972
|
+
if (text !== null) {
|
|
3973
|
+
const capped = text.length > plan.raw.chars;
|
|
3974
|
+
raw = { value: capped ? text.slice(0, plan.raw.chars) : text, ...capped ? { truncated: true } : {} };
|
|
3401
3975
|
}
|
|
3402
|
-
} catch {
|
|
3403
3976
|
}
|
|
3404
|
-
|
|
3977
|
+
const unavailable = attempted > 0 && failed === attempted;
|
|
3978
|
+
return { values, omitted, unsupported, failed, unavailable, raw };
|
|
3405
3979
|
}
|
|
3406
|
-
function
|
|
3407
|
-
|
|
3408
|
-
|
|
3409
|
-
if (
|
|
3410
|
-
if ((host.match(/:/g) || []).length > 1) return host;
|
|
3411
|
-
return host.split(":")[0];
|
|
3412
|
-
}
|
|
3413
|
-
function screenResolved(addresses, target, block) {
|
|
3414
|
-
for (const a of addresses || []) {
|
|
3415
|
-
const ip = a && typeof a === "object" ? a.address : a;
|
|
3416
|
-
if (ip && block(target.url, ip, target.method)) return ip;
|
|
3417
|
-
}
|
|
3980
|
+
function asText(value) {
|
|
3981
|
+
if (typeof value === "string") return value;
|
|
3982
|
+
if (typeof value === "number" && Number.isFinite(value)) return String(value);
|
|
3983
|
+
if (typeof value === "boolean") return String(value);
|
|
3418
3984
|
return null;
|
|
3419
3985
|
}
|
|
3420
|
-
|
|
3421
|
-
|
|
3422
|
-
|
|
3423
|
-
|
|
3424
|
-
|
|
3425
|
-
|
|
3426
|
-
|
|
3427
|
-
|
|
3428
|
-
|
|
3429
|
-
|
|
3430
|
-
|
|
3431
|
-
|
|
3432
|
-
|
|
3433
|
-
|
|
3434
|
-
|
|
3435
|
-
|
|
3436
|
-
|
|
3437
|
-
|
|
3438
|
-
|
|
3439
|
-
|
|
3440
|
-
|
|
3441
|
-
|
|
3442
|
-
|
|
3443
|
-
|
|
3444
|
-
|
|
3445
|
-
|
|
3446
|
-
|
|
3447
|
-
}
|
|
3986
|
+
|
|
3987
|
+
// src/protect/engine/pulse-client.js
|
|
3988
|
+
init_pulse_token();
|
|
3989
|
+
var DEFAULT_BASE_URL2 = "https://api.patchstack.com/monitor/pulse";
|
|
3990
|
+
var DEFAULT_CACHE_TTL2 = 3e5;
|
|
3991
|
+
var JITTER_FRACTION2 = 0.1;
|
|
3992
|
+
var PulseRuleClient = class {
|
|
3993
|
+
#siteUuid;
|
|
3994
|
+
#timeoutMs;
|
|
3995
|
+
#baseUrl;
|
|
3996
|
+
#cacheTtl;
|
|
3997
|
+
#cache = null;
|
|
3998
|
+
#cacheTime = null;
|
|
3999
|
+
#ttlEffective = 0;
|
|
4000
|
+
#etag;
|
|
4001
|
+
#pulseAuth;
|
|
4002
|
+
#detectionState;
|
|
4003
|
+
constructor({ siteUuid, baseUrl, cacheTtl, etag, timeoutMs, pulseAuth, detectionState } = {}) {
|
|
4004
|
+
this.#timeoutMs = Number(timeoutMs) > 0 ? Number(timeoutMs) : 3e4;
|
|
4005
|
+
this.#siteUuid = siteUuid ?? process.env.PATCHSTACK_SITE_UUID;
|
|
4006
|
+
this.#baseUrl = safeBaseUrl(baseUrl ?? process.env.PATCHSTACK_PULSE_RULES_URL, DEFAULT_BASE_URL2, "rule endpoint");
|
|
4007
|
+
this.#cacheTtl = Number.isFinite(cacheTtl) && cacheTtl > 0 ? cacheTtl : DEFAULT_CACHE_TTL2;
|
|
4008
|
+
this.#etag = etag ?? null;
|
|
4009
|
+
this.#pulseAuth = pulseAuth ?? null;
|
|
4010
|
+
this.#detectionState = typeof detectionState === "string" ? detectionState : null;
|
|
4011
|
+
if (!this.#siteUuid) {
|
|
4012
|
+
throw new Error("Patchstack site UUID is required. Pass { siteUuid } or set PATCHSTACK_SITE_UUID.");
|
|
3448
4013
|
}
|
|
3449
|
-
};
|
|
3450
|
-
return injectLookupOption(args, screeningLookup);
|
|
3451
|
-
}
|
|
3452
|
-
function injectLookupOption(args, lookup) {
|
|
3453
|
-
const first = args[0];
|
|
3454
|
-
if (first && typeof first === "object" && !(first instanceof URL)) {
|
|
3455
|
-
return [{ ...first, lookup }, ...args.slice(1)];
|
|
3456
4014
|
}
|
|
3457
|
-
|
|
3458
|
-
|
|
3459
|
-
|
|
3460
|
-
|
|
3461
|
-
|
|
3462
|
-
|
|
4015
|
+
async getRules() {
|
|
4016
|
+
const now = Date.now();
|
|
4017
|
+
if (this.#cache && this.#cacheTime !== null && now - this.#cacheTime < this.#ttlEffective) {
|
|
4018
|
+
return this.#cache;
|
|
4019
|
+
}
|
|
4020
|
+
const url = `${this.#baseUrl}/rules/${encodeURIComponent(this.#siteUuid)}`;
|
|
4021
|
+
try {
|
|
4022
|
+
const auth = await pulseAuthHeader(
|
|
4023
|
+
{ pulseAuth: this.#pulseAuth, endpoint: this.#baseUrl, timeoutMs: this.#timeoutMs },
|
|
4024
|
+
fetch
|
|
4025
|
+
);
|
|
4026
|
+
const headers = { Accept: "application/json", ...auth };
|
|
4027
|
+
if (this.#detectionState !== null && typeof auth.Authorization === "string") {
|
|
4028
|
+
headers["X-Patchstack-Detections"] = this.#detectionState;
|
|
4029
|
+
}
|
|
4030
|
+
if (this.#etag) headers["If-None-Match"] = this.#etag;
|
|
4031
|
+
const response = await fetch(url, { method: "GET", headers, signal: AbortSignal.timeout(this.#timeoutMs) });
|
|
4032
|
+
if (response.status === 304) {
|
|
4033
|
+
this.#touch(now);
|
|
4034
|
+
return this.#cache ?? { success: true, notModified: true, etag: this.#etag, firewall: [], whitelists: [], whitelist_keys: {} };
|
|
4035
|
+
}
|
|
4036
|
+
if (!response.ok) {
|
|
4037
|
+
return { success: false, error: `API returned ${response.status}`, firewall: [], whitelists: [], whitelist_keys: {} };
|
|
4038
|
+
}
|
|
4039
|
+
const data = await response.json();
|
|
4040
|
+
if (!data || !Array.isArray(data.firewall)) {
|
|
4041
|
+
return { success: false, error: "unexpected response shape (no firewall array)", firewall: [], whitelists: [], whitelist_keys: {} };
|
|
4042
|
+
}
|
|
4043
|
+
const result = {
|
|
4044
|
+
success: true,
|
|
4045
|
+
notModified: false,
|
|
4046
|
+
etag: response.headers?.get?.("etag") ?? null,
|
|
4047
|
+
firewall: data.firewall,
|
|
4048
|
+
whitelists: Array.isArray(data.whitelists) ? data.whitelists : [],
|
|
4049
|
+
whitelist_keys: data.whitelist_keys ?? {},
|
|
4050
|
+
...enforcementField(data)
|
|
4051
|
+
};
|
|
4052
|
+
this.#cache = result;
|
|
4053
|
+
this.#etag = result.etag;
|
|
4054
|
+
this.#touch(now);
|
|
4055
|
+
return result;
|
|
4056
|
+
} catch (err) {
|
|
4057
|
+
return {
|
|
4058
|
+
success: false,
|
|
4059
|
+
error: err.name === "TimeoutError" ? "Request timed out" : err.message,
|
|
4060
|
+
firewall: [],
|
|
4061
|
+
whitelists: [],
|
|
4062
|
+
whitelist_keys: {}
|
|
4063
|
+
};
|
|
4064
|
+
}
|
|
3463
4065
|
}
|
|
3464
|
-
|
|
3465
|
-
|
|
3466
|
-
|
|
3467
|
-
}
|
|
3468
|
-
|
|
3469
|
-
// src/protect/defaults.js
|
|
3470
|
-
var DEFAULT_RESPONSE_RULES = [
|
|
3471
|
-
{
|
|
3472
|
-
id: "resp-private-key",
|
|
3473
|
-
title: "Private key in response body",
|
|
3474
|
-
phase: "response",
|
|
3475
|
-
category: "secret-exposure",
|
|
3476
|
-
action: "redact",
|
|
3477
|
-
prefilter: ["PRIVATE KEY"],
|
|
3478
|
-
rule_v2: [{ parameter: "response.body", match: { type: "regex", value: "/-----BEGIN (?:RSA |EC |OPENSSH |DSA |PGP )?PRIVATE KEY-----/" } }]
|
|
3479
|
-
},
|
|
3480
|
-
{
|
|
3481
|
-
id: "resp-aws-access-key",
|
|
3482
|
-
title: "AWS access key id in response body",
|
|
3483
|
-
phase: "response",
|
|
3484
|
-
category: "secret-exposure",
|
|
3485
|
-
action: "redact",
|
|
3486
|
-
prefilter: ["AKIA", "ASIA"],
|
|
3487
|
-
rule_v2: [{ parameter: "response.body", match: { type: "regex", value: "/\\b(?:AKIA|ASIA)[0-9A-Z]{16}\\b/" } }]
|
|
3488
|
-
},
|
|
3489
|
-
{
|
|
3490
|
-
id: "resp-gcp-api-key",
|
|
3491
|
-
title: "Google API key in response body",
|
|
3492
|
-
phase: "response",
|
|
3493
|
-
category: "secret-exposure",
|
|
3494
|
-
action: "redact",
|
|
3495
|
-
prefilter: ["AIza"],
|
|
3496
|
-
rule_v2: [{ parameter: "response.body", match: { type: "regex", value: "/\\bAIza[0-9A-Za-z_-]{35}\\b/" } }]
|
|
3497
|
-
},
|
|
3498
|
-
{
|
|
3499
|
-
id: "resp-vendor-api-key",
|
|
3500
|
-
title: "Vendor API key / token in response body",
|
|
3501
|
-
phase: "response",
|
|
3502
|
-
category: "secret-exposure",
|
|
3503
|
-
action: "redact",
|
|
3504
|
-
// High-signal, prefix-anchored provider tokens that never legitimately appear in a response
|
|
3505
|
-
// body: Stripe (sk_live_/rk_live_), GitHub (gh[opsu]_ / github_pat_), GitLab (glpat-),
|
|
3506
|
-
// Slack (xox[baprs]-), Anthropic (sk-ant-), Google OAuth (ya29.), npm (npm_). Trailing
|
|
3507
|
-
// (?![0-9A-Za-z]) instead of \\b since some tokens end in - / _ .
|
|
3508
|
-
prefilter: ["sk_live_", "rk_live_", "ghp_", "gho_", "ghs_", "ghu_", "github_pat_", "glpat-", "xox", "sk-ant-", "ya29.", "npm_"],
|
|
3509
|
-
rule_v2: [{ parameter: "response.body", match: { type: "regex", value: "/\\b(?:sk_live_[0-9A-Za-z]{16,}|rk_live_[0-9A-Za-z]{16,}|gh[opsu]_[0-9A-Za-z]{36}|github_pat_[0-9A-Za-z_]{60,}|glpat-[0-9A-Za-z_-]{20,}|xox[baprs]-[0-9A-Za-z-]{10,}|sk-ant-[0-9A-Za-z_-]{20,}|ya29\\.[0-9A-Za-z_-]{20,}|npm_[0-9A-Za-z]{36})(?![0-9A-Za-z])/" } }]
|
|
3510
|
-
},
|
|
3511
|
-
{
|
|
3512
|
-
id: "resp-jwt",
|
|
3513
|
-
title: "JWT in response body",
|
|
3514
|
-
phase: "response",
|
|
3515
|
-
category: "secret-exposure",
|
|
3516
|
-
action: "redact",
|
|
3517
|
-
prefilter: ["eyJ"],
|
|
3518
|
-
rule_v2: [{ parameter: "response.body", match: { type: "regex", value: "/\\beyJ[A-Za-z0-9_-]{8,}\\.eyJ[A-Za-z0-9_-]{8,}\\.[A-Za-z0-9_-]{8,}\\b/" } }]
|
|
3519
|
-
},
|
|
3520
|
-
{
|
|
3521
|
-
id: "resp-db-connection-string",
|
|
3522
|
-
title: "Database connection string with credentials in response body",
|
|
3523
|
-
phase: "response",
|
|
3524
|
-
category: "secret-exposure",
|
|
3525
|
-
action: "redact",
|
|
3526
|
-
prefilter: ["mongodb", "postgres", "mysql", "redis", "amqp"],
|
|
3527
|
-
rule_v2: [{ parameter: "response.body", match: { type: "regex", value: "/\\b(?:mongodb(?:\\+srv)?|postgres(?:ql)?|mysql|redis|amqps?):\\/\\/[^\\s:@\\/]+:[^\\s:@\\/]+@/i" } }]
|
|
3528
|
-
},
|
|
3529
|
-
{
|
|
3530
|
-
id: "resp-stack-trace",
|
|
3531
|
-
title: "Node stack trace leaking in response body",
|
|
3532
|
-
phase: "response",
|
|
3533
|
-
category: "info-exposure",
|
|
3534
|
-
action: "redact",
|
|
3535
|
-
// No prefilter: a Node stack frame has no single distinctive literal (` at ` is too common to
|
|
3536
|
-
// gate on). The pattern is linearly bounded per line, so it runs on every screened body.
|
|
3537
|
-
rule_v2: [{ parameter: "response.body", match: { type: "regex", value: "/\\n\\s+at\\s+.+\\(.+:\\d+:\\d+\\)/" } }]
|
|
3538
|
-
},
|
|
3539
|
-
{
|
|
3540
|
-
id: "resp-sql-error",
|
|
3541
|
-
title: "SQL / ORM error disclosure in response body",
|
|
3542
|
-
phase: "response",
|
|
3543
|
-
category: "info-exposure",
|
|
3544
|
-
action: "redact",
|
|
3545
|
-
prefilter: ["SQLSTATE", "Sequelize", "ER_", "ORA-", "PG::", "SQLITE_ERROR", "SQL syntax"],
|
|
3546
|
-
rule_v2: [{ parameter: "response.body", match: { type: "regex", value: "/(SQLSTATE\\[[0-9A-Z]+\\]|SequelizeDatabaseError|ER_[A-Z_]+|ORA-\\d{5}|PG::[A-Za-z]+Error|SQLITE_ERROR|You have an error in your SQL syntax)/i" } }]
|
|
3547
|
-
},
|
|
3548
|
-
{
|
|
3549
|
-
id: "resp-exception-trace",
|
|
3550
|
-
title: "Backend exception / stack trace disclosure in response body",
|
|
3551
|
-
phase: "response",
|
|
3552
|
-
category: "info-exposure",
|
|
3553
|
-
action: "redact",
|
|
3554
|
-
// Multi-language exception/traceback signatures a normal API response never carries:
|
|
3555
|
-
// Python traceback, Java "Exception in thread", .NET System.*Exception, JVM stack frames,
|
|
3556
|
-
// Go goroutine dumps. (Node `at fn (file:line:col)` frames are handled by resp-stack-trace.)
|
|
3557
|
-
prefilter: ["Traceback", "Exception in thread", "System.", "goroutine ", ".java:", ".kt:", ".scala:", ".rb:", ".py:", ".cs:"],
|
|
3558
|
-
rule_v2: [{ parameter: "response.body", match: { type: "regex", value: '/(Traceback \\(most recent call last\\)|Exception in thread "|System\\.[A-Za-z.]+Exception|\\bat [\\w.$]+\\([\\w]+\\.(?:java|kt|scala|rb|py|cs):\\d+\\)|goroutine \\d+ \\[)/' } }]
|
|
4066
|
+
#touch(now) {
|
|
4067
|
+
this.#cacheTime = now;
|
|
4068
|
+
this.#ttlEffective = this.#cacheTtl - Math.floor(Math.random() * this.#cacheTtl * JITTER_FRACTION2);
|
|
3559
4069
|
}
|
|
3560
|
-
|
|
3561
|
-
|
|
3562
|
-
|
|
3563
|
-
|
|
3564
|
-
title: "Outbound request to an internal / metadata address (SSRF)",
|
|
3565
|
-
phase: "egress",
|
|
3566
|
-
category: "ssrf",
|
|
3567
|
-
rule_v2: [{ parameter: "egress.host", match: { type: "internal_host" } }]
|
|
4070
|
+
clearCache() {
|
|
4071
|
+
this.#cache = null;
|
|
4072
|
+
this.#cacheTime = null;
|
|
4073
|
+
this.#etag = null;
|
|
3568
4074
|
}
|
|
3569
|
-
|
|
3570
|
-
|
|
3571
|
-
|
|
3572
|
-
|
|
3573
|
-
return
|
|
4075
|
+
};
|
|
4076
|
+
function enforcementField(data) {
|
|
4077
|
+
if (!data || typeof data !== "object") return {};
|
|
4078
|
+
const v = data.enforcement ?? data.mode;
|
|
4079
|
+
return v === "block" || v === "dry-run" ? { enforcement: v } : {};
|
|
3574
4080
|
}
|
|
3575
|
-
function renderBlockPage({ url = "", code = "patchstack" } = {}) {
|
|
3576
|
-
const safeUrl = escapeHtml(url);
|
|
3577
|
-
const safeCode = escapeHtml(code || "patchstack");
|
|
3578
|
-
return `<!DOCTYPE html>
|
|
3579
|
-
<html lang="en">
|
|
3580
|
-
<head>
|
|
3581
|
-
<meta charset="UTF-8">
|
|
3582
|
-
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
|
3583
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
3584
|
-
<style type="text/css">
|
|
3585
|
-
.access-denied,.site-url-text{letter-spacing:.01em;color:#fff}body{background:#111216}.main-container{display:flex;justify-content:center;flex-direction:column;align-items:center}.container{width:688px;height:456px;background:#1c1e22;border-radius:16px;margin-top:120px}.content{height:330px;margin:64px}.access-denied{font-family:Verdana;font-style:normal;font-weight:600;font-size:24px;line-height:32px;display:flex;align-items:center}.descp-text,.site-url{display:flex;align-items:center}.site-url{margin-top:32px;width:560px;height:64px;background:#26282c;border-radius:4px}.reason-code-text,.site-url-text{font-family:Verdana;font-style:normal;border:none;background:#26282c}.site-url-text{text-overflow: ellipsis;font-weight:400;font-size:16px;line-height:24px;margin-left:16px;width:85%}.copy-icon{width:19px;height:22px;margin-left:20px}.copy-icon:hover{cursor:pointer}.reason-code-text{font-weight:600;font-size:24px;line-height:32px;letter-spacing:.01em;color:#AFE614;margin-left:16px;width:85%}.descp-text,.return-text{font-size:16px;line-height:24px;letter-spacing:.01em;font-weight:400}.return-text:hover{color: #AFE614}.descp-text,.logoText,.return-text,.secured-text{font-family:Verdana;font-style:normal;color:#fff}.descp{margin-top:32px;width:560px}.bottom-second-containers,.bottom-text-containers{align-items:center;display:flex}.bottom-container{margin-top:40px;display:flex;width:688px}.bottom-second-containers{margin-left:auto}.arrow-vector{margin-left:20px}.return-text{text-align:center;margin-left:12.59px;text-decoration:none}.secured-text{font-weight:400;font-size:11.2px;line-height:16px;display:flex;align-items:center;letter-spacing:.02em;margin-right:8px}.logoText{font-weight:600;font-size:16px;line-height:24px;letter-spacing:.01em;flex:none;order:1;flex-grow:0}.patch-logo{width:120px}.mobile-bottom-container{margin-top:40px;width:372px;display:none}.mobile-bottom-second-containers{margin-top:64px;display:flex;margin-left:16.5px}@media only screen and (max-width:900px){.container{background:#111216;width:370px;height:auto;margin-top:0}.content{margin-left:16.5px}.descp,.site-url{width:340px}.site-url-text{font-weight:400;font-size:14.4px;line-height:20px;letter-spacing:.02em;width:80%}.copy-icon{margin-left:20px}.reason-code-text{font-size:16px;line-height:32px;letter-spacing:.02em;margin-left:16px;width:80%}.descp-text,.return-text{font-size:14.4px;line-height:20px}.descp-text{display:flex;align-items:center;letter-spacing:.02em}.bottom-container{display:none}.mobile-bottom-container{display:block}.return-text{text-align:center;letter-spacing:.01em;color:#fff;margin-left:9.73px}}
|
|
3586
|
-
</style>
|
|
3587
|
-
<title>Access Denied</title>
|
|
3588
|
-
</head>
|
|
3589
|
-
<body>
|
|
3590
|
-
<div class="main-container">
|
|
3591
|
-
<div class="container">
|
|
3592
|
-
<div class="content">
|
|
3593
|
-
<span class="access-denied">Access Denied</span>
|
|
3594
|
-
<div class="site-url">
|
|
3595
|
-
<input id="url" class="site-url-text" type="text"
|
|
3596
|
-
value="${safeUrl}" readonly>
|
|
3597
|
-
<img class="copy-icon" src="data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTkiIGhlaWdodD0iMjMiIHZpZXdCb3g9IjAgMCAxOSAyMyIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTE0IDAuMDIzNDM3NUgwVjE2LjAyMzRIMlYyLjAyMzQ0SDE0VjAuMDIzNDM3NVpNMTkgNC4wMjM0NEg0VjIyLjAyMzRIMTlWNC4wMjM0NFpNMTcgMjAuMDIzNEg2VjYuMDIzNDRIMTdWMjAuMDIzNFoiIGZpbGw9IndoaXRlIi8+Cjwvc3ZnPgo=" alt="Copy Text" onmouseover="hover(this);" onmouseout="unhover(this);" onclick="copyText(id='url')">
|
|
3598
|
-
</div>
|
|
3599
|
-
<div class="site-url">
|
|
3600
|
-
<input id="response" class="reason-code-text" type="text" value="${safeCode}" readonly>
|
|
3601
|
-
<img class="copy-icon" src="data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTkiIGhlaWdodD0iMjMiIHZpZXdCb3g9IjAgMCAxOSAyMyIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTE0IDAuMDIzNDM3NUgwVjE2LjAyMzRIMlYyLjAyMzQ0SDE0VjAuMDIzNDM3NVpNMTkgNC4wMjM0NEg0VjIyLjAyMzRIMTlWNC4wMjM0NFpNMTcgMjAuMDIzNEg2VjYuMDIzNDRIMTdWMjAuMDIzNFoiIGZpbGw9IndoaXRlIi8+Cjwvc3ZnPgo=" alt="Copy Text" onmouseover="hover(this);" onmouseout="unhover(this);" onclick="copyText(id='response')">
|
|
3602
|
-
</div>
|
|
3603
|
-
<div class="descp">
|
|
3604
|
-
<span class="descp-text">This request has been blocked by Patchstack. If this message persists and you are a legitimate user, contact the site administrator with the reference above.</span>
|
|
3605
|
-
</div>
|
|
3606
|
-
</div>
|
|
3607
|
-
</div>
|
|
3608
|
-
<div class="bottom-container">
|
|
3609
|
-
<div class="bottom-text-containers">
|
|
3610
|
-
<img class="arrow-vector" src="data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iOCIgaGVpZ2h0PSIxMyIgdmlld0JveD0iMCAwIDggMTMiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxwYXRoIGQ9Ik03LjQxIDEuNDMzNDRMNiAwLjAyMzQzNzVMMCA2LjAyMzQ0TDYgMTIuMDIzNEw3LjQxIDEwLjYxMzRMMi44MyA2LjAyMzQ0TDcuNDEgMS40MzM0NFoiIGZpbGw9IiNBRkU2MTQiLz4KPC9zdmc+Cg==" alt="Arrow Left">
|
|
3611
|
-
<a href="/" class="return-text">Return to homepage</a>
|
|
3612
|
-
</div>
|
|
3613
|
-
<div class="bottom-second-containers">
|
|
3614
|
-
<span class="secured-text">This website is secured by</span>
|
|
3615
|
-
<a href="https://patchstack.com" class="logoText">
|
|
3616
|
-
<img class="patch-logo" src="data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTg0IiBoZWlnaHQ9IjIyIiB2aWV3Qm94PSIwIDAgMTg0IDIyIiBmaWxsPSJub25lIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPgo8cGF0aCBmaWxsLXJ1bGU9ImV2ZW5vZGQiIGNsaXAtcnVsZT0iZXZlbm9kZCIgZD0iTTUwLjQwMDEgMTEuNTAwOEM1MC40MDAxIDkuMTAwNzggNDkuMzAwMSA4LjEwMDc4IDQ2LjYwMDEgOC4xMDA3OEM0NC4wMDAxIDguMTAwNzggNDIuNzAwMSA5LjIwMDc4IDQyLjcwMDEgMTEuNTAwOEM0Mi43MDAxIDEzLjgwMDggNDQuMDAwMSAxNC45MDA4IDQ2LjYwMDEgMTQuOTAwOEM0OS4zMDAxIDE0LjkwMDggNTAuNDAwMSAxMy44MDA4IDUwLjQwMDEgMTEuNTAwOFpNNTMuNDAwMSAxMS41MDA4QzUzLjQwMDEgMTUuMzAwOCA1MS4zMDAxIDE3LjYwMDggNDcuNDAwMSAxNy42MDA4QzQ1LjQwMDEgMTcuNjAwOCA0My42MDAxIDE2LjUwMDggNDIuNzAwMSAxNC41MDA4VjIxLjUwMDhIMzkuNjAwMVY1LjYwMDc4SDQyLjcwMDFWOC41MDA3OEM0My42MDAxIDYuNTAwNzggNDUuNDAwMSA1LjMwMDc4IDQ3LjUwMDEgNS4zMDA3OEM1MS40MDAxIDUuMzAwNzggNTMuNDAwMSA3LjcwMDc4IDUzLjQwMDEgMTEuNTAwOFoiIGZpbGw9IndoaXRlIi8+CjxwYXRoIGZpbGwtcnVsZT0iZXZlbm9kZCIgY2xpcC1ydWxlPSJldmVub2RkIiBkPSJNNjYuNDAwMSAxMS40OTg0QzY2LjQwMDEgOS4xOTg0NCA2NS4xMDAxIDguMDk4NDQgNjIuNTAwMSA4LjA5ODQ0QzU5LjgwMDEgOC4wOTg0NCA1OC43MDAxIDkuMDk4NDQgNTguNzAwMSAxMS40OTg0QzU4LjcwMDEgMTMuNzk4NCA1OS44MDAxIDE0Ljg5ODQgNjIuNTAwMSAxNC44OTg0QzY1LjEwMDEgMTQuODk4NCA2Ni40MDAxIDEzLjc5ODQgNjYuNDAwMSAxMS40OTg0Wk02Ni4zMDAxIDUuNTk4NDRINjkuNDAwMVYxNy4zOTg0SDY2LjMwMDFWMTQuNDk4NEM2NS40MDAxIDE2LjQ5ODQgNjMuNzAwMSAxNy42OTg0IDYxLjYwMDEgMTcuNjk4NEM1Ny43MDAxIDE3LjY5ODQgNTUuNjAwMSAxNS4yOTg0IDU1LjYwMDEgMTEuNTk4NEM1NS42MDAxIDcuNzk4NDQgNTcuNjAwMSA1LjM5ODQ0IDYxLjQwMDEgNS4zOTg0NEM2My41MDAxIDUuMzk4NDQgNjUuNDAwMSA2LjU5ODQ0IDY2LjIwMDEgOC41OTg0NFY1LjU5ODQ0SDY2LjMwMDFaIiBmaWxsPSJ3aGl0ZSIvPgo8cGF0aCBmaWxsLXJ1bGU9ImV2ZW5vZGQiIGNsaXAtcnVsZT0iZXZlbm9kZCIgZD0iTTgxLjM5OTkgOC4zSDc3LjM5OTlWMTQuN0g4MS4wOTk5VjE3LjRINzcuOTk5OUM3NS43OTk5IDE3LjQgNzQuMjk5OSAxNS45IDc0LjI5OTkgMTMuN1Y4LjNINzEuMzk5OVY1LjZINzQuMjk5OVYySDc3LjM5OTlWNS41SDgxLjM5OTlWOC4zWiIgZmlsbD0id2hpdGUiLz4KPHBhdGggZmlsbC1ydWxlPSJldmVub2RkIiBjbGlwLXJ1bGU9ImV2ZW5vZGQiIGQ9Ik05My42OTk5IDEyLjIwMDhMOTYuNjk5OSAxMi45MDA4Qzk1Ljk5OTkgMTYuMDAwOCA5My42OTk5IDE3LjYwMDggODkuOTk5OSAxNy42MDA4Qzg3Ljc5OTkgMTcuNjAwOCA4Ni4wOTk5IDE3LjAwMDggODQuNzk5OSAxNS45MDA4QzgzLjQ5OTkgMTQuODAwOCA4Mi44OTk5IDEzLjMwMDggODIuODk5OSAxMS41MDA4QzgyLjg5OTkgOS42MDA3OCA4My40OTk5IDguMTAwNzggODQuNzk5OSA3LjAwMDc4Qzg2LjA5OTkgNS45MDA3OCA4Ny43OTk5IDUuMzAwNzggODkuOTk5OSA1LjMwMDc4QzkzLjY5OTkgNS4zMDA3OCA5NS45OTk5IDYuOTAwNzggOTYuNjk5OSAxMC4wMDA4TDkzLjY5OTkgMTAuNzAwOEM5My4yOTk5IDguODAwNzggOTIuMjk5OSA4LjEwMDc4IDg5Ljg5OTkgOC4xMDA3OEM4Ni45OTk5IDguMTAwNzggODUuODk5OSA5LjIwMDc4IDg1Ljg5OTkgMTEuNTAwOEM4NS44OTk5IDEzLjgwMDggODYuOTk5OSAxNC45MDA4IDg5Ljg5OTkgMTQuOTAwOEM5Mi4yOTk5IDE0LjkwMDggOTMuMjk5OSAxNC4yMDA4IDkzLjY5OTkgMTIuMjAwOFoiIGZpbGw9IndoaXRlIi8+CjxwYXRoIGZpbGwtcnVsZT0iZXZlbm9kZCIgY2xpcC1ydWxlPSJldmVub2RkIiBkPSJNMTExLjggMTAuNlYxNy40SDEwOC43VjExLjJDMTA4LjcgOSAxMDcuOCA4LjEgMTA1LjkgOC4xQzEwMy42IDguMSAxMDIuNCA5LjMgMTAyLjQgMTEuN1YxNy4zSDk5LjI5OThWMC41SDEwMi40VjguM0MxMDMuMSA2LjMgMTA0LjYgNS4zIDEwNyA1LjNDMTEwLjIgNS4zIDExMS44IDcuMSAxMTEuOCAxMC42WiIgZmlsbD0id2hpdGUiLz4KPHBhdGggZmlsbC1ydWxlPSJldmVub2RkIiBjbGlwLXJ1bGU9ImV2ZW5vZGQiIGQ9Ik0xMTQuMSAxNC4wOTg0TDExNy4xIDEzLjI5ODRDMTE3LjQgMTQuNzk4NCAxMTguMyAxNS4yOTg0IDEyMC4yIDE1LjI5ODRDMTIxLjggMTUuMjk4NCAxMjIuNyAxNC44OTg0IDEyMi43IDE0LjA5ODRDMTIyLjcgMTMuMjk4NCAxMjIuMiAxMi45OTg0IDEyMC42IDEyLjc5ODRMMTE4LjIgMTIuNDk4NEMxMTUuOSAxMi4xOTg0IDExNC41IDEwLjg5ODQgMTE0LjUgOC45OTg0NEMxMTQuNSA2Ljc5ODQ0IDExNi41IDUuMzk4NDQgMTE5LjggNS4zOTg0NEMxMjMuMSA1LjM5ODQ0IDEyNSA2LjY5ODQ0IDEyNS41IDguODk4NDRMMTIyLjUgOS41OTg0NEMxMjIuMyA4LjA5ODQ0IDEyMS41IDcuNjk4NDQgMTE5LjkgNy42OTg0NEMxMTguNCA3LjY5ODQ0IDExNy42IDguMDk4NDQgMTE3LjYgOC44OTg0NEMxMTcuNiA5LjU5ODQ0IDExOC4xIDkuOTk4NDQgMTE5LjMgMTAuMDk4NEwxMjIuMiAxMC40OTg0QzEyNC40IDEwLjc5ODQgMTI1LjggMTEuOTk4NCAxMjUuOCAxMy44OTg0QzEyNS44IDE2LjI5ODQgMTIzLjcgMTcuNzk4NCAxMjAuMSAxNy43OTg0QzExNi43IDE3LjU5ODQgMTE0LjUgMTYuMTk4NCAxMTQuMSAxNC4wOTg0WiIgZmlsbD0id2hpdGUiLz4KPHBhdGggZmlsbC1ydWxlPSJldmVub2RkIiBjbGlwLXJ1bGU9ImV2ZW5vZGQiIGQ9Ik0xMzYuNyA4LjNIMTMyLjdWMTQuN0gxMzYuNFYxNy40SDEzMy4zQzEzMS4xIDE3LjQgMTI5LjYgMTUuOSAxMjkuNiAxMy43VjguM0gxMjYuN1Y1LjZIMTI5LjZWMkgxMzIuN1Y1LjVIMTM2LjdWOC4zWiIgZmlsbD0id2hpdGUiLz4KPHBhdGggZmlsbC1ydWxlPSJldmVub2RkIiBjbGlwLXJ1bGU9ImV2ZW5vZGQiIGQ9Ik0xNDkuMiAxMS40OTg0QzE0OS4yIDkuMTk4NDQgMTQ3LjkgOC4wOTg0NCAxNDUuMyA4LjA5ODQ0QzE0Mi42IDguMDk4NDQgMTQxLjUgOS4wOTg0NCAxNDEuNSAxMS40OTg0QzE0MS41IDEzLjc5ODQgMTQyLjYgMTQuODk4NCAxNDUuMyAxNC44OTg0QzE0Ny45IDE0Ljg5ODQgMTQ5LjIgMTMuNzk4NCAxNDkuMiAxMS40OTg0Wk0xNDkuMSA1LjU5ODQ0SDE1Mi4yVjE3LjM5ODRIMTQ5LjFWMTQuNDk4NEMxNDguMiAxNi40OTg0IDE0Ni41IDE3LjY5ODQgMTQ0LjQgMTcuNjk4NEMxNDAuNSAxNy42OTg0IDEzOC40IDE1LjI5ODQgMTM4LjQgMTEuNTk4NEMxMzguNCA3Ljc5ODQ0IDE0MC40IDUuMzk4NDQgMTQ0LjIgNS4zOTg0NEMxNDYuMyA1LjM5ODQ0IDE0OC4yIDYuNTk4NDQgMTQ5IDguNTk4NDRWNS41OTg0NEgxNDkuMVoiIGZpbGw9IndoaXRlIi8+CjxwYXRoIGZpbGwtcnVsZT0iZXZlbm9kZCIgY2xpcC1ydWxlPSJldmVub2RkIiBkPSJNMTY1LjYgMTIuMjAwOEwxNjguNiAxMi45MDA4QzE2Ny45IDE2LjAwMDggMTY1LjYgMTcuNjAwOCAxNjEuOSAxNy42MDA4QzE1OS43IDE3LjYwMDggMTU4IDE3LjAwMDggMTU2LjcgMTUuOTAwOEMxNTUuNCAxNC44MDA4IDE1NC44IDEzLjMwMDggMTU0LjggMTEuNTAwOEMxNTQuOCA5LjYwMDc4IDE1NS40IDguMTAwNzggMTU2LjcgNy4wMDA3OEMxNTggNS45MDA3OCAxNTkuNyA1LjMwMDc4IDE2MS45IDUuMzAwNzhDMTY1LjYgNS4zMDA3OCAxNjcuOSA2LjkwMDc4IDE2OC42IDEwLjAwMDhMMTY1LjYgMTAuNzAwOEMxNjUuMiA4LjgwMDc4IDE2NC4yIDguMTAwNzggMTYxLjggOC4xMDA3OEMxNTguOSA4LjEwMDc4IDE1Ny44IDkuMjAwNzggMTU3LjggMTEuNTAwOEMxNTcuOCAxMy44MDA4IDE1OC45IDE0LjkwMDggMTYxLjggMTQuOTAwOEMxNjQuMSAxNC45MDA4IDE2NS4xIDE0LjIwMDggMTY1LjYgMTIuMjAwOFoiIGZpbGw9IndoaXRlIi8+CjxwYXRoIGZpbGwtcnVsZT0iZXZlbm9kZCIgY2xpcC1ydWxlPSJldmVub2RkIiBkPSJNMTgwLjI1NyAxNy40TDE3Ni42NTcgMTIuM0wxNzQuMTU3IDE0LjZWMTcuNEgxNzEuMTU3VjAuNUgxNzQuMTU3VjExLjFMMTc5Ljg1NyA1LjZIMTgzLjc1N0wxNzguNTU3IDEwLjVMMTgzLjk1NyAxNy40SDE4MC4yNTdaIiBmaWxsPSJ3aGl0ZSIvPgo8cGF0aCBmaWxsLXJ1bGU9ImV2ZW5vZGQiIGNsaXAtcnVsZT0iZXZlbm9kZCIgZD0iTTAgMjEuNTAwOEg3LjNWMTYuMzAwOEgwVjIxLjUwMDhaIiBmaWxsPSIjQUZFNjE0Ii8+CjxwYXRoIGZpbGwtcnVsZT0iZXZlbm9kZCIgY2xpcC1ydWxlPSJldmVub2RkIiBkPSJNNy4yOTk4IDIxLjUwMDhIMTQuNTk5OFYxNi4zMDA4SDcuMjk5OFYyMS41MDA4WiIgZmlsbD0iIzZFQkUwMCIvPgo8cGF0aCBmaWxsLXJ1bGU9ImV2ZW5vZGQiIGNsaXAtcnVsZT0iZXZlbm9kZCIgZD0iTTE0LjYwMDEgMTYuM0gyOS4yMDAxVjAuNUgxNC42MDAxVjE2LjNaIiBmaWxsPSIjQUZFNjE0Ii8+CjxwYXRoIGZpbGwtcnVsZT0iZXZlbm9kZCIgY2xpcC1ydWxlPSJldmVub2RkIiBkPSJNMCAxMUgxNC42VjAuNUgwVjExWiIgZmlsbD0iIzZFQkUwMCIvPgo8L3N2Zz4K" alt="Patchstack Logo">
|
|
3617
|
-
</a>
|
|
3618
|
-
</div>
|
|
3619
|
-
</div>
|
|
3620
|
-
|
|
3621
|
-
<div class="mobile-bottom-container">
|
|
3622
|
-
<div class="mobile-bottom-text-containers">
|
|
3623
|
-
<img class="arrow-vector" src="data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iOCIgaGVpZ2h0PSIxMyIgdmlld0JveD0iMCAwIDggMTMiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxwYXRoIGQ9Ik03LjQxIDEuNDMzNDRMNiAwLjAyMzQzNzVMMCA2LjAyMzQ0TDYgMTIuMDIzNEw3LjQxIDEwLjYxMzRMMi44MyA2LjAyMzQ0TDcuNDEgMS40MzM0NFoiIGZpbGw9IiNBRkU2MTQiLz4KPC9zdmc+Cg==" alt="Arrow Left">
|
|
3624
|
-
<a href="/" class="return-text">Return to homepage</a>
|
|
3625
|
-
</div>
|
|
3626
|
-
<div class="mobile-bottom-second-containers">
|
|
3627
|
-
<span class="secured-text">This website is secured by</span>
|
|
3628
|
-
<a href="https://patchstack.com" class="logoText">
|
|
3629
|
-
<img class="patch-logo" src="data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTg0IiBoZWlnaHQ9IjIyIiB2aWV3Qm94PSIwIDAgMTg0IDIyIiBmaWxsPSJub25lIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPgo8cGF0aCBmaWxsLXJ1bGU9ImV2ZW5vZGQiIGNsaXAtcnVsZT0iZXZlbm9kZCIgZD0iTTUwLjQwMDEgMTEuNTAwOEM1MC40MDAxIDkuMTAwNzggNDkuMzAwMSA4LjEwMDc4IDQ2LjYwMDEgOC4xMDA3OEM0NC4wMDAxIDguMTAwNzggNDIuNzAwMSA5LjIwMDc4IDQyLjcwMDEgMTEuNTAwOEM0Mi43MDAxIDEzLjgwMDggNDQuMDAwMSAxNC45MDA4IDQ2LjYwMDEgMTQuOTAwOEM0OS4zMDAxIDE0LjkwMDggNTAuNDAwMSAxMy44MDA4IDUwLjQwMDEgMTEuNTAwOFpNNTMuNDAwMSAxMS41MDA4QzUzLjQwMDEgMTUuMzAwOCA1MS4zMDAxIDE3LjYwMDggNDcuNDAwMSAxNy42MDA4QzQ1LjQwMDEgMTcuNjAwOCA0My42MDAxIDE2LjUwMDggNDIuNzAwMSAxNC41MDA4VjIxLjUwMDhIMzkuNjAwMVY1LjYwMDc4SDQyLjcwMDFWOC41MDA3OEM0My42MDAxIDYuNTAwNzggNDUuNDAwMSA1LjMwMDc4IDQ3LjUwMDEgNS4zMDA3OEM1MS40MDAxIDUuMzAwNzggNTMuNDAwMSA3LjcwMDc4IDUzLjQwMDEgMTEuNTAwOFoiIGZpbGw9IndoaXRlIi8+CjxwYXRoIGZpbGwtcnVsZT0iZXZlbm9kZCIgY2xpcC1ydWxlPSJldmVub2RkIiBkPSJNNjYuNDAwMSAxMS40OTg0QzY2LjQwMDEgOS4xOTg0NCA2NS4xMDAxIDguMDk4NDQgNjIuNTAwMSA4LjA5ODQ0QzU5LjgwMDEgOC4wOTg0NCA1OC43MDAxIDkuMDk4NDQgNTguNzAwMSAxMS40OTg0QzU4LjcwMDEgMTMuNzk4NCA1OS44MDAxIDE0Ljg5ODQgNjIuNTAwMSAxNC44OTg0QzY1LjEwMDEgMTQuODk4NCA2Ni40MDAxIDEzLjc5ODQgNjYuNDAwMSAxMS40OTg0Wk02Ni4zMDAxIDUuNTk4NDRINjkuNDAwMVYxNy4zOTg0SDY2LjMwMDFWMTQuNDk4NEM2NS40MDAxIDE2LjQ5ODQgNjMuNzAwMSAxNy42OTg0IDYxLjYwMDEgMTcuNjk4NEM1Ny43MDAxIDE3LjY5ODQgNTUuNjAwMSAxNS4yOTg0IDU1LjYwMDEgMTEuNTk4NEM1NS42MDAxIDcuNzk4NDQgNTcuNjAwMSA1LjM5ODQ0IDYxLjQwMDEgNS4zOTg0NEM2My41MDAxIDUuMzk4NDQgNjUuNDAwMSA2LjU5ODQ0IDY2LjIwMDEgOC41OTg0NFY1LjU5ODQ0SDY2LjMwMDFaIiBmaWxsPSJ3aGl0ZSIvPgo8cGF0aCBmaWxsLXJ1bGU9ImV2ZW5vZGQiIGNsaXAtcnVsZT0iZXZlbm9kZCIgZD0iTTgxLjM5OTkgOC4zSDc3LjM5OTlWMTQuN0g4MS4wOTk5VjE3LjRINzcuOTk5OUM3NS43OTk5IDE3LjQgNzQuMjk5OSAxNS45IDc0LjI5OTkgMTMuN1Y4LjNINzEuMzk5OVY1LjZINzQuMjk5OVYySDc3LjM5OTlWNS41SDgxLjM5OTlWOC4zWiIgZmlsbD0id2hpdGUiLz4KPHBhdGggZmlsbC1ydWxlPSJldmVub2RkIiBjbGlwLXJ1bGU9ImV2ZW5vZGQiIGQ9Ik05My42OTk5IDEyLjIwMDhMOTYuNjk5OSAxMi45MDA4Qzk1Ljk5OTkgMTYuMDAwOCA5My42OTk5IDE3LjYwMDggODkuOTk5OSAxNy42MDA4Qzg3Ljc5OTkgMTcuNjAwOCA4Ni4wOTk5IDE3LjAwMDggODQuNzk5OSAxNS45MDA4QzgzLjQ5OTkgMTQuODAwOCA4Mi44OTk5IDEzLjMwMDggODIuODk5OSAxMS41MDA4QzgyLjg5OTkgOS42MDA3OCA4My40OTk5IDguMTAwNzggODQuNzk5OSA3LjAwMDc4Qzg2LjA5OTkgNS45MDA3OCA4Ny43OTk5IDUuMzAwNzggODkuOTk5OSA1LjMwMDc4QzkzLjY5OTkgNS4zMDA3OCA5NS45OTk5IDYuOTAwNzggOTYuNjk5OSAxMC4wMDA4TDkzLjY5OTkgMTAuNzAwOEM5My4yOTk5IDguODAwNzggOTIuMjk5OSA4LjEwMDc4IDg5Ljg5OTkgOC4xMDA3OEM4Ni45OTk5IDguMTAwNzggODUuODk5OSA5LjIwMDc4IDg1Ljg5OTkgMTEuNTAwOEM4NS44OTk5IDEzLjgwMDggODYuOTk5OSAxNC45MDA4IDg5Ljg5OTkgMTQuOTAwOEM5Mi4yOTk5IDE0LjkwMDggOTMuMjk5OSAxNC4yMDA4IDkzLjY5OTkgMTIuMjAwOFoiIGZpbGw9IndoaXRlIi8+CjxwYXRoIGZpbGwtcnVsZT0iZXZlbm9kZCIgY2xpcC1ydWxlPSJldmVub2RkIiBkPSJNMTExLjggMTAuNlYxNy40SDEwOC43VjExLjJDMTA4LjcgOSAxMDcuOCA4LjEgMTA1LjkgOC4xQzEwMy42IDguMSAxMDIuNCA5LjMgMTAyLjQgMTEuN1YxNy4zSDk5LjI5OThWMC41SDEwMi40VjguM0MxMDMuMSA2LjMgMTA0LjYgNS4zIDEwNyA1LjNDMTEwLjIgNS4zIDExMS44IDcuMSAxMTEuOCAxMC42WiIgZmlsbD0id2hpdGUiLz4KPHBhdGggZmlsbC1ydWxlPSJldmVub2RkIiBjbGlwLXJ1bGU9ImV2ZW5vZGQiIGQ9Ik0xMTQuMSAxNC4wOTg0TDExNy4xIDEzLjI5ODRDMTE3LjQgMTQuNzk4NCAxMTguMyAxNS4yOTg0IDEyMC4yIDE1LjI5ODRDMTIxLjggMTUuMjk4NCAxMjIuNyAxNC44OTg0IDEyMi43IDE0LjA5ODRDMTIyLjcgMTMuMjk4NCAxMjIuMiAxMi45OTg0IDEyMC42IDEyLjc5ODRMMTE4LjIgMTIuNDk4NEMxMTUuOSAxMi4xOTg0IDExNC41IDEwLjg5ODQgMTE0LjUgOC45OTg0NEMxMTQuNSA2Ljc5ODQ0IDExNi41IDUuMzk4NDQgMTE5LjggNS4zOTg0NEMxMjMuMSA1LjM5ODQ0IDEyNSA2LjY5ODQ0IDEyNS41IDguODk4NDRMMTIyLjUgOS41OTg0NEMxMjIuMyA4LjA5ODQ0IDEyMS41IDcuNjk4NDQgMTE5LjkgNy42OTg0NEMxMTguNCA3LjY5ODQ0IDExNy42IDguMDk4NDQgMTE3LjYgOC44OTg0NEMxMTcuNiA5LjU5ODQ0IDExOC4xIDkuOTk4NDQgMTE5LjMgMTAuMDk4NEwxMjIuMiAxMC40OTg0QzEyNC40IDEwLjc5ODQgMTI1LjggMTEuOTk4NCAxMjUuOCAxMy44OTg0QzEyNS44IDE2LjI5ODQgMTIzLjcgMTcuNzk4NCAxMjAuMSAxNy43OTg0QzExNi43IDE3LjU5ODQgMTE0LjUgMTYuMTk4NCAxMTQuMSAxNC4wOTg0WiIgZmlsbD0id2hpdGUiLz4KPHBhdGggZmlsbC1ydWxlPSJldmVub2RkIiBjbGlwLXJ1bGU9ImV2ZW5vZGQiIGQ9Ik0xMzYuNyA4LjNIMTMyLjdWMTQuN0gxMzYuNFYxNy40SDEzMy4zQzEzMS4xIDE3LjQgMTI5LjYgMTUuOSAxMjkuNiAxMy43VjguM0gxMjYuN1Y1LjZIMTI5LjZWMkgxMzIuN1Y1LjVIMTM2LjdWOC4zWiIgZmlsbD0id2hpdGUiLz4KPHBhdGggZmlsbC1ydWxlPSJldmVub2RkIiBjbGlwLXJ1bGU9ImV2ZW5vZGQiIGQ9Ik0xNDkuMiAxMS40OTg0QzE0OS4yIDkuMTk4NDQgMTQ3LjkgOC4wOTg0NCAxNDUuMyA4LjA5ODQ0QzE0Mi42IDguMDk4NDQgMTQxLjUgOS4wOTg0NCAxNDEuNSAxMS40OTg0QzE0MS41IDEzLjc5ODQgMTQyLjYgMTQuODk4NCAxNDUuMyAxNC44OTg0QzE0Ny45IDE0Ljg5ODQgMTQ5LjIgMTMuNzk4NCAxNDkuMiAxMS40OTg0Wk0xNDkuMSA1LjU5ODQ0SDE1Mi4yVjE3LjM5ODRIMTQ5LjFWMTQuNDk4NEMxNDguMiAxNi40OTg0IDE0Ni41IDE3LjY5ODQgMTQ0LjQgMTcuNjk4NEMxNDAuNSAxNy42OTg0IDEzOC40IDE1LjI5ODQgMTM4LjQgMTEuNTk4NEMxMzguNCA3Ljc5ODQ0IDE0MC40IDUuMzk4NDQgMTQ0LjIgNS4zOTg0NEMxNDYuMyA1LjM5ODQ0IDE0OC4yIDYuNTk4NDQgMTQ5IDguNTk4NDRWNS41OTg0NEgxNDkuMVoiIGZpbGw9IndoaXRlIi8+CjxwYXRoIGZpbGwtcnVsZT0iZXZlbm9kZCIgY2xpcC1ydWxlPSJldmVub2RkIiBkPSJNMTY1LjYgMTIuMjAwOEwxNjguNiAxMi45MDA4QzE2Ny45IDE2LjAwMDggMTY1LjYgMTcuNjAwOCAxNjEuOSAxNy42MDA4QzE1OS43IDE3LjYwMDggMTU4IDE3LjAwMDggMTU2LjcgMTUuOTAwOEMxNTUuNCAxNC44MDA4IDE1NC44IDEzLjMwMDggMTU0LjggMTEuNTAwOEMxNTQuOCA5LjYwMDc4IDE1NS40IDguMTAwNzggMTU2LjcgNy4wMDA3OEMxNTggNS45MDA3OCAxNTkuNyA1LjMwMDc4IDE2MS45IDUuMzAwNzhDMTY1LjYgNS4zMDA3OCAxNjcuOSA2LjkwMDc4IDE2OC42IDEwLjAwMDhMMTY1LjYgMTAuNzAwOEMxNjUuMiA4LjgwMDc4IDE2NC4yIDguMTAwNzggMTYxLjggOC4xMDA3OEMxNTguOSA4LjEwMDc4IDE1Ny44IDkuMjAwNzggMTU3LjggMTEuNTAwOEMxNTcuOCAxMy44MDA4IDE1OC45IDE0LjkwMDggMTYxLjggMTQuOTAwOEMxNjQuMSAxNC45MDA4IDE2NS4xIDE0LjIwMDggMTY1LjYgMTIuMjAwOFoiIGZpbGw9IndoaXRlIi8+CjxwYXRoIGZpbGwtcnVsZT0iZXZlbm9kZCIgY2xpcC1ydWxlPSJldmVub2RkIiBkPSJNMTgwLjI1NyAxNy40TDE3Ni42NTcgMTIuM0wxNzQuMTU3IDE0LjZWMTcuNEgxNzEuMTU3VjAuNUgxNzQuMTU3VjExLjFMMTc5Ljg1NyA1LjZIMTgzLjc1N0wxNzguNTU3IDEwLjVMMTgzLjk1NyAxNy40SDE4MC4yNTdaIiBmaWxsPSJ3aGl0ZSIvPgo8cGF0aCBmaWxsLXJ1bGU9ImV2ZW5vZGQiIGNsaXAtcnVsZT0iZXZlbm9kZCIgZD0iTTAgMjEuNTAwOEg3LjNWMTYuMzAwOEgwVjIxLjUwMDhaIiBmaWxsPSIjQUZFNjE0Ii8+CjxwYXRoIGZpbGwtcnVsZT0iZXZlbm9kZCIgY2xpcC1ydWxlPSJldmVub2RkIiBkPSJNNy4yOTk4IDIxLjUwMDhIMTQuNTk5OFYxNi4zMDA4SDcuMjk5OFYyMS41MDA4WiIgZmlsbD0iIzZFQkUwMCIvPgo8cGF0aCBmaWxsLXJ1bGU9ImV2ZW5vZGQiIGNsaXAtcnVsZT0iZXZlbm9kZCIgZD0iTTE0LjYwMDEgMTYuM0gyOS4yMDAxVjAuNUgxNC42MDAxVjE2LjNaIiBmaWxsPSIjQUZFNjE0Ii8+CjxwYXRoIGZpbGwtcnVsZT0iZXZlbm9kZCIgY2xpcC1ydWxlPSJldmVub2RkIiBkPSJNMCAxMUgxNC42VjAuNUgwVjExWiIgZmlsbD0iIzZFQkUwMCIvPgo8L3N2Zz4K" alt="Patchstack Logo">
|
|
3630
|
-
</a>
|
|
3631
|
-
</div>
|
|
3632
|
-
</div>
|
|
3633
|
-
</div>
|
|
3634
|
-
</body>
|
|
3635
|
-
<script>
|
|
3636
|
-
function copyText(id) {
|
|
3637
|
-
var copyText = document.getElementById(id)
|
|
3638
|
-
copyText.select();
|
|
3639
|
-
copyText.setSelectionRange(0, 99999);
|
|
3640
|
-
navigator.clipboard.writeText(copyText.value);
|
|
3641
|
-
}
|
|
3642
4081
|
|
|
3643
|
-
|
|
3644
|
-
|
|
3645
|
-
|
|
3646
|
-
|
|
3647
|
-
|
|
3648
|
-
|
|
4082
|
+
// src/protect/engine/fetch.js
|
|
4083
|
+
var MAX_BODY_BYTES = 1024 * 1024;
|
|
4084
|
+
async function fromFetchRequest(request, options = {}) {
|
|
4085
|
+
const maxBodyBytes = options.maxBodyBytes ?? MAX_BODY_BYTES;
|
|
4086
|
+
const url = new URL(request.url);
|
|
4087
|
+
const method = (request.method || "GET").toUpperCase();
|
|
4088
|
+
const headers = {};
|
|
4089
|
+
request.headers.forEach((value, key) => {
|
|
4090
|
+
headers[key.toLowerCase()] = value;
|
|
4091
|
+
});
|
|
4092
|
+
const query = {};
|
|
4093
|
+
for (const [key, value] of url.searchParams) {
|
|
4094
|
+
if (key in query) {
|
|
4095
|
+
query[key] = Array.isArray(query[key]) ? [...query[key], value] : [query[key], value];
|
|
4096
|
+
} else {
|
|
4097
|
+
query[key] = value;
|
|
3649
4098
|
}
|
|
3650
|
-
|
|
3651
|
-
|
|
4099
|
+
}
|
|
4100
|
+
let rawBody = "";
|
|
4101
|
+
if (method !== "GET" && method !== "HEAD") {
|
|
4102
|
+
rawBody = await readCappedText(request, maxBodyBytes);
|
|
4103
|
+
}
|
|
4104
|
+
const contentType = headers["content-type"] || "";
|
|
4105
|
+
let body = {};
|
|
4106
|
+
let files;
|
|
4107
|
+
if (rawBody) {
|
|
4108
|
+
const parsed = parseBody(rawBody, contentType);
|
|
4109
|
+
body = parsed.body;
|
|
4110
|
+
files = parsed.files;
|
|
4111
|
+
}
|
|
4112
|
+
const uri = url.pathname + url.search;
|
|
4113
|
+
const client = resolveClientIp({ peer: options.peer, headers, trustedProxy: options.trustedProxy });
|
|
4114
|
+
return {
|
|
4115
|
+
method,
|
|
4116
|
+
url: uri,
|
|
4117
|
+
originalUrl: uri,
|
|
4118
|
+
query,
|
|
4119
|
+
body,
|
|
4120
|
+
files,
|
|
4121
|
+
headers,
|
|
4122
|
+
// The resolved address, as an own property. Nothing downstream re-derives or falls back: a consumer
|
|
4123
|
+
// that guessed differently would attribute one request to two addresses.
|
|
4124
|
+
ip: client.ip ?? "",
|
|
4125
|
+
_clientIp: client,
|
|
4126
|
+
cookies: parseCookies(headers.cookie),
|
|
4127
|
+
// Verbatim body text: preserves literal keys (e.g. `__proto__`) that JSON.stringify
|
|
4128
|
+
// drops, so prototype-pollution rules on `raw` are robust.
|
|
4129
|
+
_rawBody: rawBody
|
|
4130
|
+
};
|
|
3652
4131
|
}
|
|
3653
|
-
|
|
3654
|
-
|
|
3655
|
-
var fsMod;
|
|
3656
|
-
async function loadFs() {
|
|
3657
|
-
if (fsMod !== void 0) return fsMod;
|
|
4132
|
+
async function readCappedText(request, max) {
|
|
4133
|
+
let clone;
|
|
3658
4134
|
try {
|
|
3659
|
-
|
|
3660
|
-
fsMod = {
|
|
3661
|
-
readFileSync: fs.readFileSync,
|
|
3662
|
-
writeFileSync: fs.writeFileSync,
|
|
3663
|
-
mkdirSync: fs.mkdirSync,
|
|
3664
|
-
join: path7.join
|
|
3665
|
-
};
|
|
4135
|
+
clone = request.clone();
|
|
3666
4136
|
} catch {
|
|
3667
|
-
|
|
4137
|
+
return "";
|
|
3668
4138
|
}
|
|
3669
|
-
|
|
3670
|
-
|
|
3671
|
-
|
|
3672
|
-
|
|
3673
|
-
|
|
3674
|
-
|
|
3675
|
-
|
|
3676
|
-
|
|
3677
|
-
|
|
3678
|
-
|
|
3679
|
-
|
|
3680
|
-
|
|
3681
|
-
|
|
3682
|
-
|
|
3683
|
-
|
|
4139
|
+
const body = clone.body;
|
|
4140
|
+
if (body && typeof body.getReader === "function") {
|
|
4141
|
+
const reader = body.getReader();
|
|
4142
|
+
const chunks = [];
|
|
4143
|
+
let buffered = 0;
|
|
4144
|
+
try {
|
|
4145
|
+
for (; ; ) {
|
|
4146
|
+
const { done, value } = await reader.read();
|
|
4147
|
+
if (done) break;
|
|
4148
|
+
if (!value || buffered >= max) continue;
|
|
4149
|
+
const take = Math.min(value.byteLength, max - buffered);
|
|
4150
|
+
chunks.push(take === value.byteLength ? value : value.subarray(0, take));
|
|
4151
|
+
buffered += take;
|
|
4152
|
+
}
|
|
4153
|
+
} catch {
|
|
4154
|
+
return "";
|
|
3684
4155
|
}
|
|
3685
|
-
|
|
4156
|
+
try {
|
|
4157
|
+
return new TextDecoder().decode(concatChunks(chunks, buffered));
|
|
4158
|
+
} catch {
|
|
4159
|
+
return "";
|
|
4160
|
+
}
|
|
4161
|
+
}
|
|
4162
|
+
try {
|
|
4163
|
+
const text = await clone.text();
|
|
4164
|
+
return text.length > max ? text.slice(0, max) : text;
|
|
4165
|
+
} catch {
|
|
4166
|
+
return "";
|
|
4167
|
+
}
|
|
3686
4168
|
}
|
|
3687
|
-
function
|
|
3688
|
-
const
|
|
3689
|
-
|
|
3690
|
-
|
|
3691
|
-
|
|
3692
|
-
|
|
3693
|
-
return toEnvelope(await adapter.read());
|
|
3694
|
-
} catch {
|
|
3695
|
-
return null;
|
|
3696
|
-
}
|
|
3697
|
-
},
|
|
3698
|
-
write: async (env) => {
|
|
3699
|
-
try {
|
|
3700
|
-
await adapter.write(env);
|
|
3701
|
-
} catch {
|
|
3702
|
-
}
|
|
3703
|
-
}
|
|
3704
|
-
};
|
|
4169
|
+
function concatChunks(chunks, total) {
|
|
4170
|
+
const out = new Uint8Array(total);
|
|
4171
|
+
let offset = 0;
|
|
4172
|
+
for (const c of chunks) {
|
|
4173
|
+
out.set(c, offset);
|
|
4174
|
+
offset += c.byteLength;
|
|
3705
4175
|
}
|
|
3706
|
-
|
|
3707
|
-
return {
|
|
3708
|
-
read: async () => cacheRead(dir),
|
|
3709
|
-
write: async (env) => cacheWrite(dir, env)
|
|
3710
|
-
};
|
|
4176
|
+
return out;
|
|
3711
4177
|
}
|
|
3712
|
-
|
|
3713
|
-
|
|
3714
|
-
const
|
|
3715
|
-
|
|
3716
|
-
|
|
3717
|
-
|
|
3718
|
-
|
|
3719
|
-
|
|
4178
|
+
function parseBody(rawBody, contentType) {
|
|
4179
|
+
const ct = String(contentType || "").toLowerCase();
|
|
4180
|
+
const isJson = ct.includes("application/json") || /\+json\b/.test(ct);
|
|
4181
|
+
const isForm = ct.includes("application/x-www-form-urlencoded");
|
|
4182
|
+
const isMultipart = ct.includes("multipart/form-data");
|
|
4183
|
+
const isAmbiguous = ct === "" || ct.startsWith("text/plain") || ct.includes("csp-report") || ct.includes("/json");
|
|
4184
|
+
if (isMultipart) {
|
|
4185
|
+
const boundary = /boundary=("?)([^";]+)\1/i.exec(contentType)?.[2];
|
|
4186
|
+
if (boundary) return parseMultipart(rawBody, boundary);
|
|
4187
|
+
return { body: {}, files: void 0 };
|
|
4188
|
+
}
|
|
4189
|
+
if (isForm) {
|
|
4190
|
+
const body = {};
|
|
4191
|
+
for (const [k, v] of new URLSearchParams(rawBody)) body[k] = k in body ? [].concat(body[k], v) : v;
|
|
4192
|
+
return { body, files: void 0 };
|
|
3720
4193
|
}
|
|
4194
|
+
if (isJson || isAmbiguous) {
|
|
4195
|
+
try {
|
|
4196
|
+
const parsed = JSON.parse(rawBody);
|
|
4197
|
+
if (parsed && typeof parsed === "object") return { body: parsed, files: void 0 };
|
|
4198
|
+
} catch {
|
|
4199
|
+
}
|
|
4200
|
+
}
|
|
4201
|
+
return { body: {}, files: void 0 };
|
|
3721
4202
|
}
|
|
3722
|
-
|
|
3723
|
-
|
|
3724
|
-
const
|
|
3725
|
-
|
|
3726
|
-
|
|
3727
|
-
|
|
3728
|
-
|
|
3729
|
-
|
|
4203
|
+
function parseMultipart(rawBody, boundary) {
|
|
4204
|
+
const body = {};
|
|
4205
|
+
const files = {};
|
|
4206
|
+
for (const part of rawBody.split("--" + boundary)) {
|
|
4207
|
+
const sep = /\r?\n\r?\n/.exec(part);
|
|
4208
|
+
if (!sep) continue;
|
|
4209
|
+
const rawHeaders = part.slice(0, sep.index);
|
|
4210
|
+
const disposition = /content-disposition:[^\r\n]*/i.exec(rawHeaders)?.[0];
|
|
4211
|
+
if (!disposition) continue;
|
|
4212
|
+
const name = /name="([^"]*)"/i.exec(disposition)?.[1];
|
|
4213
|
+
if (name == null) continue;
|
|
4214
|
+
const content = part.slice(sep.index + sep[0].length).replace(/\r?\n$/, "");
|
|
4215
|
+
const filename = /filename="([^"]*)"/i.exec(disposition)?.[1];
|
|
4216
|
+
if (filename !== void 0) {
|
|
4217
|
+
const partType = /content-type:\s*([^\r\n;]+)/i.exec(rawHeaders)?.[1]?.trim() || "";
|
|
4218
|
+
const file = { filename, type: partType, content };
|
|
4219
|
+
files[name] = name in files ? [].concat(files[name], file) : file;
|
|
4220
|
+
} else {
|
|
4221
|
+
body[name] = name in body ? [].concat(body[name], content) : content;
|
|
4222
|
+
}
|
|
3730
4223
|
}
|
|
4224
|
+
return { body, files };
|
|
3731
4225
|
}
|
|
3732
|
-
function
|
|
3733
|
-
|
|
3734
|
-
if (
|
|
3735
|
-
return
|
|
4226
|
+
function parseCookies(header) {
|
|
4227
|
+
const cookies = {};
|
|
4228
|
+
if (!header) {
|
|
4229
|
+
return cookies;
|
|
3736
4230
|
}
|
|
3737
|
-
|
|
3738
|
-
|
|
4231
|
+
for (const pair of header.split(";")) {
|
|
4232
|
+
const idx = pair.indexOf("=");
|
|
4233
|
+
if (idx === -1) {
|
|
4234
|
+
continue;
|
|
4235
|
+
}
|
|
4236
|
+
cookies[pair.slice(0, idx).trim()] = pair.slice(idx + 1).trim();
|
|
3739
4237
|
}
|
|
3740
|
-
return
|
|
4238
|
+
return cookies;
|
|
3741
4239
|
}
|
|
3742
4240
|
|
|
3743
|
-
// src/protect/
|
|
3744
|
-
|
|
3745
|
-
|
|
3746
|
-
|
|
3747
|
-
|
|
3748
|
-
|
|
3749
|
-
|
|
3750
|
-
|
|
3751
|
-
|
|
3752
|
-
|
|
3753
|
-
|
|
3754
|
-
|
|
3755
|
-
|
|
3756
|
-
|
|
3757
|
-
|
|
3758
|
-
|
|
3759
|
-
optional_key_suffixes: Object.freeze(["content", "filename", "type"])
|
|
3760
|
-
}),
|
|
3761
|
-
server: Object.freeze({
|
|
3762
|
-
keyed: true,
|
|
3763
|
-
keys: Object.freeze([
|
|
3764
|
-
"REQUEST_URI",
|
|
3765
|
-
"REQUEST_METHOD",
|
|
3766
|
-
"HTTP_USER_AGENT",
|
|
3767
|
-
"HTTP_REFERER",
|
|
3768
|
-
"HTTP_HOST",
|
|
3769
|
-
"REMOTE_ADDR",
|
|
3770
|
-
"ip",
|
|
3771
|
-
"CONTENT_TYPE",
|
|
3772
|
-
"CONTENT_LENGTH"
|
|
3773
|
-
]),
|
|
3774
|
-
key_prefixes: Object.freeze(["HTTP_"])
|
|
3775
|
-
}),
|
|
3776
|
-
response: Object.freeze({
|
|
3777
|
-
keyed: true,
|
|
3778
|
-
keys: Object.freeze(["status", "body", "headers"]),
|
|
3779
|
-
key_prefixes: Object.freeze(["header."])
|
|
3780
|
-
}),
|
|
3781
|
-
egress: Object.freeze({ keyed: true, keys: Object.freeze(["url", "host", "method"]) })
|
|
3782
|
-
});
|
|
3783
|
-
var MATCH_TYPES = Object.freeze([
|
|
3784
|
-
"array_in_array",
|
|
3785
|
-
"array_key_value",
|
|
3786
|
-
"contains",
|
|
3787
|
-
"cors_reflected",
|
|
3788
|
-
"cross_origin",
|
|
3789
|
-
"ctype_alnum",
|
|
3790
|
-
"ctype_digit",
|
|
3791
|
-
"ctype_special",
|
|
3792
|
-
"equals",
|
|
3793
|
-
"equals_strict",
|
|
3794
|
-
"hostname",
|
|
3795
|
-
"in_array",
|
|
3796
|
-
"inline_js_xss",
|
|
3797
|
-
"inline_xss",
|
|
3798
|
-
"internal_host",
|
|
3799
|
-
"is_numeric",
|
|
3800
|
-
"isset",
|
|
3801
|
-
"less_than",
|
|
3802
|
-
"more_than",
|
|
3803
|
-
"not_contains",
|
|
3804
|
-
"not_in_array",
|
|
3805
|
-
"off_origin",
|
|
3806
|
-
"quotes",
|
|
3807
|
-
"regex",
|
|
3808
|
-
"stripos"
|
|
3809
|
-
]);
|
|
3810
|
-
var PARAMETERLESS_MATCH_TYPES = Object.freeze(["cross_origin", "off_origin", "cors_reflected"]);
|
|
3811
|
-
var OPERANDLESS_MATCH_TYPES = Object.freeze([
|
|
3812
|
-
"isset",
|
|
3813
|
-
"quotes",
|
|
3814
|
-
"is_numeric",
|
|
3815
|
-
"ctype_alnum",
|
|
3816
|
-
"ctype_digit",
|
|
3817
|
-
"ctype_special",
|
|
3818
|
-
"inline_xss",
|
|
3819
|
-
"inline_js_xss",
|
|
3820
|
-
"internal_host",
|
|
3821
|
-
"cross_origin",
|
|
3822
|
-
"off_origin",
|
|
3823
|
-
"cors_reflected"
|
|
3824
|
-
]);
|
|
3825
|
-
var OPERAND_SHAPES = Object.freeze([
|
|
3826
|
-
"non-empty-string",
|
|
3827
|
-
"regex-literal",
|
|
3828
|
-
"string-or-string-list",
|
|
3829
|
-
"scalar",
|
|
3830
|
-
"string",
|
|
3831
|
-
"numeric",
|
|
3832
|
-
"scalar-or-non-empty-list",
|
|
3833
|
-
"non-empty-list",
|
|
3834
|
-
"object",
|
|
3835
|
-
"non-empty-object",
|
|
3836
|
-
"non-empty-string-list",
|
|
3837
|
-
"header-name-map",
|
|
3838
|
-
"header-name-list",
|
|
3839
|
-
"cookie-flags"
|
|
3840
|
-
]);
|
|
3841
|
-
var COOKIE_FLAGS = Object.freeze(["httpOnly", "secure", "sameSite"]);
|
|
3842
|
-
var SAME_SITE_VALUES = Object.freeze(["Strict", "Lax", "None"]);
|
|
3843
|
-
var HEADER_NAME = /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/;
|
|
3844
|
-
var REGEX_LITERAL = /^\/(.+?)\/([gimsuy]*)$/s;
|
|
3845
|
-
var MATCH_OPERANDS = Object.freeze({
|
|
3846
|
-
array_key_value: Object.freeze({
|
|
3847
|
-
required: Object.freeze(["key", "match"]),
|
|
3848
|
-
shapes: Object.freeze({ key: "string-or-string-list", match: "object" })
|
|
3849
|
-
}),
|
|
3850
|
-
regex: Object.freeze({ required: Object.freeze(["value"]), shapes: Object.freeze({ value: "regex-literal" }) }),
|
|
3851
|
-
contains: Object.freeze({ required: Object.freeze(["value"]), shapes: Object.freeze({ value: "non-empty-string" }) }),
|
|
3852
|
-
stripos: Object.freeze({ required: Object.freeze(["value"]), shapes: Object.freeze({ value: "non-empty-string" }) }),
|
|
3853
|
-
not_contains: Object.freeze({ required: Object.freeze(["value"]), shapes: Object.freeze({ value: "non-empty-string" }) }),
|
|
3854
|
-
// A hostname is compared to `new URL(value).hostname`, so an empty one only ever equals the empty
|
|
3855
|
-
// hostname of an opaque-scheme URL (`file:`, `mailto:`, `data:`) — never a host, and a false positive
|
|
3856
|
-
// on those. The other three are the list-membership family: an empty list makes `in_array` and
|
|
3857
|
-
// `array_in_array` inert and `not_in_array` true for every value.
|
|
3858
|
-
hostname: Object.freeze({ required: Object.freeze(["value"]), shapes: Object.freeze({ value: "non-empty-string" }) }),
|
|
3859
|
-
in_array: Object.freeze({ required: Object.freeze(["value"]), shapes: Object.freeze({ value: "scalar-or-non-empty-list" }) }),
|
|
3860
|
-
not_in_array: Object.freeze({ required: Object.freeze(["value"]), shapes: Object.freeze({ value: "scalar-or-non-empty-list" }) }),
|
|
3861
|
-
array_in_array: Object.freeze({ required: Object.freeze(["value"]), shapes: Object.freeze({ value: "non-empty-list" }) }),
|
|
3862
|
-
// The comparison family. `equals` coerces, so any scalar can match. `equals_strict` does not, and its
|
|
3863
|
-
// subject is always a string by the time it is compared — so a number operand is unconditionally
|
|
3864
|
-
// inert, which is the trap the engine's own note about `'1' !== 1` describes.
|
|
3865
|
-
equals: Object.freeze({ required: Object.freeze(["value"]), shapes: Object.freeze({ value: "scalar" }) }),
|
|
3866
|
-
equals_strict: Object.freeze({ required: Object.freeze(["value"]), shapes: Object.freeze({ value: "string" }) }),
|
|
3867
|
-
less_than: Object.freeze({ required: Object.freeze(["value"]), shapes: Object.freeze({ value: "numeric" }) }),
|
|
3868
|
-
more_than: Object.freeze({ required: Object.freeze(["value"]), shapes: Object.freeze({ value: "numeric" }) })
|
|
3869
|
-
});
|
|
3870
|
-
function operandShapeProblem(shape, value) {
|
|
3871
|
-
const isFilledString = (v) => typeof v === "string" && v.trim() !== "";
|
|
3872
|
-
const isScalar = (v) => typeof v === "string" || typeof v === "number" || typeof v === "boolean";
|
|
3873
|
-
const isHeaderName = (v) => typeof v === "string" && HEADER_NAME.test(v);
|
|
3874
|
-
switch (shape) {
|
|
3875
|
-
case "non-empty-string":
|
|
3876
|
-
return isFilledString(value) ? null : "must be a non-empty string";
|
|
3877
|
-
case "regex-literal":
|
|
3878
|
-
if (!isFilledString(value)) return "must be a non-empty string";
|
|
3879
|
-
return REGEX_LITERAL.test(value) ? null : "must be a delimited pattern, as in /pattern/i";
|
|
3880
|
-
case "string-or-string-list":
|
|
3881
|
-
if (isFilledString(value)) return null;
|
|
3882
|
-
if (Array.isArray(value) && value.length > 0 && value.every(isFilledString)) return null;
|
|
3883
|
-
return "must be a non-empty string, or a non-empty list of them";
|
|
3884
|
-
case "scalar":
|
|
3885
|
-
return isScalar(value) ? null : "must be a string, number or boolean";
|
|
3886
|
-
case "string":
|
|
3887
|
-
return typeof value === "string" ? null : "must be a string (this comparison is type-strict)";
|
|
3888
|
-
case "numeric":
|
|
3889
|
-
return isScalar(value) && Number.isFinite(Number(value)) ? null : "must be a number";
|
|
3890
|
-
case "scalar-or-non-empty-list":
|
|
3891
|
-
if (isScalar(value)) return null;
|
|
3892
|
-
if (Array.isArray(value) && value.length > 0 && value.every(isScalar)) return null;
|
|
3893
|
-
return "must be a scalar, or a non-empty list of them";
|
|
3894
|
-
case "non-empty-list":
|
|
3895
|
-
return Array.isArray(value) && value.length > 0 && value.every(isScalar) ? null : "must be a non-empty list of scalars";
|
|
3896
|
-
case "header-name-map": {
|
|
3897
|
-
if (value === null || typeof value !== "object" || Array.isArray(value)) return "must be an object";
|
|
3898
|
-
const names = Object.keys(value);
|
|
3899
|
-
if (names.length === 0) return "must be an object with at least one entry";
|
|
3900
|
-
const bad = names.find((name) => !isHeaderName(name));
|
|
3901
|
-
return bad === void 0 ? null : `names an invalid HTTP header, ${JSON.stringify(bad)}`;
|
|
3902
|
-
}
|
|
3903
|
-
case "header-name-list": {
|
|
3904
|
-
if (!Array.isArray(value) || value.length === 0) return "must be a non-empty list of HTTP header names";
|
|
3905
|
-
const bad = value.find((name) => !isHeaderName(name));
|
|
3906
|
-
return bad === void 0 ? null : `names an invalid HTTP header, ${JSON.stringify(bad)}`;
|
|
4241
|
+
// src/protect/engine/node.js
|
|
4242
|
+
function fromNodeRequest(req, rawBody = "", options = {}) {
|
|
4243
|
+
const method = (req.method || "GET").toUpperCase();
|
|
4244
|
+
const headers = {};
|
|
4245
|
+
for (const [key, value] of Object.entries(req.headers || {})) {
|
|
4246
|
+
headers[key.toLowerCase()] = Array.isArray(value) ? value.join(", ") : value;
|
|
4247
|
+
}
|
|
4248
|
+
const host = headers.host || "localhost";
|
|
4249
|
+
let url;
|
|
4250
|
+
try {
|
|
4251
|
+
url = new URL(req.url || "/", `http://${host}`);
|
|
4252
|
+
} catch {
|
|
4253
|
+
try {
|
|
4254
|
+
url = new URL(req.url || "/", "http://localhost");
|
|
4255
|
+
} catch {
|
|
4256
|
+
url = new URL("http://localhost/");
|
|
3907
4257
|
}
|
|
3908
|
-
|
|
3909
|
-
|
|
3910
|
-
|
|
3911
|
-
|
|
3912
|
-
|
|
3913
|
-
|
|
3914
|
-
|
|
3915
|
-
}
|
|
3916
|
-
}
|
|
3917
|
-
const sameSite = value.sameSite;
|
|
3918
|
-
const sameSiteOff = sameSite === false;
|
|
3919
|
-
if (sameSite !== void 0 && !sameSiteOff) {
|
|
3920
|
-
if (typeof sameSite !== "string" || !SAME_SITE_VALUES.some((v) => v.toLowerCase() === sameSite.toLowerCase())) {
|
|
3921
|
-
return `flag "sameSite" must be false, or one of ${SAME_SITE_VALUES.join(", ")}`;
|
|
3922
|
-
}
|
|
3923
|
-
}
|
|
3924
|
-
if (value.httpOnly === false && value.secure === false && sameSiteOff) {
|
|
3925
|
-
return "turns off every flag, so it would harden nothing";
|
|
3926
|
-
}
|
|
3927
|
-
return null;
|
|
4258
|
+
}
|
|
4259
|
+
const query = {};
|
|
4260
|
+
for (const [key, value] of url.searchParams) {
|
|
4261
|
+
if (key in query) {
|
|
4262
|
+
query[key] = Array.isArray(query[key]) ? [...query[key], value] : [query[key], value];
|
|
4263
|
+
} else {
|
|
4264
|
+
query[key] = value;
|
|
3928
4265
|
}
|
|
3929
|
-
case "object":
|
|
3930
|
-
return value !== null && typeof value === "object" && !Array.isArray(value) ? null : "must be an object";
|
|
3931
|
-
case "non-empty-object":
|
|
3932
|
-
return value !== null && typeof value === "object" && !Array.isArray(value) && Object.keys(value).length > 0 ? null : "must be an object with at least one entry";
|
|
3933
|
-
case "non-empty-string-list":
|
|
3934
|
-
return Array.isArray(value) && value.length > 0 && value.every(isFilledString) ? null : "must be a non-empty list of non-empty strings";
|
|
3935
|
-
default:
|
|
3936
|
-
return null;
|
|
3937
4266
|
}
|
|
4267
|
+
const contentType = headers["content-type"] || "";
|
|
4268
|
+
let body = {};
|
|
4269
|
+
let files;
|
|
4270
|
+
if (rawBody) {
|
|
4271
|
+
const parsed = parseBody(rawBody, contentType);
|
|
4272
|
+
body = parsed.body;
|
|
4273
|
+
files = parsed.files;
|
|
4274
|
+
}
|
|
4275
|
+
const uri = url.pathname + url.search;
|
|
4276
|
+
const client = resolveClientIp({
|
|
4277
|
+
peer: req.socket?.remoteAddress,
|
|
4278
|
+
headers,
|
|
4279
|
+
trustedProxy: options.trustedProxy
|
|
4280
|
+
});
|
|
4281
|
+
return {
|
|
4282
|
+
method,
|
|
4283
|
+
url: uri,
|
|
4284
|
+
originalUrl: uri,
|
|
4285
|
+
query,
|
|
4286
|
+
body,
|
|
4287
|
+
files,
|
|
4288
|
+
headers,
|
|
4289
|
+
ip: client.ip ?? "",
|
|
4290
|
+
_clientIp: client,
|
|
4291
|
+
cookies: parseCookies2(headers.cookie),
|
|
4292
|
+
// Verbatim body text: preserves literal keys (e.g. `__proto__`) that JSON.stringify drops.
|
|
4293
|
+
_rawBody: rawBody
|
|
4294
|
+
};
|
|
3938
4295
|
}
|
|
3939
|
-
|
|
3940
|
-
|
|
3941
|
-
|
|
3942
|
-
|
|
3943
|
-
|
|
3944
|
-
"
|
|
3945
|
-
|
|
3946
|
-
|
|
3947
|
-
|
|
3948
|
-
|
|
3949
|
-
|
|
3950
|
-
|
|
3951
|
-
|
|
3952
|
-
"encode",
|
|
3953
|
-
"set-header",
|
|
3954
|
-
"remove-header",
|
|
3955
|
-
"harden-cookie"
|
|
3956
|
-
]);
|
|
3957
|
-
var WHEN_KEYS = Object.freeze(["path", "method"]);
|
|
3958
|
-
var RULE_PROPERTIES = Object.freeze([
|
|
3959
|
-
"id",
|
|
3960
|
-
"rule_id",
|
|
3961
|
-
"title",
|
|
3962
|
-
"category",
|
|
3963
|
-
"phase",
|
|
3964
|
-
"action",
|
|
3965
|
-
"rule_v2",
|
|
3966
|
-
"when",
|
|
3967
|
-
"message",
|
|
3968
|
-
"enforcement",
|
|
3969
|
-
"source_revision",
|
|
3970
|
-
"prefilter",
|
|
3971
|
-
"max_bytes",
|
|
3972
|
-
"bypass_limit",
|
|
3973
|
-
"set_headers",
|
|
3974
|
-
"remove_headers",
|
|
3975
|
-
"cookie_flags",
|
|
3976
|
-
"ensure"
|
|
3977
|
-
]);
|
|
3978
|
-
var ACTION_PROPERTIES = Object.freeze({
|
|
3979
|
-
block: Object.freeze({ required: Object.freeze([]), defaulted: Object.freeze([]), phases: Object.freeze(["request", "response", "egress"]) }),
|
|
3980
|
-
redact: Object.freeze({ required: Object.freeze([]), defaulted: Object.freeze([]), phases: Object.freeze(["response"]) }),
|
|
3981
|
-
encode: Object.freeze({ required: Object.freeze([]), defaulted: Object.freeze([]), phases: Object.freeze(["response"]) }),
|
|
3982
|
-
"set-header": Object.freeze({
|
|
3983
|
-
required: Object.freeze(["set_headers"]),
|
|
3984
|
-
defaulted: Object.freeze(["ensure"]),
|
|
3985
|
-
phases: Object.freeze(["response"]),
|
|
3986
|
-
shapes: Object.freeze({ set_headers: "header-name-map" })
|
|
3987
|
-
}),
|
|
3988
|
-
"remove-header": Object.freeze({
|
|
3989
|
-
required: Object.freeze(["remove_headers"]),
|
|
3990
|
-
defaulted: Object.freeze([]),
|
|
3991
|
-
phases: Object.freeze(["response"]),
|
|
3992
|
-
shapes: Object.freeze({ remove_headers: "header-name-list" })
|
|
3993
|
-
}),
|
|
3994
|
-
"harden-cookie": Object.freeze({
|
|
3995
|
-
required: Object.freeze([]),
|
|
3996
|
-
defaulted: Object.freeze(["cookie_flags"]),
|
|
3997
|
-
phases: Object.freeze(["response"]),
|
|
3998
|
-
shapes: Object.freeze({ cookie_flags: "cookie-flags" })
|
|
3999
|
-
})
|
|
4000
|
-
});
|
|
4001
|
-
var LIMITS = Object.freeze({
|
|
4002
|
-
maxRules: 5e3,
|
|
4003
|
-
maxWhitelists: 2e3,
|
|
4004
|
-
maxConditionsPerRule: 250,
|
|
4005
|
-
maxNestingDepth: 12,
|
|
4006
|
-
maxRegexLength: 1e3,
|
|
4007
|
-
maxValueLength: 8192
|
|
4008
|
-
});
|
|
4009
|
-
var GROUP_PARAMETER = "rules";
|
|
4010
|
-
function isGroup(condition) {
|
|
4011
|
-
return condition?.parameter === GROUP_PARAMETER && Array.isArray(condition?.rules);
|
|
4296
|
+
function parseCookies2(header) {
|
|
4297
|
+
const cookies = {};
|
|
4298
|
+
if (!header) {
|
|
4299
|
+
return cookies;
|
|
4300
|
+
}
|
|
4301
|
+
for (const pair of header.split(";")) {
|
|
4302
|
+
const idx = pair.indexOf("=");
|
|
4303
|
+
if (idx === -1) {
|
|
4304
|
+
continue;
|
|
4305
|
+
}
|
|
4306
|
+
cookies[pair.slice(0, idx).trim()] = pair.slice(idx + 1).trim();
|
|
4307
|
+
}
|
|
4308
|
+
return cookies;
|
|
4012
4309
|
}
|
|
4013
|
-
|
|
4014
|
-
|
|
4015
|
-
|
|
4016
|
-
const
|
|
4017
|
-
if (
|
|
4018
|
-
|
|
4310
|
+
|
|
4311
|
+
// src/protect/egress.js
|
|
4312
|
+
async function installEgressGuard({ shouldBlock, onBlock, onSkip, dnsScreen = true, lookup, allowHosts } = {}) {
|
|
4313
|
+
const restores = [];
|
|
4314
|
+
if (typeof shouldBlock !== "function") return () => {
|
|
4315
|
+
};
|
|
4316
|
+
const exempt = new Set((allowHosts ?? []).map((h) => String(h).toLowerCase()));
|
|
4317
|
+
const block = (url, host, method) => {
|
|
4318
|
+
if (!shouldBlock(url, host, method)) return false;
|
|
4319
|
+
notify(onBlock, { url, host, method }, "onEgressBlock");
|
|
4320
|
+
return true;
|
|
4321
|
+
};
|
|
4322
|
+
let screen = null;
|
|
4323
|
+
if (dnsScreen) {
|
|
4324
|
+
try {
|
|
4325
|
+
const resolveLookup = lookup ?? (await import("dns")).lookup;
|
|
4326
|
+
const { isIP } = await import("net");
|
|
4327
|
+
if (typeof resolveLookup === "function" && typeof isIP === "function") {
|
|
4328
|
+
screen = { lookup: resolveLookup, isIP, isExempt: (h) => exempt.has(String(h).toLowerCase()) };
|
|
4329
|
+
}
|
|
4330
|
+
} catch {
|
|
4331
|
+
screen = null;
|
|
4332
|
+
}
|
|
4019
4333
|
}
|
|
4020
|
-
|
|
4021
|
-
|
|
4334
|
+
const skip = (reason, detail) => notify(onSkip, { phase: "egress", reason, detail }, "onSkip");
|
|
4335
|
+
const resolvesToDisallowed = (url, host, method) => new Promise((resolve) => {
|
|
4336
|
+
if (!screen) {
|
|
4337
|
+
if (host && dnsScreen) skip("resolver-unavailable", { host });
|
|
4338
|
+
return resolve(false);
|
|
4339
|
+
}
|
|
4340
|
+
if (!host || screen.isIP(host) !== 0 || screen.isExempt(host)) return resolve(false);
|
|
4341
|
+
try {
|
|
4342
|
+
screen.lookup(host, { all: true }, (err, addresses) => {
|
|
4343
|
+
if (err || !Array.isArray(addresses)) {
|
|
4344
|
+
skip("resolver-failed", { host });
|
|
4345
|
+
return resolve(false);
|
|
4346
|
+
}
|
|
4347
|
+
for (const a of addresses) {
|
|
4348
|
+
const ip = a && typeof a === "object" ? a.address : a;
|
|
4349
|
+
if (ip && block(url, ip, method)) return resolve(true);
|
|
4350
|
+
}
|
|
4351
|
+
resolve(false);
|
|
4352
|
+
});
|
|
4353
|
+
} catch {
|
|
4354
|
+
skip("resolver-failed", { host });
|
|
4355
|
+
resolve(false);
|
|
4356
|
+
}
|
|
4357
|
+
});
|
|
4358
|
+
const originalFetch = globalThis.fetch;
|
|
4359
|
+
if (typeof originalFetch === "function" && !originalFetch.__patchstackGuarded) {
|
|
4360
|
+
const REDIRECT_STATUSES = /* @__PURE__ */ new Set([301, 302, 303, 307, 308]);
|
|
4361
|
+
const MAX_REDIRECTS = 20;
|
|
4362
|
+
const screenUrl = async (u, method) => {
|
|
4363
|
+
let host = null;
|
|
4364
|
+
try {
|
|
4365
|
+
host = new URL(u).hostname;
|
|
4366
|
+
} catch {
|
|
4367
|
+
host = null;
|
|
4368
|
+
}
|
|
4369
|
+
if (block(u, host, method) || await resolvesToDisallowed(u, host, method)) {
|
|
4370
|
+
throw new Error(`Patchstack blocked an outbound request to a disallowed address: ${host ?? u}`);
|
|
4371
|
+
}
|
|
4372
|
+
};
|
|
4373
|
+
const guarded = async (input, init) => {
|
|
4374
|
+
let cur;
|
|
4375
|
+
try {
|
|
4376
|
+
cur = new Request(input, { ...init || {}, redirect: "manual" });
|
|
4377
|
+
} catch {
|
|
4378
|
+
return originalFetch(input, init);
|
|
4379
|
+
}
|
|
4380
|
+
const callerRedirect = init && init.redirect || input && input.redirect || "follow";
|
|
4381
|
+
let url = cur.url;
|
|
4382
|
+
let method = cur.method;
|
|
4383
|
+
await screenUrl(url, method);
|
|
4384
|
+
if (callerRedirect !== "follow") return originalFetch(input, init);
|
|
4385
|
+
const headers = new Headers(cur.headers);
|
|
4386
|
+
const signal = cur.signal;
|
|
4387
|
+
let body = method === "GET" || method === "HEAD" ? void 0 : await cur.clone().arrayBuffer();
|
|
4388
|
+
for (let hop = 0; ; hop++) {
|
|
4389
|
+
const resp = await originalFetch(
|
|
4390
|
+
hop === 0 ? cur : new Request(url, { method, headers, body, redirect: "manual", signal })
|
|
4391
|
+
);
|
|
4392
|
+
const location = REDIRECT_STATUSES.has(resp.status) ? resp.headers.get("location") : null;
|
|
4393
|
+
if (!location) return resp;
|
|
4394
|
+
if (hop >= MAX_REDIRECTS) throw new Error("Patchstack blocked an outbound request: too many redirects");
|
|
4395
|
+
const next = new URL(location, url).href;
|
|
4396
|
+
if (resp.status === 303 || (resp.status === 301 || resp.status === 302) && method === "POST") {
|
|
4397
|
+
method = "GET";
|
|
4398
|
+
body = void 0;
|
|
4399
|
+
headers.delete("content-type");
|
|
4400
|
+
headers.delete("content-length");
|
|
4401
|
+
}
|
|
4402
|
+
if (new URL(next).origin !== new URL(url).origin) {
|
|
4403
|
+
headers.delete("authorization");
|
|
4404
|
+
headers.delete("cookie");
|
|
4405
|
+
}
|
|
4406
|
+
await screenUrl(next, method);
|
|
4407
|
+
url = next;
|
|
4408
|
+
}
|
|
4409
|
+
};
|
|
4410
|
+
guarded.__patchstackGuarded = true;
|
|
4411
|
+
globalThis.fetch = guarded;
|
|
4412
|
+
restores.push(() => {
|
|
4413
|
+
if (globalThis.fetch === guarded) globalThis.fetch = originalFetch;
|
|
4414
|
+
});
|
|
4022
4415
|
}
|
|
4023
|
-
|
|
4024
|
-
|
|
4025
|
-
|
|
4026
|
-
|
|
4416
|
+
for (const moduleName of ["node:http", "node:https"]) {
|
|
4417
|
+
try {
|
|
4418
|
+
const mod = await import(moduleName);
|
|
4419
|
+
const restore = patchHttpModule(mod.default ?? mod, block, screen);
|
|
4420
|
+
if (restore) restores.push(restore);
|
|
4421
|
+
} catch {
|
|
4422
|
+
}
|
|
4027
4423
|
}
|
|
4028
|
-
return
|
|
4424
|
+
return () => {
|
|
4425
|
+
for (const restore of restores) {
|
|
4426
|
+
try {
|
|
4427
|
+
restore();
|
|
4428
|
+
} catch {
|
|
4429
|
+
}
|
|
4430
|
+
}
|
|
4431
|
+
};
|
|
4029
4432
|
}
|
|
4030
|
-
function
|
|
4031
|
-
if (
|
|
4032
|
-
|
|
4033
|
-
|
|
4034
|
-
|
|
4035
|
-
|
|
4036
|
-
|
|
4433
|
+
function patchHttpModule(http, block, screen) {
|
|
4434
|
+
if (!http || typeof http.request !== "function" || http.__patchstackGuarded) return null;
|
|
4435
|
+
const originalRequest = http.request;
|
|
4436
|
+
const originalGet = http.get;
|
|
4437
|
+
const wrap = (original) => function(...args) {
|
|
4438
|
+
const target = extractHttpTarget(args);
|
|
4439
|
+
if (target && block(target.url, target.host, target.method)) {
|
|
4440
|
+
throw new Error(`Patchstack blocked an outbound request to a disallowed address: ${target.host ?? target.url}`);
|
|
4037
4441
|
}
|
|
4038
|
-
|
|
4442
|
+
if (target && screen && target.host && screen.isIP(target.host) === 0 && !screen.isExempt(target.host)) {
|
|
4443
|
+
try {
|
|
4444
|
+
args = withScreeningLookup(args, target, block, screen.lookup);
|
|
4445
|
+
} catch {
|
|
4446
|
+
}
|
|
4447
|
+
}
|
|
4448
|
+
return original.apply(this, args);
|
|
4449
|
+
};
|
|
4450
|
+
const guardedRequest = wrap(originalRequest);
|
|
4451
|
+
http.request = guardedRequest;
|
|
4452
|
+
let guardedGet;
|
|
4453
|
+
if (typeof originalGet === "function") {
|
|
4454
|
+
guardedGet = wrap(originalGet);
|
|
4455
|
+
http.get = guardedGet;
|
|
4039
4456
|
}
|
|
4040
|
-
|
|
4041
|
-
|
|
4042
|
-
|
|
4457
|
+
http.__patchstackGuarded = true;
|
|
4458
|
+
return () => {
|
|
4459
|
+
if (http.request === guardedRequest) http.request = originalRequest;
|
|
4460
|
+
if (guardedGet && http.get === guardedGet) http.get = originalGet;
|
|
4461
|
+
delete http.__patchstackGuarded;
|
|
4462
|
+
};
|
|
4463
|
+
}
|
|
4464
|
+
function extractHttpTarget(args) {
|
|
4465
|
+
const first = args[0];
|
|
4466
|
+
try {
|
|
4467
|
+
if (typeof first === "string" || first instanceof URL) {
|
|
4468
|
+
const url = new URL(String(first));
|
|
4469
|
+
const opts = args.find((a) => a && typeof a === "object" && !(a instanceof URL));
|
|
4470
|
+
return { url: url.href, host: url.hostname, method: opts && opts.method || "GET" };
|
|
4471
|
+
}
|
|
4472
|
+
if (first && typeof first === "object") {
|
|
4473
|
+
const host = normalizeHost(first.hostname || first.host) || "localhost";
|
|
4474
|
+
const protocol = first.protocol || "http:";
|
|
4475
|
+
const port = first.port ? `:${first.port}` : "";
|
|
4476
|
+
const path7 = first.path || "/";
|
|
4477
|
+
return { url: `${protocol}//${host}${port}${path7}`, host, method: first.method || "GET" };
|
|
4478
|
+
}
|
|
4479
|
+
} catch {
|
|
4043
4480
|
}
|
|
4044
|
-
|
|
4045
|
-
|
|
4046
|
-
|
|
4047
|
-
|
|
4048
|
-
|
|
4049
|
-
|
|
4481
|
+
return null;
|
|
4482
|
+
}
|
|
4483
|
+
function normalizeHost(raw) {
|
|
4484
|
+
const host = String(raw || "").trim();
|
|
4485
|
+
const bracketed = /^\[([^\]]+)\]/.exec(host);
|
|
4486
|
+
if (bracketed) return bracketed[1];
|
|
4487
|
+
if ((host.match(/:/g) || []).length > 1) return host;
|
|
4488
|
+
return host.split(":")[0];
|
|
4489
|
+
}
|
|
4490
|
+
function screenResolved(addresses, target, block) {
|
|
4491
|
+
for (const a of addresses || []) {
|
|
4492
|
+
const ip = a && typeof a === "object" ? a.address : a;
|
|
4493
|
+
if (ip && block(target.url, ip, target.method)) return ip;
|
|
4050
4494
|
}
|
|
4051
|
-
|
|
4052
|
-
const key = parameter.slice(dot + 1);
|
|
4053
|
-
if (key === "") return `source "${name}" needs a key after the dot`;
|
|
4054
|
-
return keyProblem(name, source, key);
|
|
4495
|
+
return null;
|
|
4055
4496
|
}
|
|
4056
|
-
function
|
|
4057
|
-
|
|
4058
|
-
|
|
4497
|
+
function withScreeningLookup(args, target, block, lookup) {
|
|
4498
|
+
const screeningLookup = (hostname, options, callback) => {
|
|
4499
|
+
let opts = options;
|
|
4500
|
+
let cb = callback;
|
|
4501
|
+
if (typeof opts === "function") {
|
|
4502
|
+
cb = opts;
|
|
4503
|
+
opts = {};
|
|
4504
|
+
}
|
|
4505
|
+
if (!opts || typeof opts !== "object") opts = {};
|
|
4506
|
+
try {
|
|
4507
|
+
lookup(hostname, { ...opts, all: true }, (err, addresses) => {
|
|
4508
|
+
if (err) return cb(err);
|
|
4509
|
+
const list = Array.isArray(addresses) ? addresses : [];
|
|
4510
|
+
const blocked = screenResolved(list, target, block);
|
|
4511
|
+
if (blocked) {
|
|
4512
|
+
return cb(new Error(`Patchstack blocked an outbound request to a disallowed address: ${target.host} resolved to ${blocked}`));
|
|
4513
|
+
}
|
|
4514
|
+
if (opts.all) return cb(null, list);
|
|
4515
|
+
const first = list[0];
|
|
4516
|
+
if (!first) return cb(new Error(`Patchstack: could not resolve ${hostname}`));
|
|
4517
|
+
return cb(null, first.address, first.family);
|
|
4518
|
+
});
|
|
4519
|
+
} catch {
|
|
4520
|
+
try {
|
|
4521
|
+
lookup(hostname, opts, cb);
|
|
4522
|
+
} catch {
|
|
4523
|
+
cb(new Error(`Patchstack: lookup failed for ${hostname}`));
|
|
4524
|
+
}
|
|
4525
|
+
}
|
|
4526
|
+
};
|
|
4527
|
+
return injectLookupOption(args, screeningLookup);
|
|
4528
|
+
}
|
|
4529
|
+
function injectLookupOption(args, lookup) {
|
|
4530
|
+
const first = args[0];
|
|
4531
|
+
if (first && typeof first === "object" && !(first instanceof URL)) {
|
|
4532
|
+
return [{ ...first, lookup }, ...args.slice(1)];
|
|
4059
4533
|
}
|
|
4060
|
-
const
|
|
4061
|
-
|
|
4062
|
-
if (
|
|
4063
|
-
|
|
4064
|
-
|
|
4534
|
+
const rest = args.slice(1);
|
|
4535
|
+
const optIdx = rest.findIndex((a) => a && typeof a === "object" && !(a instanceof URL));
|
|
4536
|
+
if (optIdx !== -1) {
|
|
4537
|
+
const next = [...rest];
|
|
4538
|
+
next[optIdx] = { ...rest[optIdx], lookup };
|
|
4539
|
+
return [first, ...next];
|
|
4065
4540
|
}
|
|
4066
|
-
const
|
|
4067
|
-
|
|
4068
|
-
|
|
4069
|
-
].join(", ");
|
|
4070
|
-
return `"${name}" has no key "${key}" (expected one of: ${accepted})`;
|
|
4541
|
+
const cbIdx = rest.findIndex((a) => typeof a === "function");
|
|
4542
|
+
if (cbIdx === -1) return [first, { lookup }, ...rest];
|
|
4543
|
+
return [first, ...rest.slice(0, cbIdx), { lookup }, ...rest.slice(cbIdx)];
|
|
4071
4544
|
}
|
|
4072
|
-
|
|
4073
|
-
|
|
4074
|
-
|
|
4075
|
-
|
|
4076
|
-
|
|
4077
|
-
|
|
4078
|
-
|
|
4545
|
+
|
|
4546
|
+
// src/protect/defaults.js
|
|
4547
|
+
var DEFAULT_RESPONSE_RULES = [
|
|
4548
|
+
{
|
|
4549
|
+
id: "resp-private-key",
|
|
4550
|
+
title: "Private key in response body",
|
|
4551
|
+
phase: "response",
|
|
4552
|
+
category: "secret-exposure",
|
|
4553
|
+
action: "redact",
|
|
4554
|
+
prefilter: ["PRIVATE KEY"],
|
|
4555
|
+
rule_v2: [{ parameter: "response.body", match: { type: "regex", value: "/-----BEGIN (?:RSA |EC |OPENSSH |DSA |PGP )?PRIVATE KEY-----/" } }]
|
|
4556
|
+
},
|
|
4557
|
+
{
|
|
4558
|
+
id: "resp-aws-access-key",
|
|
4559
|
+
title: "AWS access key id in response body",
|
|
4560
|
+
phase: "response",
|
|
4561
|
+
category: "secret-exposure",
|
|
4562
|
+
action: "redact",
|
|
4563
|
+
prefilter: ["AKIA", "ASIA"],
|
|
4564
|
+
rule_v2: [{ parameter: "response.body", match: { type: "regex", value: "/\\b(?:AKIA|ASIA)[0-9A-Z]{16}\\b/" } }]
|
|
4565
|
+
},
|
|
4566
|
+
{
|
|
4567
|
+
id: "resp-gcp-api-key",
|
|
4568
|
+
title: "Google API key in response body",
|
|
4569
|
+
phase: "response",
|
|
4570
|
+
category: "secret-exposure",
|
|
4571
|
+
action: "redact",
|
|
4572
|
+
prefilter: ["AIza"],
|
|
4573
|
+
rule_v2: [{ parameter: "response.body", match: { type: "regex", value: "/\\bAIza[0-9A-Za-z_-]{35}\\b/" } }]
|
|
4574
|
+
},
|
|
4575
|
+
{
|
|
4576
|
+
id: "resp-vendor-api-key",
|
|
4577
|
+
title: "Vendor API key / token in response body",
|
|
4578
|
+
phase: "response",
|
|
4579
|
+
category: "secret-exposure",
|
|
4580
|
+
action: "redact",
|
|
4581
|
+
// High-signal, prefix-anchored provider tokens that never legitimately appear in a response
|
|
4582
|
+
// body: Stripe (sk_live_/rk_live_), GitHub (gh[opsu]_ / github_pat_), GitLab (glpat-),
|
|
4583
|
+
// Slack (xox[baprs]-), Anthropic (sk-ant-), Google OAuth (ya29.), npm (npm_). Trailing
|
|
4584
|
+
// (?![0-9A-Za-z]) instead of \\b since some tokens end in - / _ .
|
|
4585
|
+
prefilter: ["sk_live_", "rk_live_", "ghp_", "gho_", "ghs_", "ghu_", "github_pat_", "glpat-", "xox", "sk-ant-", "ya29.", "npm_"],
|
|
4586
|
+
rule_v2: [{ parameter: "response.body", match: { type: "regex", value: "/\\b(?:sk_live_[0-9A-Za-z]{16,}|rk_live_[0-9A-Za-z]{16,}|gh[opsu]_[0-9A-Za-z]{36}|github_pat_[0-9A-Za-z_]{60,}|glpat-[0-9A-Za-z_-]{20,}|xox[baprs]-[0-9A-Za-z-]{10,}|sk-ant-[0-9A-Za-z_-]{20,}|ya29\\.[0-9A-Za-z_-]{20,}|npm_[0-9A-Za-z]{36})(?![0-9A-Za-z])/" } }]
|
|
4587
|
+
},
|
|
4588
|
+
{
|
|
4589
|
+
id: "resp-supabase-secret-key",
|
|
4590
|
+
title: "Supabase secret key in response body",
|
|
4591
|
+
phase: "response",
|
|
4592
|
+
category: "secret-exposure",
|
|
4593
|
+
action: "redact",
|
|
4594
|
+
// Supabase's opaque key format, which is not a JWT and so is not covered by the JWT rule below.
|
|
4595
|
+
// `sb_secret_` is the elevated one — documented as full access, bypassing Row Level Security,
|
|
4596
|
+
// backend-only — while `sb_publishable_` is meant for public clients and must pass untouched.
|
|
4597
|
+
// Both key systems are live at once: new keys sit alongside the legacy `anon` / `service_role`
|
|
4598
|
+
// JWTs rather than replacing them, so this rule and the JWT rule cover different halves.
|
|
4599
|
+
//
|
|
4600
|
+
// The documented grammar is the prefix, 22 base64url characters, `_`, then an 8-character
|
|
4601
|
+
// base64url checksum. Matching it exactly is what keeps the mask on the key and off the response
|
|
4602
|
+
// around it: a variable-length suffix would either run past the key into adjacent syntax or stop
|
|
4603
|
+
// short and leave the tail of a real key visible. The trailing guard rejects a longer run of key
|
|
4604
|
+
// characters, which is not this format.
|
|
4605
|
+
prefilter: ["sb_secret_"],
|
|
4606
|
+
rule_v2: [{ parameter: "response.body", match: { type: "regex", value: "/\\bsb_secret_[A-Za-z0-9_-]{22}_[A-Za-z0-9_-]{8}(?![A-Za-z0-9_-])/" } }]
|
|
4607
|
+
},
|
|
4608
|
+
{
|
|
4609
|
+
id: "resp-supabase-service-role-key",
|
|
4610
|
+
title: "Supabase service_role key in response body",
|
|
4611
|
+
phase: "response",
|
|
4612
|
+
category: "secret-exposure",
|
|
4613
|
+
action: "redact",
|
|
4614
|
+
// Scoped to the one role that must never leave: `service_role` bypasses Row Level Security
|
|
4615
|
+
// entirely. A JWT in a response body is not inherently a leak — the Supabase `anon` key is public
|
|
4616
|
+
// by design and a login response carries the user's own access token — so this decides on the
|
|
4617
|
+
// decoded payload rather than on the shape of the token.
|
|
4618
|
+
//
|
|
4619
|
+
// `jwt_claim_equals` also yields the matching token spans, which is what lets `redact` mask those
|
|
4620
|
+
// tokens and serve the rest of the response.
|
|
4621
|
+
//
|
|
4622
|
+
// A broad any-JWT rule is a reasonable thing to want; it belongs in the managed hardening bundle as
|
|
4623
|
+
// an opt-in, observe-only rule rather than an on-by-default mask.
|
|
4624
|
+
prefilter: ["eyJ"],
|
|
4625
|
+
rule_v2: [{ parameter: "response.body", match: { type: "jwt_claim_equals", claim: "role", value: "service_role" } }]
|
|
4626
|
+
},
|
|
4627
|
+
{
|
|
4628
|
+
id: "resp-db-connection-string",
|
|
4629
|
+
title: "Database connection string with credentials in response body",
|
|
4630
|
+
phase: "response",
|
|
4631
|
+
category: "secret-exposure",
|
|
4632
|
+
action: "redact",
|
|
4633
|
+
prefilter: ["mongodb", "postgres", "mysql", "redis", "amqp"],
|
|
4634
|
+
rule_v2: [{ parameter: "response.body", match: { type: "regex", value: "/\\b(?:mongodb(?:\\+srv)?|postgres(?:ql)?|mysql|redis|amqps?):\\/\\/[^\\s:@\\/]+:[^\\s:@\\/]+@/i" } }]
|
|
4635
|
+
},
|
|
4636
|
+
{
|
|
4637
|
+
id: "resp-stack-trace",
|
|
4638
|
+
title: "Node stack trace leaking in response body",
|
|
4639
|
+
phase: "response",
|
|
4640
|
+
category: "info-exposure",
|
|
4641
|
+
action: "redact",
|
|
4642
|
+
// No prefilter: a Node stack frame has no single distinctive literal (` at ` is too common to
|
|
4643
|
+
// gate on). The pattern is linearly bounded per line, so it runs on every screened body.
|
|
4644
|
+
rule_v2: [{ parameter: "response.body", match: { type: "regex", value: "/\\n\\s+at\\s+.+\\(.+:\\d+:\\d+\\)/" } }]
|
|
4645
|
+
},
|
|
4646
|
+
{
|
|
4647
|
+
id: "resp-sql-error",
|
|
4648
|
+
title: "SQL / ORM error disclosure in response body",
|
|
4649
|
+
phase: "response",
|
|
4650
|
+
category: "info-exposure",
|
|
4651
|
+
action: "redact",
|
|
4652
|
+
prefilter: ["SQLSTATE", "Sequelize", "ER_", "ORA-", "PG::", "SQLITE_ERROR", "SQL syntax"],
|
|
4653
|
+
rule_v2: [{ parameter: "response.body", match: { type: "regex", value: "/(SQLSTATE\\[[0-9A-Z]+\\]|SequelizeDatabaseError|ER_[A-Z_]+|ORA-\\d{5}|PG::[A-Za-z]+Error|SQLITE_ERROR|You have an error in your SQL syntax)/i" } }]
|
|
4654
|
+
},
|
|
4655
|
+
{
|
|
4656
|
+
id: "resp-exception-trace",
|
|
4657
|
+
title: "Backend exception / stack trace disclosure in response body",
|
|
4658
|
+
phase: "response",
|
|
4659
|
+
category: "info-exposure",
|
|
4660
|
+
action: "redact",
|
|
4661
|
+
// Multi-language exception/traceback signatures a normal API response never carries:
|
|
4662
|
+
// Python traceback, Java "Exception in thread", .NET System.*Exception, JVM stack frames,
|
|
4663
|
+
// Go goroutine dumps. (Node `at fn (file:line:col)` frames are handled by resp-stack-trace.)
|
|
4664
|
+
prefilter: ["Traceback", "Exception in thread", "System.", "goroutine ", ".java:", ".kt:", ".scala:", ".rb:", ".py:", ".cs:"],
|
|
4665
|
+
rule_v2: [{ parameter: "response.body", match: { type: "regex", value: '/(Traceback \\(most recent call last\\)|Exception in thread "|System\\.[A-Za-z.]+Exception|\\bat [\\w.$]+\\([\\w]+\\.(?:java|kt|scala|rb|py|cs):\\d+\\)|goroutine \\d+ \\[)/' } }]
|
|
4079
4666
|
}
|
|
4080
|
-
|
|
4081
|
-
|
|
4667
|
+
];
|
|
4668
|
+
var DEFAULT_EGRESS_RULES = [
|
|
4669
|
+
{
|
|
4670
|
+
id: "egress-internal-address",
|
|
4671
|
+
title: "Outbound request to an internal / metadata address (SSRF)",
|
|
4672
|
+
phase: "egress",
|
|
4673
|
+
category: "ssrf",
|
|
4674
|
+
rule_v2: [{ parameter: "egress.host", match: { type: "internal_host" } }]
|
|
4082
4675
|
}
|
|
4083
|
-
|
|
4084
|
-
|
|
4085
|
-
|
|
4086
|
-
|
|
4087
|
-
|
|
4088
|
-
|
|
4676
|
+
];
|
|
4677
|
+
|
|
4678
|
+
// src/protect/block-page.js
|
|
4679
|
+
function escapeHtml(s) {
|
|
4680
|
+
return String(s).replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[c]);
|
|
4681
|
+
}
|
|
4682
|
+
function renderBlockPage({ url = "", code = "patchstack" } = {}) {
|
|
4683
|
+
const safeUrl = escapeHtml(url);
|
|
4684
|
+
const safeCode = escapeHtml(code || "patchstack");
|
|
4685
|
+
return `<!DOCTYPE html>
|
|
4686
|
+
<html lang="en">
|
|
4687
|
+
<head>
|
|
4688
|
+
<meta charset="UTF-8">
|
|
4689
|
+
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
|
4690
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
4691
|
+
<style type="text/css">
|
|
4692
|
+
.access-denied,.site-url-text{letter-spacing:.01em;color:#fff}body{background:#111216}.main-container{display:flex;justify-content:center;flex-direction:column;align-items:center}.container{width:688px;height:456px;background:#1c1e22;border-radius:16px;margin-top:120px}.content{height:330px;margin:64px}.access-denied{font-family:Verdana;font-style:normal;font-weight:600;font-size:24px;line-height:32px;display:flex;align-items:center}.descp-text,.site-url{display:flex;align-items:center}.site-url{margin-top:32px;width:560px;height:64px;background:#26282c;border-radius:4px}.reason-code-text,.site-url-text{font-family:Verdana;font-style:normal;border:none;background:#26282c}.site-url-text{text-overflow: ellipsis;font-weight:400;font-size:16px;line-height:24px;margin-left:16px;width:85%}.copy-icon{width:19px;height:22px;margin-left:20px}.copy-icon:hover{cursor:pointer}.reason-code-text{font-weight:600;font-size:24px;line-height:32px;letter-spacing:.01em;color:#AFE614;margin-left:16px;width:85%}.descp-text,.return-text{font-size:16px;line-height:24px;letter-spacing:.01em;font-weight:400}.return-text:hover{color: #AFE614}.descp-text,.logoText,.return-text,.secured-text{font-family:Verdana;font-style:normal;color:#fff}.descp{margin-top:32px;width:560px}.bottom-second-containers,.bottom-text-containers{align-items:center;display:flex}.bottom-container{margin-top:40px;display:flex;width:688px}.bottom-second-containers{margin-left:auto}.arrow-vector{margin-left:20px}.return-text{text-align:center;margin-left:12.59px;text-decoration:none}.secured-text{font-weight:400;font-size:11.2px;line-height:16px;display:flex;align-items:center;letter-spacing:.02em;margin-right:8px}.logoText{font-weight:600;font-size:16px;line-height:24px;letter-spacing:.01em;flex:none;order:1;flex-grow:0}.patch-logo{width:120px}.mobile-bottom-container{margin-top:40px;width:372px;display:none}.mobile-bottom-second-containers{margin-top:64px;display:flex;margin-left:16.5px}@media only screen and (max-width:900px){.container{background:#111216;width:370px;height:auto;margin-top:0}.content{margin-left:16.5px}.descp,.site-url{width:340px}.site-url-text{font-weight:400;font-size:14.4px;line-height:20px;letter-spacing:.02em;width:80%}.copy-icon{margin-left:20px}.reason-code-text{font-size:16px;line-height:32px;letter-spacing:.02em;margin-left:16px;width:80%}.descp-text,.return-text{font-size:14.4px;line-height:20px}.descp-text{display:flex;align-items:center;letter-spacing:.02em}.bottom-container{display:none}.mobile-bottom-container{display:block}.return-text{text-align:center;letter-spacing:.01em;color:#fff;margin-left:9.73px}}
|
|
4693
|
+
</style>
|
|
4694
|
+
<title>Access Denied</title>
|
|
4695
|
+
</head>
|
|
4696
|
+
<body>
|
|
4697
|
+
<div class="main-container">
|
|
4698
|
+
<div class="container">
|
|
4699
|
+
<div class="content">
|
|
4700
|
+
<span class="access-denied">Access Denied</span>
|
|
4701
|
+
<div class="site-url">
|
|
4702
|
+
<input id="url" class="site-url-text" type="text"
|
|
4703
|
+
value="${safeUrl}" readonly>
|
|
4704
|
+
<img class="copy-icon" src="data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTkiIGhlaWdodD0iMjMiIHZpZXdCb3g9IjAgMCAxOSAyMyIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTE0IDAuMDIzNDM3NUgwVjE2LjAyMzRIMlYyLjAyMzQ0SDE0VjAuMDIzNDM3NVpNMTkgNC4wMjM0NEg0VjIyLjAyMzRIMTlWNC4wMjM0NFpNMTcgMjAuMDIzNEg2VjYuMDIzNDRIMTdWMjAuMDIzNFoiIGZpbGw9IndoaXRlIi8+Cjwvc3ZnPgo=" alt="Copy Text" onmouseover="hover(this);" onmouseout="unhover(this);" onclick="copyText(id='url')">
|
|
4705
|
+
</div>
|
|
4706
|
+
<div class="site-url">
|
|
4707
|
+
<input id="response" class="reason-code-text" type="text" value="${safeCode}" readonly>
|
|
4708
|
+
<img class="copy-icon" src="data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTkiIGhlaWdodD0iMjMiIHZpZXdCb3g9IjAgMCAxOSAyMyIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTE0IDAuMDIzNDM3NUgwVjE2LjAyMzRIMlYyLjAyMzQ0SDE0VjAuMDIzNDM3NVpNMTkgNC4wMjM0NEg0VjIyLjAyMzRIMTlWNC4wMjM0NFpNMTcgMjAuMDIzNEg2VjYuMDIzNDRIMTdWMjAuMDIzNFoiIGZpbGw9IndoaXRlIi8+Cjwvc3ZnPgo=" alt="Copy Text" onmouseover="hover(this);" onmouseout="unhover(this);" onclick="copyText(id='response')">
|
|
4709
|
+
</div>
|
|
4710
|
+
<div class="descp">
|
|
4711
|
+
<span class="descp-text">This request has been blocked by Patchstack. If this message persists and you are a legitimate user, contact the site administrator with the reference above.</span>
|
|
4712
|
+
</div>
|
|
4713
|
+
</div>
|
|
4714
|
+
</div>
|
|
4715
|
+
<div class="bottom-container">
|
|
4716
|
+
<div class="bottom-text-containers">
|
|
4717
|
+
<img class="arrow-vector" src="data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iOCIgaGVpZ2h0PSIxMyIgdmlld0JveD0iMCAwIDggMTMiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxwYXRoIGQ9Ik03LjQxIDEuNDMzNDRMNiAwLjAyMzQzNzVMMCA2LjAyMzQ0TDYgMTIuMDIzNEw3LjQxIDEwLjYxMzRMMi44MyA2LjAyMzQ0TDcuNDEgMS40MzM0NFoiIGZpbGw9IiNBRkU2MTQiLz4KPC9zdmc+Cg==" alt="Arrow Left">
|
|
4718
|
+
<a href="/" class="return-text">Return to homepage</a>
|
|
4719
|
+
</div>
|
|
4720
|
+
<div class="bottom-second-containers">
|
|
4721
|
+
<span class="secured-text">This website is secured by</span>
|
|
4722
|
+
<a href="https://patchstack.com" class="logoText">
|
|
4723
|
+
<img class="patch-logo" src="data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTg0IiBoZWlnaHQ9IjIyIiB2aWV3Qm94PSIwIDAgMTg0IDIyIiBmaWxsPSJub25lIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPgo8cGF0aCBmaWxsLXJ1bGU9ImV2ZW5vZGQiIGNsaXAtcnVsZT0iZXZlbm9kZCIgZD0iTTUwLjQwMDEgMTEuNTAwOEM1MC40MDAxIDkuMTAwNzggNDkuMzAwMSA4LjEwMDc4IDQ2LjYwMDEgOC4xMDA3OEM0NC4wMDAxIDguMTAwNzggNDIuNzAwMSA5LjIwMDc4IDQyLjcwMDEgMTEuNTAwOEM0Mi43MDAxIDEzLjgwMDggNDQuMDAwMSAxNC45MDA4IDQ2LjYwMDEgMTQuOTAwOEM0OS4zMDAxIDE0LjkwMDggNTAuNDAwMSAxMy44MDA4IDUwLjQwMDEgMTEuNTAwOFpNNTMuNDAwMSAxMS41MDA4QzUzLjQwMDEgMTUuMzAwOCA1MS4zMDAxIDE3LjYwMDggNDcuNDAwMSAxNy42MDA4QzQ1LjQwMDEgMTcuNjAwOCA0My42MDAxIDE2LjUwMDggNDIuNzAwMSAxNC41MDA4VjIxLjUwMDhIMzkuNjAwMVY1LjYwMDc4SDQyLjcwMDFWOC41MDA3OEM0My42MDAxIDYuNTAwNzggNDUuNDAwMSA1LjMwMDc4IDQ3LjUwMDEgNS4zMDA3OEM1MS40MDAxIDUuMzAwNzggNTMuNDAwMSA3LjcwMDc4IDUzLjQwMDEgMTEuNTAwOFoiIGZpbGw9IndoaXRlIi8+CjxwYXRoIGZpbGwtcnVsZT0iZXZlbm9kZCIgY2xpcC1ydWxlPSJldmVub2RkIiBkPSJNNjYuNDAwMSAxMS40OTg0QzY2LjQwMDEgOS4xOTg0NCA2NS4xMDAxIDguMDk4NDQgNjIuNTAwMSA4LjA5ODQ0QzU5LjgwMDEgOC4wOTg0NCA1OC43MDAxIDkuMDk4NDQgNTguNzAwMSAxMS40OTg0QzU4LjcwMDEgMTMuNzk4NCA1OS44MDAxIDE0Ljg5ODQgNjIuNTAwMSAxNC44OTg0QzY1LjEwMDEgMTQuODk4NCA2Ni40MDAxIDEzLjc5ODQgNjYuNDAwMSAxMS40OTg0Wk02Ni4zMDAxIDUuNTk4NDRINjkuNDAwMVYxNy4zOTg0SDY2LjMwMDFWMTQuNDk4NEM2NS40MDAxIDE2LjQ5ODQgNjMuNzAwMSAxNy42OTg0IDYxLjYwMDEgMTcuNjk4NEM1Ny43MDAxIDE3LjY5ODQgNTUuNjAwMSAxNS4yOTg0IDU1LjYwMDEgMTEuNTk4NEM1NS42MDAxIDcuNzk4NDQgNTcuNjAwMSA1LjM5ODQ0IDYxLjQwMDEgNS4zOTg0NEM2My41MDAxIDUuMzk4NDQgNjUuNDAwMSA2LjU5ODQ0IDY2LjIwMDEgOC41OTg0NFY1LjU5ODQ0SDY2LjMwMDFaIiBmaWxsPSJ3aGl0ZSIvPgo8cGF0aCBmaWxsLXJ1bGU9ImV2ZW5vZGQiIGNsaXAtcnVsZT0iZXZlbm9kZCIgZD0iTTgxLjM5OTkgOC4zSDc3LjM5OTlWMTQuN0g4MS4wOTk5VjE3LjRINzcuOTk5OUM3NS43OTk5IDE3LjQgNzQuMjk5OSAxNS45IDc0LjI5OTkgMTMuN1Y4LjNINzEuMzk5OVY1LjZINzQuMjk5OVYySDc3LjM5OTlWNS41SDgxLjM5OTlWOC4zWiIgZmlsbD0id2hpdGUiLz4KPHBhdGggZmlsbC1ydWxlPSJldmVub2RkIiBjbGlwLXJ1bGU9ImV2ZW5vZGQiIGQ9Ik05My42OTk5IDEyLjIwMDhMOTYuNjk5OSAxMi45MDA4Qzk1Ljk5OTkgMTYuMDAwOCA5My42OTk5IDE3LjYwMDggODkuOTk5OSAxNy42MDA4Qzg3Ljc5OTkgMTcuNjAwOCA4Ni4wOTk5IDE3LjAwMDggODQuNzk5OSAxNS45MDA4QzgzLjQ5OTkgMTQuODAwOCA4Mi44OTk5IDEzLjMwMDggODIuODk5OSAxMS41MDA4QzgyLjg5OTkgOS42MDA3OCA4My40OTk5IDguMTAwNzggODQuNzk5OSA3LjAwMDc4Qzg2LjA5OTkgNS45MDA3OCA4Ny43OTk5IDUuMzAwNzggODkuOTk5OSA1LjMwMDc4QzkzLjY5OTkgNS4zMDA3OCA5NS45OTk5IDYuOTAwNzggOTYuNjk5OSAxMC4wMDA4TDkzLjY5OTkgMTAuNzAwOEM5My4yOTk5IDguODAwNzggOTIuMjk5OSA4LjEwMDc4IDg5Ljg5OTkgOC4xMDA3OEM4Ni45OTk5IDguMTAwNzggODUuODk5OSA5LjIwMDc4IDg1Ljg5OTkgMTEuNTAwOEM4NS44OTk5IDEzLjgwMDggODYuOTk5OSAxNC45MDA4IDg5Ljg5OTkgMTQuOTAwOEM5Mi4yOTk5IDE0LjkwMDggOTMuMjk5OSAxNC4yMDA4IDkzLjY5OTkgMTIuMjAwOFoiIGZpbGw9IndoaXRlIi8+CjxwYXRoIGZpbGwtcnVsZT0iZXZlbm9kZCIgY2xpcC1ydWxlPSJldmVub2RkIiBkPSJNMTExLjggMTAuNlYxNy40SDEwOC43VjExLjJDMTA4LjcgOSAxMDcuOCA4LjEgMTA1LjkgOC4xQzEwMy42IDguMSAxMDIuNCA5LjMgMTAyLjQgMTEuN1YxNy4zSDk5LjI5OThWMC41SDEwMi40VjguM0MxMDMuMSA2LjMgMTA0LjYgNS4zIDEwNyA1LjNDMTEwLjIgNS4zIDExMS44IDcuMSAxMTEuOCAxMC42WiIgZmlsbD0id2hpdGUiLz4KPHBhdGggZmlsbC1ydWxlPSJldmVub2RkIiBjbGlwLXJ1bGU9ImV2ZW5vZGQiIGQ9Ik0xMTQuMSAxNC4wOTg0TDExNy4xIDEzLjI5ODRDMTE3LjQgMTQuNzk4NCAxMTguMyAxNS4yOTg0IDEyMC4yIDE1LjI5ODRDMTIxLjggMTUuMjk4NCAxMjIuNyAxNC44OTg0IDEyMi43IDE0LjA5ODRDMTIyLjcgMTMuMjk4NCAxMjIuMiAxMi45OTg0IDEyMC42IDEyLjc5ODRMMTE4LjIgMTIuNDk4NEMxMTUuOSAxMi4xOTg0IDExNC41IDEwLjg5ODQgMTE0LjUgOC45OTg0NEMxMTQuNSA2Ljc5ODQ0IDExNi41IDUuMzk4NDQgMTE5LjggNS4zOTg0NEMxMjMuMSA1LjM5ODQ0IDEyNSA2LjY5ODQ0IDEyNS41IDguODk4NDRMMTIyLjUgOS41OTg0NEMxMjIuMyA4LjA5ODQ0IDEyMS41IDcuNjk4NDQgMTE5LjkgNy42OTg0NEMxMTguNCA3LjY5ODQ0IDExNy42IDguMDk4NDQgMTE3LjYgOC44OTg0NEMxMTcuNiA5LjU5ODQ0IDExOC4xIDkuOTk4NDQgMTE5LjMgMTAuMDk4NEwxMjIuMiAxMC40OTg0QzEyNC40IDEwLjc5ODQgMTI1LjggMTEuOTk4NCAxMjUuOCAxMy44OTg0QzEyNS44IDE2LjI5ODQgMTIzLjcgMTcuNzk4NCAxMjAuMSAxNy43OTg0QzExNi43IDE3LjU5ODQgMTE0LjUgMTYuMTk4NCAxMTQuMSAxNC4wOTg0WiIgZmlsbD0id2hpdGUiLz4KPHBhdGggZmlsbC1ydWxlPSJldmVub2RkIiBjbGlwLXJ1bGU9ImV2ZW5vZGQiIGQ9Ik0xMzYuNyA4LjNIMTMyLjdWMTQuN0gxMzYuNFYxNy40SDEzMy4zQzEzMS4xIDE3LjQgMTI5LjYgMTUuOSAxMjkuNiAxMy43VjguM0gxMjYuN1Y1LjZIMTI5LjZWMkgxMzIuN1Y1LjVIMTM2LjdWOC4zWiIgZmlsbD0id2hpdGUiLz4KPHBhdGggZmlsbC1ydWxlPSJldmVub2RkIiBjbGlwLXJ1bGU9ImV2ZW5vZGQiIGQ9Ik0xNDkuMiAxMS40OTg0QzE0OS4yIDkuMTk4NDQgMTQ3LjkgOC4wOTg0NCAxNDUuMyA4LjA5ODQ0QzE0Mi42IDguMDk4NDQgMTQxLjUgOS4wOTg0NCAxNDEuNSAxMS40OTg0QzE0MS41IDEzLjc5ODQgMTQyLjYgMTQuODk4NCAxNDUuMyAxNC44OTg0QzE0Ny45IDE0Ljg5ODQgMTQ5LjIgMTMuNzk4NCAxNDkuMiAxMS40OTg0Wk0xNDkuMSA1LjU5ODQ0SDE1Mi4yVjE3LjM5ODRIMTQ5LjFWMTQuNDk4NEMxNDguMiAxNi40OTg0IDE0Ni41IDE3LjY5ODQgMTQ0LjQgMTcuNjk4NEMxNDAuNSAxNy42OTg0IDEzOC40IDE1LjI5ODQgMTM4LjQgMTEuNTk4NEMxMzguNCA3Ljc5ODQ0IDE0MC40IDUuMzk4NDQgMTQ0LjIgNS4zOTg0NEMxNDYuMyA1LjM5ODQ0IDE0OC4yIDYuNTk4NDQgMTQ5IDguNTk4NDRWNS41OTg0NEgxNDkuMVoiIGZpbGw9IndoaXRlIi8+CjxwYXRoIGZpbGwtcnVsZT0iZXZlbm9kZCIgY2xpcC1ydWxlPSJldmVub2RkIiBkPSJNMTY1LjYgMTIuMjAwOEwxNjguNiAxMi45MDA4QzE2Ny45IDE2LjAwMDggMTY1LjYgMTcuNjAwOCAxNjEuOSAxNy42MDA4QzE1OS43IDE3LjYwMDggMTU4IDE3LjAwMDggMTU2LjcgMTUuOTAwOEMxNTUuNCAxNC44MDA4IDE1NC44IDEzLjMwMDggMTU0LjggMTEuNTAwOEMxNTQuOCA5LjYwMDc4IDE1NS40IDguMTAwNzggMTU2LjcgNy4wMDA3OEMxNTggNS45MDA3OCAxNTkuNyA1LjMwMDc4IDE2MS45IDUuMzAwNzhDMTY1LjYgNS4zMDA3OCAxNjcuOSA2LjkwMDc4IDE2OC42IDEwLjAwMDhMMTY1LjYgMTAuNzAwOEMxNjUuMiA4LjgwMDc4IDE2NC4yIDguMTAwNzggMTYxLjggOC4xMDA3OEMxNTguOSA4LjEwMDc4IDE1Ny44IDkuMjAwNzggMTU3LjggMTEuNTAwOEMxNTcuOCAxMy44MDA4IDE1OC45IDE0LjkwMDggMTYxLjggMTQuOTAwOEMxNjQuMSAxNC45MDA4IDE2NS4xIDE0LjIwMDggMTY1LjYgMTIuMjAwOFoiIGZpbGw9IndoaXRlIi8+CjxwYXRoIGZpbGwtcnVsZT0iZXZlbm9kZCIgY2xpcC1ydWxlPSJldmVub2RkIiBkPSJNMTgwLjI1NyAxNy40TDE3Ni42NTcgMTIuM0wxNzQuMTU3IDE0LjZWMTcuNEgxNzEuMTU3VjAuNUgxNzQuMTU3VjExLjFMMTc5Ljg1NyA1LjZIMTgzLjc1N0wxNzguNTU3IDEwLjVMMTgzLjk1NyAxNy40SDE4MC4yNTdaIiBmaWxsPSJ3aGl0ZSIvPgo8cGF0aCBmaWxsLXJ1bGU9ImV2ZW5vZGQiIGNsaXAtcnVsZT0iZXZlbm9kZCIgZD0iTTAgMjEuNTAwOEg3LjNWMTYuMzAwOEgwVjIxLjUwMDhaIiBmaWxsPSIjQUZFNjE0Ii8+CjxwYXRoIGZpbGwtcnVsZT0iZXZlbm9kZCIgY2xpcC1ydWxlPSJldmVub2RkIiBkPSJNNy4yOTk4IDIxLjUwMDhIMTQuNTk5OFYxNi4zMDA4SDcuMjk5OFYyMS41MDA4WiIgZmlsbD0iIzZFQkUwMCIvPgo8cGF0aCBmaWxsLXJ1bGU9ImV2ZW5vZGQiIGNsaXAtcnVsZT0iZXZlbm9kZCIgZD0iTTE0LjYwMDEgMTYuM0gyOS4yMDAxVjAuNUgxNC42MDAxVjE2LjNaIiBmaWxsPSIjQUZFNjE0Ii8+CjxwYXRoIGZpbGwtcnVsZT0iZXZlbm9kZCIgY2xpcC1ydWxlPSJldmVub2RkIiBkPSJNMCAxMUgxNC42VjAuNUgwVjExWiIgZmlsbD0iIzZFQkUwMCIvPgo8L3N2Zz4K" alt="Patchstack Logo">
|
|
4724
|
+
</a>
|
|
4725
|
+
</div>
|
|
4726
|
+
</div>
|
|
4727
|
+
|
|
4728
|
+
<div class="mobile-bottom-container">
|
|
4729
|
+
<div class="mobile-bottom-text-containers">
|
|
4730
|
+
<img class="arrow-vector" src="data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iOCIgaGVpZ2h0PSIxMyIgdmlld0JveD0iMCAwIDggMTMiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxwYXRoIGQ9Ik03LjQxIDEuNDMzNDRMNiAwLjAyMzQzNzVMMCA2LjAyMzQ0TDYgMTIuMDIzNEw3LjQxIDEwLjYxMzRMMi44MyA2LjAyMzQ0TDcuNDEgMS40MzM0NFoiIGZpbGw9IiNBRkU2MTQiLz4KPC9zdmc+Cg==" alt="Arrow Left">
|
|
4731
|
+
<a href="/" class="return-text">Return to homepage</a>
|
|
4732
|
+
</div>
|
|
4733
|
+
<div class="mobile-bottom-second-containers">
|
|
4734
|
+
<span class="secured-text">This website is secured by</span>
|
|
4735
|
+
<a href="https://patchstack.com" class="logoText">
|
|
4736
|
+
<img class="patch-logo" src="data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTg0IiBoZWlnaHQ9IjIyIiB2aWV3Qm94PSIwIDAgMTg0IDIyIiBmaWxsPSJub25lIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPgo8cGF0aCBmaWxsLXJ1bGU9ImV2ZW5vZGQiIGNsaXAtcnVsZT0iZXZlbm9kZCIgZD0iTTUwLjQwMDEgMTEuNTAwOEM1MC40MDAxIDkuMTAwNzggNDkuMzAwMSA4LjEwMDc4IDQ2LjYwMDEgOC4xMDA3OEM0NC4wMDAxIDguMTAwNzggNDIuNzAwMSA5LjIwMDc4IDQyLjcwMDEgMTEuNTAwOEM0Mi43MDAxIDEzLjgwMDggNDQuMDAwMSAxNC45MDA4IDQ2LjYwMDEgMTQuOTAwOEM0OS4zMDAxIDE0LjkwMDggNTAuNDAwMSAxMy44MDA4IDUwLjQwMDEgMTEuNTAwOFpNNTMuNDAwMSAxMS41MDA4QzUzLjQwMDEgMTUuMzAwOCA1MS4zMDAxIDE3LjYwMDggNDcuNDAwMSAxNy42MDA4QzQ1LjQwMDEgMTcuNjAwOCA0My42MDAxIDE2LjUwMDggNDIuNzAwMSAxNC41MDA4VjIxLjUwMDhIMzkuNjAwMVY1LjYwMDc4SDQyLjcwMDFWOC41MDA3OEM0My42MDAxIDYuNTAwNzggNDUuNDAwMSA1LjMwMDc4IDQ3LjUwMDEgNS4zMDA3OEM1MS40MDAxIDUuMzAwNzggNTMuNDAwMSA3LjcwMDc4IDUzLjQwMDEgMTEuNTAwOFoiIGZpbGw9IndoaXRlIi8+CjxwYXRoIGZpbGwtcnVsZT0iZXZlbm9kZCIgY2xpcC1ydWxlPSJldmVub2RkIiBkPSJNNjYuNDAwMSAxMS40OTg0QzY2LjQwMDEgOS4xOTg0NCA2NS4xMDAxIDguMDk4NDQgNjIuNTAwMSA4LjA5ODQ0QzU5LjgwMDEgOC4wOTg0NCA1OC43MDAxIDkuMDk4NDQgNTguNzAwMSAxMS40OTg0QzU4LjcwMDEgMTMuNzk4NCA1OS44MDAxIDE0Ljg5ODQgNjIuNTAwMSAxNC44OTg0QzY1LjEwMDEgMTQuODk4NCA2Ni40MDAxIDEzLjc5ODQgNjYuNDAwMSAxMS40OTg0Wk02Ni4zMDAxIDUuNTk4NDRINjkuNDAwMVYxNy4zOTg0SDY2LjMwMDFWMTQuNDk4NEM2NS40MDAxIDE2LjQ5ODQgNjMuNzAwMSAxNy42OTg0IDYxLjYwMDEgMTcuNjk4NEM1Ny43MDAxIDE3LjY5ODQgNTUuNjAwMSAxNS4yOTg0IDU1LjYwMDEgMTEuNTk4NEM1NS42MDAxIDcuNzk4NDQgNTcuNjAwMSA1LjM5ODQ0IDYxLjQwMDEgNS4zOTg0NEM2My41MDAxIDUuMzk4NDQgNjUuNDAwMSA2LjU5ODQ0IDY2LjIwMDEgOC41OTg0NFY1LjU5ODQ0SDY2LjMwMDFaIiBmaWxsPSJ3aGl0ZSIvPgo8cGF0aCBmaWxsLXJ1bGU9ImV2ZW5vZGQiIGNsaXAtcnVsZT0iZXZlbm9kZCIgZD0iTTgxLjM5OTkgOC4zSDc3LjM5OTlWMTQuN0g4MS4wOTk5VjE3LjRINzcuOTk5OUM3NS43OTk5IDE3LjQgNzQuMjk5OSAxNS45IDc0LjI5OTkgMTMuN1Y4LjNINzEuMzk5OVY1LjZINzQuMjk5OVYySDc3LjM5OTlWNS41SDgxLjM5OTlWOC4zWiIgZmlsbD0id2hpdGUiLz4KPHBhdGggZmlsbC1ydWxlPSJldmVub2RkIiBjbGlwLXJ1bGU9ImV2ZW5vZGQiIGQ9Ik05My42OTk5IDEyLjIwMDhMOTYuNjk5OSAxMi45MDA4Qzk1Ljk5OTkgMTYuMDAwOCA5My42OTk5IDE3LjYwMDggODkuOTk5OSAxNy42MDA4Qzg3Ljc5OTkgMTcuNjAwOCA4Ni4wOTk5IDE3LjAwMDggODQuNzk5OSAxNS45MDA4QzgzLjQ5OTkgMTQuODAwOCA4Mi44OTk5IDEzLjMwMDggODIuODk5OSAxMS41MDA4QzgyLjg5OTkgOS42MDA3OCA4My40OTk5IDguMTAwNzggODQuNzk5OSA3LjAwMDc4Qzg2LjA5OTkgNS45MDA3OCA4Ny43OTk5IDUuMzAwNzggODkuOTk5OSA1LjMwMDc4QzkzLjY5OTkgNS4zMDA3OCA5NS45OTk5IDYuOTAwNzggOTYuNjk5OSAxMC4wMDA4TDkzLjY5OTkgMTAuNzAwOEM5My4yOTk5IDguODAwNzggOTIuMjk5OSA4LjEwMDc4IDg5Ljg5OTkgOC4xMDA3OEM4Ni45OTk5IDguMTAwNzggODUuODk5OSA5LjIwMDc4IDg1Ljg5OTkgMTEuNTAwOEM4NS44OTk5IDEzLjgwMDggODYuOTk5OSAxNC45MDA4IDg5Ljg5OTkgMTQuOTAwOEM5Mi4yOTk5IDE0LjkwMDggOTMuMjk5OSAxNC4yMDA4IDkzLjY5OTkgMTIuMjAwOFoiIGZpbGw9IndoaXRlIi8+CjxwYXRoIGZpbGwtcnVsZT0iZXZlbm9kZCIgY2xpcC1ydWxlPSJldmVub2RkIiBkPSJNMTExLjggMTAuNlYxNy40SDEwOC43VjExLjJDMTA4LjcgOSAxMDcuOCA4LjEgMTA1LjkgOC4xQzEwMy42IDguMSAxMDIuNCA5LjMgMTAyLjQgMTEuN1YxNy4zSDk5LjI5OThWMC41SDEwMi40VjguM0MxMDMuMSA2LjMgMTA0LjYgNS4zIDEwNyA1LjNDMTEwLjIgNS4zIDExMS44IDcuMSAxMTEuOCAxMC42WiIgZmlsbD0id2hpdGUiLz4KPHBhdGggZmlsbC1ydWxlPSJldmVub2RkIiBjbGlwLXJ1bGU9ImV2ZW5vZGQiIGQ9Ik0xMTQuMSAxNC4wOTg0TDExNy4xIDEzLjI5ODRDMTE3LjQgMTQuNzk4NCAxMTguMyAxNS4yOTg0IDEyMC4yIDE1LjI5ODRDMTIxLjggMTUuMjk4NCAxMjIuNyAxNC44OTg0IDEyMi43IDE0LjA5ODRDMTIyLjcgMTMuMjk4NCAxMjIuMiAxMi45OTg0IDEyMC42IDEyLjc5ODRMMTE4LjIgMTIuNDk4NEMxMTUuOSAxMi4xOTg0IDExNC41IDEwLjg5ODQgMTE0LjUgOC45OTg0NEMxMTQuNSA2Ljc5ODQ0IDExNi41IDUuMzk4NDQgMTE5LjggNS4zOTg0NEMxMjMuMSA1LjM5ODQ0IDEyNSA2LjY5ODQ0IDEyNS41IDguODk4NDRMMTIyLjUgOS41OTg0NEMxMjIuMyA4LjA5ODQ0IDEyMS41IDcuNjk4NDQgMTE5LjkgNy42OTg0NEMxMTguNCA3LjY5ODQ0IDExNy42IDguMDk4NDQgMTE3LjYgOC44OTg0NEMxMTcuNiA5LjU5ODQ0IDExOC4xIDkuOTk4NDQgMTE5LjMgMTAuMDk4NEwxMjIuMiAxMC40OTg0QzEyNC40IDEwLjc5ODQgMTI1LjggMTEuOTk4NCAxMjUuOCAxMy44OTg0QzEyNS44IDE2LjI5ODQgMTIzLjcgMTcuNzk4NCAxMjAuMSAxNy43OTg0QzExNi43IDE3LjU5ODQgMTE0LjUgMTYuMTk4NCAxMTQuMSAxNC4wOTg0WiIgZmlsbD0id2hpdGUiLz4KPHBhdGggZmlsbC1ydWxlPSJldmVub2RkIiBjbGlwLXJ1bGU9ImV2ZW5vZGQiIGQ9Ik0xMzYuNyA4LjNIMTMyLjdWMTQuN0gxMzYuNFYxNy40SDEzMy4zQzEzMS4xIDE3LjQgMTI5LjYgMTUuOSAxMjkuNiAxMy43VjguM0gxMjYuN1Y1LjZIMTI5LjZWMkgxMzIuN1Y1LjVIMTM2LjdWOC4zWiIgZmlsbD0id2hpdGUiLz4KPHBhdGggZmlsbC1ydWxlPSJldmVub2RkIiBjbGlwLXJ1bGU9ImV2ZW5vZGQiIGQ9Ik0xNDkuMiAxMS40OTg0QzE0OS4yIDkuMTk4NDQgMTQ3LjkgOC4wOTg0NCAxNDUuMyA4LjA5ODQ0QzE0Mi42IDguMDk4NDQgMTQxLjUgOS4wOTg0NCAxNDEuNSAxMS40OTg0QzE0MS41IDEzLjc5ODQgMTQyLjYgMTQuODk4NCAxNDUuMyAxNC44OTg0QzE0Ny45IDE0Ljg5ODQgMTQ5LjIgMTMuNzk4NCAxNDkuMiAxMS40OTg0Wk0xNDkuMSA1LjU5ODQ0SDE1Mi4yVjE3LjM5ODRIMTQ5LjFWMTQuNDk4NEMxNDguMiAxNi40OTg0IDE0Ni41IDE3LjY5ODQgMTQ0LjQgMTcuNjk4NEMxNDAuNSAxNy42OTg0IDEzOC40IDE1LjI5ODQgMTM4LjQgMTEuNTk4NEMxMzguNCA3Ljc5ODQ0IDE0MC40IDUuMzk4NDQgMTQ0LjIgNS4zOTg0NEMxNDYuMyA1LjM5ODQ0IDE0OC4yIDYuNTk4NDQgMTQ5IDguNTk4NDRWNS41OTg0NEgxNDkuMVoiIGZpbGw9IndoaXRlIi8+CjxwYXRoIGZpbGwtcnVsZT0iZXZlbm9kZCIgY2xpcC1ydWxlPSJldmVub2RkIiBkPSJNMTY1LjYgMTIuMjAwOEwxNjguNiAxMi45MDA4QzE2Ny45IDE2LjAwMDggMTY1LjYgMTcuNjAwOCAxNjEuOSAxNy42MDA4QzE1OS43IDE3LjYwMDggMTU4IDE3LjAwMDggMTU2LjcgMTUuOTAwOEMxNTUuNCAxNC44MDA4IDE1NC44IDEzLjMwMDggMTU0LjggMTEuNTAwOEMxNTQuOCA5LjYwMDc4IDE1NS40IDguMTAwNzggMTU2LjcgNy4wMDA3OEMxNTggNS45MDA3OCAxNTkuNyA1LjMwMDc4IDE2MS45IDUuMzAwNzhDMTY1LjYgNS4zMDA3OCAxNjcuOSA2LjkwMDc4IDE2OC42IDEwLjAwMDhMMTY1LjYgMTAuNzAwOEMxNjUuMiA4LjgwMDc4IDE2NC4yIDguMTAwNzggMTYxLjggOC4xMDA3OEMxNTguOSA4LjEwMDc4IDE1Ny44IDkuMjAwNzggMTU3LjggMTEuNTAwOEMxNTcuOCAxMy44MDA4IDE1OC45IDE0LjkwMDggMTYxLjggMTQuOTAwOEMxNjQuMSAxNC45MDA4IDE2NS4xIDE0LjIwMDggMTY1LjYgMTIuMjAwOFoiIGZpbGw9IndoaXRlIi8+CjxwYXRoIGZpbGwtcnVsZT0iZXZlbm9kZCIgY2xpcC1ydWxlPSJldmVub2RkIiBkPSJNMTgwLjI1NyAxNy40TDE3Ni42NTcgMTIuM0wxNzQuMTU3IDE0LjZWMTcuNEgxNzEuMTU3VjAuNUgxNzQuMTU3VjExLjFMMTc5Ljg1NyA1LjZIMTgzLjc1N0wxNzguNTU3IDEwLjVMMTgzLjk1NyAxNy40SDE4MC4yNTdaIiBmaWxsPSJ3aGl0ZSIvPgo8cGF0aCBmaWxsLXJ1bGU9ImV2ZW5vZGQiIGNsaXAtcnVsZT0iZXZlbm9kZCIgZD0iTTAgMjEuNTAwOEg3LjNWMTYuMzAwOEgwVjIxLjUwMDhaIiBmaWxsPSIjQUZFNjE0Ii8+CjxwYXRoIGZpbGwtcnVsZT0iZXZlbm9kZCIgY2xpcC1ydWxlPSJldmVub2RkIiBkPSJNNy4yOTk4IDIxLjUwMDhIMTQuNTk5OFYxNi4zMDA4SDcuMjk5OFYyMS41MDA4WiIgZmlsbD0iIzZFQkUwMCIvPgo8cGF0aCBmaWxsLXJ1bGU9ImV2ZW5vZGQiIGNsaXAtcnVsZT0iZXZlbm9kZCIgZD0iTTE0LjYwMDEgMTYuM0gyOS4yMDAxVjAuNUgxNC42MDAxVjE2LjNaIiBmaWxsPSIjQUZFNjE0Ii8+CjxwYXRoIGZpbGwtcnVsZT0iZXZlbm9kZCIgY2xpcC1ydWxlPSJldmVub2RkIiBkPSJNMCAxMUgxNC42VjAuNUgwVjExWiIgZmlsbD0iIzZFQkUwMCIvPgo8L3N2Zz4K" alt="Patchstack Logo">
|
|
4737
|
+
</a>
|
|
4738
|
+
</div>
|
|
4739
|
+
</div>
|
|
4740
|
+
</div>
|
|
4741
|
+
</body>
|
|
4742
|
+
<script>
|
|
4743
|
+
function copyText(id) {
|
|
4744
|
+
var copyText = document.getElementById(id)
|
|
4745
|
+
copyText.select();
|
|
4746
|
+
copyText.setSelectionRange(0, 99999);
|
|
4747
|
+
navigator.clipboard.writeText(copyText.value);
|
|
4089
4748
|
}
|
|
4090
|
-
|
|
4091
|
-
|
|
4092
|
-
|
|
4093
|
-
if (problem) return `match type "${match.type}" operand "${operand}" ${problem}`;
|
|
4749
|
+
|
|
4750
|
+
function hover(element) {
|
|
4751
|
+
element.setAttribute('src', 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTkiIGhlaWdodD0iMjMiIHZpZXdCb3g9IjAgMCAxOSAyMyIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTE0IDAuMDIzNDM3NUgwVjE2LjAyMzRIMlYyLjAyMzQ0SDE0VjAuMDIzNDM3NVpNMTkgNC4wMjM0NEg0VjIyLjAyMzRIMTlWNC4wMjM0NFpNMTcgMjAuMDIzNEg2VjYuMDIzNDRIMTdWMjAuMDIzNFoiIGZpbGw9IiNBRkU2MTQiLz4KPC9zdmc+Cg==');
|
|
4094
4752
|
}
|
|
4095
|
-
|
|
4096
|
-
|
|
4097
|
-
|
|
4098
|
-
return null;
|
|
4099
|
-
}
|
|
4100
|
-
function mutationsProblem(mutations) {
|
|
4101
|
-
if (mutations === void 0 || mutations === null) return null;
|
|
4102
|
-
if (!Array.isArray(mutations)) return "mutations must be an array";
|
|
4103
|
-
for (const mutation of mutations) {
|
|
4104
|
-
if (typeof mutation !== "string" || !MUTATIONS.includes(mutation)) {
|
|
4105
|
-
return `unknown mutation "${String(mutation)}" \u2014 applied as a no-op, so the condition would test the untransformed value`;
|
|
4753
|
+
|
|
4754
|
+
function unhover(element) {
|
|
4755
|
+
element.setAttribute('src', 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTkiIGhlaWdodD0iMjMiIHZpZXdCb3g9IjAgMCAxOSAyMyIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTE0IDAuMDIzNDM3NUgwVjE2LjAyMzRIMlYyLjAyMzQ0SDE0VjAuMDIzNDM3NVpNMTkgNC4wMjM0NEg0VjIyLjAyMzRIMTlWNC4wMjM0NFpNMTcgMjAuMDIzNEg2VjYuMDIzNDRIMTdWMjAuMDIzNFoiIGZpbGw9IndoaXRlIi8+Cjwvc3ZnPgo=');
|
|
4106
4756
|
}
|
|
4107
|
-
|
|
4108
|
-
|
|
4757
|
+
</script>
|
|
4758
|
+
</html>`;
|
|
4109
4759
|
}
|
|
4110
|
-
|
|
4111
|
-
|
|
4112
|
-
|
|
4113
|
-
|
|
4114
|
-
if (
|
|
4115
|
-
|
|
4116
|
-
|
|
4117
|
-
|
|
4118
|
-
|
|
4119
|
-
|
|
4120
|
-
|
|
4121
|
-
|
|
4122
|
-
|
|
4123
|
-
|
|
4124
|
-
|
|
4125
|
-
if (!methods.every(isNonEmptyString)) return "when.method must be a non-empty string, or a list of them";
|
|
4760
|
+
|
|
4761
|
+
// src/protect/rules/store.js
|
|
4762
|
+
var fsMod;
|
|
4763
|
+
async function loadFs() {
|
|
4764
|
+
if (fsMod !== void 0) return fsMod;
|
|
4765
|
+
try {
|
|
4766
|
+
const [fs, path7] = await Promise.all([import("fs"), import("path")]);
|
|
4767
|
+
fsMod = {
|
|
4768
|
+
readFileSync: fs.readFileSync,
|
|
4769
|
+
writeFileSync: fs.writeFileSync,
|
|
4770
|
+
mkdirSync: fs.mkdirSync,
|
|
4771
|
+
join: path7.join
|
|
4772
|
+
};
|
|
4773
|
+
} catch {
|
|
4774
|
+
fsMod = null;
|
|
4126
4775
|
}
|
|
4127
|
-
return
|
|
4128
|
-
}
|
|
4129
|
-
function isNonEmptyString(value) {
|
|
4130
|
-
return typeof value === "string" && value.trim() !== "";
|
|
4776
|
+
return fsMod;
|
|
4131
4777
|
}
|
|
4132
|
-
function
|
|
4133
|
-
|
|
4134
|
-
|
|
4135
|
-
|
|
4136
|
-
|
|
4137
|
-
|
|
4138
|
-
|
|
4778
|
+
function makeStore(options = {}) {
|
|
4779
|
+
let mem = null;
|
|
4780
|
+
const durable = durableTier(options);
|
|
4781
|
+
return {
|
|
4782
|
+
async read() {
|
|
4783
|
+
if (mem) return mem;
|
|
4784
|
+
const env = await durable.read();
|
|
4785
|
+
if (env) mem = env;
|
|
4786
|
+
return env;
|
|
4787
|
+
},
|
|
4788
|
+
async write(env) {
|
|
4789
|
+
if (env) mem = env;
|
|
4790
|
+
await durable.write(env);
|
|
4139
4791
|
}
|
|
4140
|
-
}
|
|
4141
|
-
for (const [property, shape] of Object.entries(spec?.shapes ?? {})) {
|
|
4142
|
-
if (rule?.[property] === void 0 || rule?.[property] === null) continue;
|
|
4143
|
-
const problem = operandShapeProblem(shape, rule[property]);
|
|
4144
|
-
if (problem) return `action "${action}" property "${property}" ${problem}`;
|
|
4145
|
-
}
|
|
4146
|
-
const phase = rule?.phase ?? "request";
|
|
4147
|
-
if (spec !== void 0 && !spec.phases.includes(phase)) {
|
|
4148
|
-
return `action "${action}" is carried out on the ${spec.phases.join("/")} phase; on "${phase}" the rule blocks instead`;
|
|
4149
|
-
}
|
|
4150
|
-
return null;
|
|
4792
|
+
};
|
|
4151
4793
|
}
|
|
4152
|
-
|
|
4153
|
-
|
|
4154
|
-
|
|
4155
|
-
|
|
4156
|
-
|
|
4157
|
-
|
|
4158
|
-
|
|
4159
|
-
|
|
4160
|
-
|
|
4161
|
-
|
|
4162
|
-
|
|
4163
|
-
|
|
4164
|
-
|
|
4165
|
-
|
|
4166
|
-
|
|
4167
|
-
|
|
4168
|
-
|
|
4169
|
-
|
|
4170
|
-
}
|
|
4171
|
-
const whitelists = [];
|
|
4172
|
-
for (const wl of inWhitelists) {
|
|
4173
|
-
if (whitelists.length >= LIMITS2.maxWhitelists) {
|
|
4174
|
-
rejected.push({ id: idOf(wl), reason: `bundle exceeds maxWhitelists (${LIMITS2.maxWhitelists})` });
|
|
4175
|
-
continue;
|
|
4176
|
-
}
|
|
4177
|
-
if (!opts.allowGlobalWhitelists && wl && Array.isArray(wl.rule_v2) && !wl.rule_id) {
|
|
4178
|
-
rejected.push({ id: idOf(wl), reason: "whitelist has no rule_id (would suppress every rule); set allowGlobalWhitelists to permit" });
|
|
4179
|
-
continue;
|
|
4180
|
-
}
|
|
4181
|
-
const reason = conditionsProblem(wl?.rule_v2);
|
|
4182
|
-
if (reason) rejected.push({ id: idOf(wl), reason: `whitelist: ${reason}` });
|
|
4183
|
-
else whitelists.push(wl);
|
|
4794
|
+
function durableTier(options) {
|
|
4795
|
+
const adapter = options.ruleCache;
|
|
4796
|
+
if (adapter && typeof adapter.read === "function" && typeof adapter.write === "function") {
|
|
4797
|
+
return {
|
|
4798
|
+
read: async () => {
|
|
4799
|
+
try {
|
|
4800
|
+
return toEnvelope(await adapter.read());
|
|
4801
|
+
} catch {
|
|
4802
|
+
return null;
|
|
4803
|
+
}
|
|
4804
|
+
},
|
|
4805
|
+
write: async (env) => {
|
|
4806
|
+
try {
|
|
4807
|
+
await adapter.write(env);
|
|
4808
|
+
} catch {
|
|
4809
|
+
}
|
|
4810
|
+
}
|
|
4811
|
+
};
|
|
4184
4812
|
}
|
|
4813
|
+
const dir = options.cacheDir;
|
|
4185
4814
|
return {
|
|
4186
|
-
|
|
4187
|
-
|
|
4815
|
+
read: async () => cacheRead(dir),
|
|
4816
|
+
write: async (env) => cacheWrite(dir, env)
|
|
4188
4817
|
};
|
|
4189
4818
|
}
|
|
4190
|
-
function
|
|
4191
|
-
|
|
4192
|
-
|
|
4193
|
-
|
|
4194
|
-
|
|
4195
|
-
|
|
4196
|
-
|
|
4197
|
-
|
|
4198
|
-
|
|
4199
|
-
const scopeReason = whenProblem(rule.when);
|
|
4200
|
-
if (scopeReason) return scopeReason;
|
|
4201
|
-
const capOverride = rule.max_bytes;
|
|
4202
|
-
if (capOverride !== void 0 && !(Number(capOverride) > 0)) return "max_bytes must be a positive number";
|
|
4203
|
-
return conditionsProblem(rule.rule_v2);
|
|
4819
|
+
async function cacheWrite(dir, env) {
|
|
4820
|
+
if (!dir) return;
|
|
4821
|
+
const fs = await loadFs();
|
|
4822
|
+
if (!fs) return;
|
|
4823
|
+
try {
|
|
4824
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
4825
|
+
fs.writeFileSync(fs.join(dir, "patchstack-rules.json"), JSON.stringify(env));
|
|
4826
|
+
} catch {
|
|
4827
|
+
}
|
|
4204
4828
|
}
|
|
4205
|
-
function
|
|
4206
|
-
if (!
|
|
4207
|
-
|
|
4208
|
-
if (
|
|
4209
|
-
|
|
4210
|
-
return
|
|
4829
|
+
async function cacheRead(dir) {
|
|
4830
|
+
if (!dir) return null;
|
|
4831
|
+
const fs = await loadFs();
|
|
4832
|
+
if (!fs) return null;
|
|
4833
|
+
try {
|
|
4834
|
+
return toEnvelope(JSON.parse(fs.readFileSync(fs.join(dir, "patchstack-rules.json"), "utf8")));
|
|
4835
|
+
} catch {
|
|
4836
|
+
return null;
|
|
4211
4837
|
}
|
|
4212
|
-
|
|
4213
|
-
|
|
4214
|
-
|
|
4215
|
-
|
|
4216
|
-
|
|
4217
|
-
|
|
4218
|
-
|
|
4219
|
-
}
|
|
4220
|
-
const parameterReason = parameterProblem(c.parameter);
|
|
4221
|
-
if (parameterReason) return parameterReason;
|
|
4222
|
-
const mutationReason = mutationsProblem(c.mutations);
|
|
4223
|
-
if (mutationReason) return mutationReason;
|
|
4224
|
-
const matchReason = matchProblem(c.match, c.parameter);
|
|
4225
|
-
if (matchReason) return matchReason;
|
|
4226
|
-
const m = c.match;
|
|
4227
|
-
if (m.type === "regex") {
|
|
4228
|
-
if (typeof m.value !== "string") return "regex match.value must be a string";
|
|
4229
|
-
if (m.value.length > LIMITS2.maxRegexLength) return `regex longer than ${LIMITS2.maxRegexLength} chars`;
|
|
4230
|
-
} else if (typeof m.value === "string" && m.value.length > LIMITS2.maxValueLength) {
|
|
4231
|
-
return `match.value longer than ${LIMITS2.maxValueLength} chars`;
|
|
4232
|
-
}
|
|
4233
|
-
if (m.match) {
|
|
4234
|
-
const nested = conditionsProblem([{ parameter: c.parameter, match: m.match }], depth + 1);
|
|
4235
|
-
if (nested) return nested;
|
|
4236
|
-
}
|
|
4838
|
+
}
|
|
4839
|
+
function toEnvelope(value) {
|
|
4840
|
+
if (!value || typeof value !== "object") return null;
|
|
4841
|
+
if (value.bundle && typeof value.bundle === "object") {
|
|
4842
|
+
return { bundle: value.bundle, etag: value.etag ?? null };
|
|
4843
|
+
}
|
|
4844
|
+
if (Array.isArray(value.firewall) || Array.isArray(value.whitelists)) {
|
|
4845
|
+
return { bundle: value, etag: null };
|
|
4237
4846
|
}
|
|
4238
4847
|
return null;
|
|
4239
4848
|
}
|
|
@@ -4259,67 +4868,70 @@ function reportRejections(rejected, options, label) {
|
|
|
4259
4868
|
`${label}: rejected the entire update because ${rejected.length} rule(s) failed validation \u2014 keeping the previous ruleset and NOT caching this response: ${sample}${rejected.length > 3 ? ", \u2026" : ""}`
|
|
4260
4869
|
), "onError");
|
|
4261
4870
|
}
|
|
4262
|
-
function fromSource(bundle, reason) {
|
|
4263
|
-
return
|
|
4871
|
+
function fromSource(bundle, origin, reason) {
|
|
4872
|
+
return {
|
|
4873
|
+
...bundle,
|
|
4874
|
+
source: reason === void 0 ? { ok: true, origin } : { ok: false, origin, reason }
|
|
4875
|
+
};
|
|
4264
4876
|
}
|
|
4265
4877
|
async function resolveRules(options, store, ctx = {}) {
|
|
4266
4878
|
const timeoutMs = ctx.timeoutMs;
|
|
4267
4879
|
if (options.siteUuid) {
|
|
4268
4880
|
const prior = await store.read();
|
|
4269
|
-
const client = new PulseRuleClient({ siteUuid: options.siteUuid, baseUrl: options.pulseRulesUrl, etag: prior?.etag, timeoutMs, pulseAuth: ctx.pulseAuth,
|
|
4881
|
+
const client = new PulseRuleClient({ siteUuid: options.siteUuid, baseUrl: options.pulseRulesUrl, etag: prior?.etag, timeoutMs, pulseAuth: ctx.pulseAuth, detectionState: ctx.detectionState });
|
|
4270
4882
|
const res = await client.getRules();
|
|
4271
|
-
if (res.success && res.notModified && prior?.bundle) return fromSource(normalizeBundle(prior.bundle, options));
|
|
4883
|
+
if (res.success && res.notModified && prior?.bundle) return fromSource(normalizeBundle(prior.bundle, options), "cache");
|
|
4272
4884
|
if (res.success && !res.notModified) {
|
|
4273
4885
|
const rejected = liveUpdateRejections(res, options);
|
|
4274
4886
|
if (rejected.length > 0) {
|
|
4275
4887
|
reportRejections(rejected, options, "rule update rejected");
|
|
4276
|
-
if (prior?.bundle) return fromSource(normalizeBundle(prior.bundle, options), "update rejected");
|
|
4277
|
-
if (options.rules) return fromSource(normalizeBundle(options.rules, options), "update rejected");
|
|
4278
|
-
return fromSource(emptyBundle(), "update rejected");
|
|
4888
|
+
if (prior?.bundle) return fromSource(normalizeBundle(prior.bundle, options), "cache", "update rejected");
|
|
4889
|
+
if (options.rules) return fromSource(normalizeBundle(options.rules, options), "bundled", "update rejected");
|
|
4890
|
+
return fromSource(emptyBundle(), "empty", "update rejected");
|
|
4279
4891
|
}
|
|
4280
4892
|
const bundle = normalizeBundle(res, options);
|
|
4281
4893
|
await store.write({ bundle, etag: res.etag ?? null });
|
|
4282
|
-
return fromSource(bundle);
|
|
4894
|
+
return fromSource(bundle, "api");
|
|
4283
4895
|
}
|
|
4284
4896
|
if (prior?.bundle) {
|
|
4285
4897
|
notify(options.onError, new Error(`pulse rule fetch failed (${res.error ?? "no usable response"}); using cached bundle`), "onError");
|
|
4286
|
-
return fromSource(normalizeBundle(prior.bundle, options), res.error ?? "no usable response");
|
|
4898
|
+
return fromSource(normalizeBundle(prior.bundle, options), "cache", res.error ?? "no usable response");
|
|
4287
4899
|
}
|
|
4288
4900
|
if (options.rules) {
|
|
4289
4901
|
notify(options.onError, new Error(`pulse rule fetch failed (${res.error ?? "no usable response"}); using bundled fallback`), "onError");
|
|
4290
|
-
return fromSource(normalizeBundle(options.rules, options), res.error ?? "no usable response");
|
|
4902
|
+
return fromSource(normalizeBundle(options.rules, options), "bundled", res.error ?? "no usable response");
|
|
4291
4903
|
}
|
|
4292
4904
|
notify(options.onError, new Error(`pulse rule fetch failed (${res.error ?? "no usable response"}); no cache \u2014 running with no rules`), "onError");
|
|
4293
|
-
return fromSource(emptyBundle(), res.error ?? "no usable response");
|
|
4905
|
+
return fromSource(emptyBundle(), "empty", res.error ?? "no usable response");
|
|
4294
4906
|
}
|
|
4295
4907
|
if (options.token) {
|
|
4296
4908
|
const prior = await store.read();
|
|
4297
4909
|
const client = new PatchstackRuleClient({ token: options.token, baseUrl: options.baseUrl, etag: prior?.etag, timeoutMs });
|
|
4298
4910
|
const res = await client.getRules();
|
|
4299
|
-
if (res.success && res.notModified && prior?.bundle) return fromSource(normalizeBundle(prior.bundle, options));
|
|
4911
|
+
if (res.success && res.notModified && prior?.bundle) return fromSource(normalizeBundle(prior.bundle, options), "cache");
|
|
4300
4912
|
if (res.success && !res.notModified) {
|
|
4301
4913
|
const rejected = liveUpdateRejections(res, options);
|
|
4302
4914
|
if (rejected.length > 0) {
|
|
4303
4915
|
reportRejections(rejected, options, "rule update rejected");
|
|
4304
|
-
if (prior?.bundle) return fromSource(normalizeBundle(prior.bundle, options), "update rejected");
|
|
4305
|
-
if (options.rules) return fromSource(normalizeBundle(options.rules, options), "update rejected");
|
|
4306
|
-
return fromSource(emptyBundle(), "update rejected");
|
|
4916
|
+
if (prior?.bundle) return fromSource(normalizeBundle(prior.bundle, options), "cache", "update rejected");
|
|
4917
|
+
if (options.rules) return fromSource(normalizeBundle(options.rules, options), "bundled", "update rejected");
|
|
4918
|
+
return fromSource(emptyBundle(), "empty", "update rejected");
|
|
4307
4919
|
}
|
|
4308
4920
|
const bundle = normalizeBundle(res, options);
|
|
4309
4921
|
await store.write({ bundle, etag: res.etag ?? null });
|
|
4310
|
-
return fromSource(bundle);
|
|
4922
|
+
return fromSource(bundle, "api");
|
|
4311
4923
|
}
|
|
4312
4924
|
if (prior?.bundle) {
|
|
4313
4925
|
notify(options.onError, new Error(`rule fetch failed (${res.error ?? "no usable response"}); using cached bundle`), "onError");
|
|
4314
|
-
return fromSource(normalizeBundle(prior.bundle, options), res.error ?? "no usable response");
|
|
4926
|
+
return fromSource(normalizeBundle(prior.bundle, options), "cache", res.error ?? "no usable response");
|
|
4315
4927
|
}
|
|
4316
4928
|
notify(options.onError, new Error(`rule fetch failed (${res.error ?? "no usable response"}); no cache \u2014 running with no rules`), "onError");
|
|
4317
|
-
return fromSource(emptyBundle(), res.error ?? "no usable response");
|
|
4929
|
+
return fromSource(emptyBundle(), "empty", res.error ?? "no usable response");
|
|
4318
4930
|
}
|
|
4319
4931
|
if (options.rules) {
|
|
4320
|
-
return fromSource(normalizeBundle(options.rules, options));
|
|
4932
|
+
return fromSource(normalizeBundle(options.rules, options), "bundled");
|
|
4321
4933
|
}
|
|
4322
|
-
return fromSource(emptyBundle());
|
|
4934
|
+
return fromSource(emptyBundle(), "empty");
|
|
4323
4935
|
}
|
|
4324
4936
|
function normalizeBundle(b, options = {}) {
|
|
4325
4937
|
const enforcement = b?.enforcement ?? b?.mode;
|
|
@@ -4416,6 +5028,164 @@ var DEFAULT_BASE_URL3 = "https://api.patchstack.com/monitor/pulse";
|
|
|
4416
5028
|
var DEFAULT_FLUSH_MS = 5e3;
|
|
4417
5029
|
var MAX_BATCH = 50;
|
|
4418
5030
|
var MAX_QUEUE = 500;
|
|
5031
|
+
var MAX_ATTEMPTS = 4;
|
|
5032
|
+
var RETRY_BASE_MS = 1e3;
|
|
5033
|
+
var RETRY_CAP_MS = 3e4;
|
|
5034
|
+
var RETRYABLE_EXPLICIT = /* @__PURE__ */ new Set([408, 425, 429]);
|
|
5035
|
+
function worthRetrying(status) {
|
|
5036
|
+
if (status === null) return true;
|
|
5037
|
+
return RETRYABLE_EXPLICIT.has(status) || status >= 500 && status <= 599;
|
|
5038
|
+
}
|
|
5039
|
+
var ATTEMPT_TIMEOUT_MS = 1e4;
|
|
5040
|
+
var STOP_BUDGET_MS = 5e3;
|
|
5041
|
+
var MAX_ROUTE_CHARS = 256;
|
|
5042
|
+
var MAX_PARAMETERS = 25;
|
|
5043
|
+
var MAX_PARAMETER_CHARS = 64;
|
|
5044
|
+
var MAX_IDENTIFIER_CHARS = 256;
|
|
5045
|
+
var MAX_CAPTURED_VALUES = 10;
|
|
5046
|
+
var MAX_CAPTURED_VALUE_CHARS = 512;
|
|
5047
|
+
var MAX_QUERY_KEYS = 10;
|
|
5048
|
+
var MAX_METHOD_CHARS = 16;
|
|
5049
|
+
var MAX_BODY_BYTES2 = 64 * 1024;
|
|
5050
|
+
function queryKeysOf(path7) {
|
|
5051
|
+
if (typeof path7 !== "string") return { keys: [], total: 0 };
|
|
5052
|
+
const start = path7.indexOf("?");
|
|
5053
|
+
if (start === -1) return { keys: [], total: 0 };
|
|
5054
|
+
let names;
|
|
5055
|
+
try {
|
|
5056
|
+
names = [...new URLSearchParams(path7.slice(start + 1)).keys()];
|
|
5057
|
+
} catch {
|
|
5058
|
+
return { keys: [], total: 0 };
|
|
5059
|
+
}
|
|
5060
|
+
const seen = /* @__PURE__ */ new Set();
|
|
5061
|
+
const keys = [];
|
|
5062
|
+
for (const name of names) {
|
|
5063
|
+
if (name === "") continue;
|
|
5064
|
+
if (seen.has(name)) continue;
|
|
5065
|
+
seen.add(name);
|
|
5066
|
+
if (keys.length < MAX_QUERY_KEYS) keys.push(name);
|
|
5067
|
+
}
|
|
5068
|
+
return { keys, total: seen.size };
|
|
5069
|
+
}
|
|
5070
|
+
function boundCapture(capture) {
|
|
5071
|
+
if (!capture || typeof capture !== "object" || Array.isArray(capture)) return null;
|
|
5072
|
+
const truncated = [];
|
|
5073
|
+
const own = (object, key) => Object.hasOwn(object, key) ? object[key] : void 0;
|
|
5074
|
+
const declaredPlan = own(capture, "plan");
|
|
5075
|
+
const plan = typeof declaredPlan === "string" ? capText(declaredPlan, MAX_IDENTIFIER_CHARS) : null;
|
|
5076
|
+
if (plan === null) return null;
|
|
5077
|
+
if (plan.truncated) truncated.push("plan");
|
|
5078
|
+
const out = { plan: plan.value };
|
|
5079
|
+
let overCap = 0;
|
|
5080
|
+
let rejected = 0;
|
|
5081
|
+
const declaredValues = own(capture, "values");
|
|
5082
|
+
if (Array.isArray(declaredValues) && declaredValues.length > 0) {
|
|
5083
|
+
const values = [];
|
|
5084
|
+
for (const entry of declaredValues) {
|
|
5085
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
|
|
5086
|
+
rejected += 1;
|
|
5087
|
+
continue;
|
|
5088
|
+
}
|
|
5089
|
+
const declaredParameter = own(entry, "parameter");
|
|
5090
|
+
const text = scalarText(own(entry, "value"));
|
|
5091
|
+
if (typeof declaredParameter !== "string" || text === null) {
|
|
5092
|
+
rejected += 1;
|
|
5093
|
+
continue;
|
|
5094
|
+
}
|
|
5095
|
+
if (values.length >= MAX_CAPTURED_VALUES) {
|
|
5096
|
+
overCap += 1;
|
|
5097
|
+
continue;
|
|
5098
|
+
}
|
|
5099
|
+
const label = capText(declaredParameter, MAX_PARAMETER_CHARS);
|
|
5100
|
+
const value = capText(text, MAX_CAPTURED_VALUE_CHARS);
|
|
5101
|
+
if (label.truncated && !truncated.includes("parameter")) truncated.push("parameter");
|
|
5102
|
+
if (value.truncated && !truncated.includes("value")) truncated.push("value");
|
|
5103
|
+
values.push({
|
|
5104
|
+
parameter: label.value,
|
|
5105
|
+
value: value.value,
|
|
5106
|
+
...own(entry, "truncated") === true || value.truncated ? { truncated: true } : {}
|
|
5107
|
+
});
|
|
5108
|
+
}
|
|
5109
|
+
if (overCap > 0) truncated.push("values");
|
|
5110
|
+
if (values.length > 0) out.values = values;
|
|
5111
|
+
}
|
|
5112
|
+
const declaredRaw = own(capture, "raw");
|
|
5113
|
+
if (declaredRaw !== void 0 && declaredRaw !== null) {
|
|
5114
|
+
const rawValue = declaredRaw && typeof declaredRaw === "object" ? own(declaredRaw, "value") : void 0;
|
|
5115
|
+
if (typeof rawValue === "string") {
|
|
5116
|
+
const raw = capText(rawValue, MAX_CAPTURED_VALUE_CHARS);
|
|
5117
|
+
out.raw = {
|
|
5118
|
+
value: raw.value,
|
|
5119
|
+
...own(declaredRaw, "truncated") === true || raw.truncated ? { truncated: true } : {}
|
|
5120
|
+
};
|
|
5121
|
+
} else {
|
|
5122
|
+
rejected += 1;
|
|
5123
|
+
}
|
|
5124
|
+
}
|
|
5125
|
+
const declaredCount = (key) => {
|
|
5126
|
+
const value = own(capture, key);
|
|
5127
|
+
return Number.isFinite(value) && value > 0 ? Math.floor(value) : 0;
|
|
5128
|
+
};
|
|
5129
|
+
const totals = {
|
|
5130
|
+
omitted: declaredCount("omitted") + overCap,
|
|
5131
|
+
unsupported: declaredCount("unsupported") + rejected,
|
|
5132
|
+
failed: declaredCount("failed")
|
|
5133
|
+
};
|
|
5134
|
+
for (const [key, total] of Object.entries(totals)) {
|
|
5135
|
+
if (total > 0) out[key] = total;
|
|
5136
|
+
}
|
|
5137
|
+
if (own(capture, "unavailable") === true) out.unavailable = true;
|
|
5138
|
+
if (truncated.length > 0) out.truncated = truncated;
|
|
5139
|
+
return out;
|
|
5140
|
+
}
|
|
5141
|
+
function scalarText(value) {
|
|
5142
|
+
if (typeof value === "string") return value;
|
|
5143
|
+
if (typeof value === "number" && Number.isFinite(value)) return String(value);
|
|
5144
|
+
if (typeof value === "boolean") return String(value);
|
|
5145
|
+
return null;
|
|
5146
|
+
}
|
|
5147
|
+
function makeInstanceId() {
|
|
5148
|
+
try {
|
|
5149
|
+
const uuid = globalThis.crypto?.randomUUID?.();
|
|
5150
|
+
if (typeof uuid === "string" && uuid !== "") return uuid;
|
|
5151
|
+
} catch {
|
|
5152
|
+
}
|
|
5153
|
+
return `${Date.now().toString(36)}-${Math.floor(Math.random() * 16777215).toString(36)}`;
|
|
5154
|
+
}
|
|
5155
|
+
function capText(value, limit) {
|
|
5156
|
+
const text = typeof value === "string" ? value : "";
|
|
5157
|
+
return text.length > limit ? { value: text.slice(0, limit), truncated: true } : { value: text, truncated: false };
|
|
5158
|
+
}
|
|
5159
|
+
function retryDelayMs(attempts, retryAfter, random = Math.random) {
|
|
5160
|
+
const advertised = parseRetryAfter(retryAfter);
|
|
5161
|
+
if (advertised !== null) return Math.min(advertised, RETRY_CAP_MS);
|
|
5162
|
+
const backoff = Math.min(RETRY_BASE_MS * 2 ** (attempts - 1), RETRY_CAP_MS);
|
|
5163
|
+
return Math.min(RETRY_CAP_MS, Math.round(backoff * (0.75 + random() * 0.5)));
|
|
5164
|
+
}
|
|
5165
|
+
function parseRetryAfter(value) {
|
|
5166
|
+
if (typeof value !== "string" || value.trim() === "") return null;
|
|
5167
|
+
const seconds = Number(value.trim());
|
|
5168
|
+
if (Number.isFinite(seconds)) return seconds >= 0 ? seconds * 1e3 : null;
|
|
5169
|
+
const at = Date.parse(value);
|
|
5170
|
+
if (Number.isNaN(at)) return null;
|
|
5171
|
+
return Math.max(0, at - Date.now());
|
|
5172
|
+
}
|
|
5173
|
+
var encoder = new TextEncoder();
|
|
5174
|
+
function byteLength(text) {
|
|
5175
|
+
return encoder.encode(text).length;
|
|
5176
|
+
}
|
|
5177
|
+
function splitToFit(events, maxBytes, wrap = (batch) => ({ detections: batch })) {
|
|
5178
|
+
const batch = events.slice();
|
|
5179
|
+
const rest = [];
|
|
5180
|
+
while (batch.length > 1 && byteLength(JSON.stringify(wrap(batch))) > maxBytes) {
|
|
5181
|
+
rest.unshift(batch.pop());
|
|
5182
|
+
}
|
|
5183
|
+
return [batch, rest];
|
|
5184
|
+
}
|
|
5185
|
+
function unattended(timer) {
|
|
5186
|
+
if (timer && typeof timer.unref === "function") timer.unref();
|
|
5187
|
+
return timer;
|
|
5188
|
+
}
|
|
4419
5189
|
function ruleParameters(rule) {
|
|
4420
5190
|
const out = /* @__PURE__ */ new Set();
|
|
4421
5191
|
const walk2 = (conditions) => {
|
|
@@ -4450,12 +5220,22 @@ function createDetectionReporter(opts) {
|
|
|
4450
5220
|
},
|
|
4451
5221
|
flush() {
|
|
4452
5222
|
},
|
|
4453
|
-
stop()
|
|
4454
|
-
},
|
|
5223
|
+
stop: () => Promise.resolve(),
|
|
4455
5224
|
setRulesEtag() {
|
|
4456
5225
|
},
|
|
5226
|
+
announce() {
|
|
5227
|
+
},
|
|
4457
5228
|
dropped: () => 0,
|
|
4458
|
-
health: () => ({
|
|
5229
|
+
health: () => ({
|
|
5230
|
+
sent: 0,
|
|
5231
|
+
delivered: 0,
|
|
5232
|
+
failed: 0,
|
|
5233
|
+
dropped: 0,
|
|
5234
|
+
retried: 0,
|
|
5235
|
+
reauthorized: 0,
|
|
5236
|
+
lastDeliveredAt: null,
|
|
5237
|
+
capability: { announced: 0, acknowledged: 0, failed: 0, retried: 0, lastAcknowledgedAt: null }
|
|
5238
|
+
})
|
|
4459
5239
|
};
|
|
4460
5240
|
}
|
|
4461
5241
|
const configured = opts.baseUrl ?? process.env?.PATCHSTACK_PULSE_RULES_URL;
|
|
@@ -4469,47 +5249,233 @@ function createDetectionReporter(opts) {
|
|
|
4469
5249
|
let failed = 0;
|
|
4470
5250
|
let droppedTotal = 0;
|
|
4471
5251
|
let lastDeliveredAt = null;
|
|
5252
|
+
let capabilityAnnounced = 0;
|
|
5253
|
+
let capabilityAcknowledged = 0;
|
|
5254
|
+
let capabilityFailed = 0;
|
|
5255
|
+
let lastCapabilityAckAt = null;
|
|
4472
5256
|
let queue = [];
|
|
4473
5257
|
let timer = null;
|
|
5258
|
+
let retryTimer = null;
|
|
4474
5259
|
let stopped = false;
|
|
4475
5260
|
let dropped = 0;
|
|
5261
|
+
let retried = 0;
|
|
5262
|
+
let capabilityRetried = 0;
|
|
5263
|
+
let flushRequested = false;
|
|
5264
|
+
let draining = false;
|
|
5265
|
+
let drainResolve = null;
|
|
5266
|
+
let drainPromise = null;
|
|
5267
|
+
let budgetTimer = null;
|
|
5268
|
+
let epoch = 0;
|
|
5269
|
+
let reauthorized = 0;
|
|
5270
|
+
let sending = false;
|
|
5271
|
+
let inFlight = null;
|
|
5272
|
+
let pendingState = null;
|
|
5273
|
+
const instanceId = makeInstanceId();
|
|
5274
|
+
let sequence = 0;
|
|
5275
|
+
const bodyFor = (events, droppedWith, state) => ({
|
|
5276
|
+
detections: events,
|
|
5277
|
+
// The count of what never made it, sent WITH the batch rather than inferred from a gap: a consumer
|
|
5278
|
+
// computing a false-positive rate needs to know its denominator is short, and silence about that
|
|
5279
|
+
// would make a truncated sample look like a complete one.
|
|
5280
|
+
dropped: droppedWith,
|
|
5281
|
+
...state !== null ? { reporting_state: state } : {}
|
|
5282
|
+
});
|
|
5283
|
+
const takeBatch = (droppedWith, state) => {
|
|
5284
|
+
const [batch, rest] = splitToFit(
|
|
5285
|
+
queue.splice(0, MAX_BATCH),
|
|
5286
|
+
MAX_BODY_BYTES2,
|
|
5287
|
+
(events) => bodyFor(events, droppedWith, state)
|
|
5288
|
+
);
|
|
5289
|
+
if (rest.length > 0) queue.unshift(...rest);
|
|
5290
|
+
return batch;
|
|
5291
|
+
};
|
|
5292
|
+
const authConfig = { pulseAuth: opts.pulseAuth, endpoint: baseUrl, timeoutMs: ATTEMPT_TIMEOUT_MS };
|
|
5293
|
+
const detectionsUrl = `${baseUrl}/detections/${encodeURIComponent(siteUuid)}`;
|
|
5294
|
+
const post = async (body, key, signal, transport) => (
|
|
5295
|
+
// Through the shared Pulse path, which attaches the token and — on a 401 — discards it and retries
|
|
5296
|
+
// once with a fresh one. A cached token can stop being valid before it expires, and the server's
|
|
5297
|
+
// refusal is authoritative over our own clock; the batch's own headers, this key included, are
|
|
5298
|
+
// carried through both sends, so the redelivery is still recognisable as the same batch.
|
|
5299
|
+
pulseFetch(
|
|
5300
|
+
authConfig,
|
|
5301
|
+
detectionsUrl,
|
|
5302
|
+
{
|
|
5303
|
+
method: "POST",
|
|
5304
|
+
...signal ? { signal } : {},
|
|
5305
|
+
headers: {
|
|
5306
|
+
"Content-Type": "application/json",
|
|
5307
|
+
Accept: "application/json",
|
|
5308
|
+
"User-Agent": "@patchstack/connect",
|
|
5309
|
+
// The same key on every attempt of one batch: an acknowledgement can be lost after the server
|
|
5310
|
+
// has already taken the batch, and a redelivery has to be identifiable as the same one.
|
|
5311
|
+
"Idempotency-Key": key
|
|
5312
|
+
},
|
|
5313
|
+
body: JSON.stringify(body)
|
|
5314
|
+
},
|
|
5315
|
+
transport
|
|
5316
|
+
)
|
|
5317
|
+
);
|
|
5318
|
+
const abandon = () => {
|
|
5319
|
+
if (!inFlight) return;
|
|
5320
|
+
failed += inFlight.events.length;
|
|
5321
|
+
if (inFlight.state !== null) capabilityFailed += 1;
|
|
5322
|
+
inFlight = null;
|
|
5323
|
+
};
|
|
5324
|
+
const settle = () => {
|
|
5325
|
+
if (!inFlight) return;
|
|
5326
|
+
if (inFlight.events.length > 0) {
|
|
5327
|
+
delivered += inFlight.events.length;
|
|
5328
|
+
lastDeliveredAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
5329
|
+
}
|
|
5330
|
+
if (inFlight.state !== null) {
|
|
5331
|
+
capabilityAcknowledged += 1;
|
|
5332
|
+
lastCapabilityAckAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
5333
|
+
}
|
|
5334
|
+
inFlight = null;
|
|
5335
|
+
};
|
|
5336
|
+
const attempt = async () => {
|
|
5337
|
+
if (!inFlight) return;
|
|
5338
|
+
const mine = epoch;
|
|
5339
|
+
sending = true;
|
|
5340
|
+
inFlight.attempts += 1;
|
|
5341
|
+
if (inFlight.attempts > 1) {
|
|
5342
|
+
if (inFlight.events.length > 0) retried += 1;
|
|
5343
|
+
if (inFlight.state !== null) capabilityRetried += 1;
|
|
5344
|
+
}
|
|
5345
|
+
const body = bodyFor(inFlight.events, inFlight.dropped, inFlight.state);
|
|
5346
|
+
let status = null;
|
|
5347
|
+
let retryAfter = null;
|
|
5348
|
+
const controller = typeof AbortController === "function" ? new AbortController() : null;
|
|
5349
|
+
inFlight.controller = controller;
|
|
5350
|
+
const timeout = controller ? unattended(setTimeout(() => controller.abort(), ATTEMPT_TIMEOUT_MS)) : null;
|
|
5351
|
+
inFlight.timeout = timeout;
|
|
5352
|
+
let sends = 0;
|
|
5353
|
+
const counted = (url, init) => {
|
|
5354
|
+
if (String(url) === detectionsUrl) {
|
|
5355
|
+
sends += 1;
|
|
5356
|
+
if (sends > 1 && mine === epoch) reauthorized += 1;
|
|
5357
|
+
}
|
|
5358
|
+
return fetchImpl(url, init);
|
|
5359
|
+
};
|
|
5360
|
+
try {
|
|
5361
|
+
const res = await post(body, inFlight.key, controller?.signal, counted);
|
|
5362
|
+
if (mine !== epoch) return;
|
|
5363
|
+
if (res && res.ok) {
|
|
5364
|
+
settle();
|
|
5365
|
+
if (timeout) clearTimeout(timeout);
|
|
5366
|
+
finish();
|
|
5367
|
+
return;
|
|
5368
|
+
}
|
|
5369
|
+
status = typeof res?.status === "number" ? res.status : 0;
|
|
5370
|
+
retryAfter = res?.headers?.get?.("retry-after") ?? null;
|
|
5371
|
+
} catch {
|
|
5372
|
+
status = null;
|
|
5373
|
+
} finally {
|
|
5374
|
+
if (timeout) clearTimeout(timeout);
|
|
5375
|
+
if (inFlight && inFlight.controller === controller) {
|
|
5376
|
+
inFlight.controller = null;
|
|
5377
|
+
inFlight.timeout = null;
|
|
5378
|
+
}
|
|
5379
|
+
}
|
|
5380
|
+
if (mine !== epoch) return;
|
|
5381
|
+
const retryable = worthRetrying(status);
|
|
5382
|
+
if (retryable && inFlight.attempts < MAX_ATTEMPTS && !stopped) {
|
|
5383
|
+
const delay = retryDelayMs(inFlight.attempts, retryAfter);
|
|
5384
|
+
sending = false;
|
|
5385
|
+
retryTimer = unattended(
|
|
5386
|
+
setTimeout(() => {
|
|
5387
|
+
retryTimer = null;
|
|
5388
|
+
void attempt();
|
|
5389
|
+
}, delay)
|
|
5390
|
+
);
|
|
5391
|
+
return;
|
|
5392
|
+
}
|
|
5393
|
+
abandon();
|
|
5394
|
+
finish();
|
|
5395
|
+
};
|
|
5396
|
+
const finish = () => {
|
|
5397
|
+
sending = false;
|
|
5398
|
+
kick();
|
|
5399
|
+
};
|
|
5400
|
+
const arm = () => {
|
|
5401
|
+
if (!timer) {
|
|
5402
|
+
timer = unattended(
|
|
5403
|
+
setTimeout(() => {
|
|
5404
|
+
timer = null;
|
|
5405
|
+
flushRequested = true;
|
|
5406
|
+
kick();
|
|
5407
|
+
}, flushMs)
|
|
5408
|
+
);
|
|
5409
|
+
}
|
|
5410
|
+
};
|
|
5411
|
+
const due = () => draining || pendingState !== null || queue.length >= MAX_BATCH || flushRequested;
|
|
5412
|
+
const kick = () => {
|
|
5413
|
+
if (sending || inFlight) return;
|
|
5414
|
+
if (typeof fetchImpl !== "function") {
|
|
5415
|
+
if (draining) drained();
|
|
5416
|
+
return;
|
|
5417
|
+
}
|
|
5418
|
+
if (stopped && !draining) return;
|
|
5419
|
+
if (queue.length === 0 && pendingState === null) {
|
|
5420
|
+
flushRequested = false;
|
|
5421
|
+
if (draining) drained();
|
|
5422
|
+
return;
|
|
5423
|
+
}
|
|
5424
|
+
if (!due()) {
|
|
5425
|
+
arm();
|
|
5426
|
+
return;
|
|
5427
|
+
}
|
|
5428
|
+
flushRequested = false;
|
|
5429
|
+
const droppedWith = dropped;
|
|
5430
|
+
dropped = 0;
|
|
5431
|
+
droppedTotal += droppedWith;
|
|
5432
|
+
const state = pendingState;
|
|
5433
|
+
pendingState = null;
|
|
5434
|
+
const events = takeBatch(droppedWith, state);
|
|
5435
|
+
sent += events.length;
|
|
5436
|
+
if (state !== null) capabilityAnnounced += 1;
|
|
5437
|
+
sequence += 1;
|
|
5438
|
+
inFlight = { key: `${instanceId}-${sequence}`, events, dropped: droppedWith, state, attempts: 0 };
|
|
5439
|
+
void attempt();
|
|
5440
|
+
};
|
|
5441
|
+
const terminate = () => {
|
|
5442
|
+
epoch += 1;
|
|
5443
|
+
if (inFlight) {
|
|
5444
|
+
inFlight.controller?.abort();
|
|
5445
|
+
if (inFlight.timeout) clearTimeout(inFlight.timeout);
|
|
5446
|
+
failed += inFlight.events.length;
|
|
5447
|
+
if (inFlight.state !== null) capabilityFailed += 1;
|
|
5448
|
+
inFlight = null;
|
|
5449
|
+
}
|
|
5450
|
+
if (pendingState !== null) {
|
|
5451
|
+
pendingState = null;
|
|
5452
|
+
capabilityAnnounced += 1;
|
|
5453
|
+
capabilityFailed += 1;
|
|
5454
|
+
}
|
|
5455
|
+
sending = false;
|
|
5456
|
+
drained();
|
|
5457
|
+
};
|
|
5458
|
+
const drained = () => {
|
|
5459
|
+
draining = false;
|
|
5460
|
+
if (queue.length > 0) {
|
|
5461
|
+
droppedTotal += queue.length;
|
|
5462
|
+
queue = [];
|
|
5463
|
+
}
|
|
5464
|
+
if (budgetTimer) {
|
|
5465
|
+
clearTimeout(budgetTimer);
|
|
5466
|
+
budgetTimer = null;
|
|
5467
|
+
}
|
|
5468
|
+
const resolve = drainResolve;
|
|
5469
|
+
drainResolve = null;
|
|
5470
|
+
if (resolve) resolve();
|
|
5471
|
+
};
|
|
4476
5472
|
const flush = () => {
|
|
4477
5473
|
if (timer) {
|
|
4478
5474
|
clearTimeout(timer);
|
|
4479
5475
|
timer = null;
|
|
4480
5476
|
}
|
|
4481
|
-
|
|
4482
|
-
|
|
4483
|
-
const droppedWith = dropped;
|
|
4484
|
-
dropped = 0;
|
|
4485
|
-
droppedTotal += droppedWith;
|
|
4486
|
-
sent += batch.length;
|
|
4487
|
-
void (async () => {
|
|
4488
|
-
try {
|
|
4489
|
-
const res = await fetchImpl(`${baseUrl}/detections/${encodeURIComponent(siteUuid)}`, {
|
|
4490
|
-
method: "POST",
|
|
4491
|
-
headers: {
|
|
4492
|
-
"Content-Type": "application/json",
|
|
4493
|
-
Accept: "application/json",
|
|
4494
|
-
"User-Agent": "@patchstack/connect",
|
|
4495
|
-
// Same credential path as the rules fetch. The detections endpoint is site-addressed and
|
|
4496
|
-
// requires a verified, site-bound token, so a batch sent without one is refused — which is
|
|
4497
|
-
// why the runtime does not build a reporter when no credential resolves, rather than
|
|
4498
|
-
// posting into a 401.
|
|
4499
|
-
...await pulseAuthHeader({ pulseAuth: opts.pulseAuth, endpoint: baseUrl }, fetchImpl)
|
|
4500
|
-
},
|
|
4501
|
-
body: JSON.stringify({ detections: batch, dropped: droppedWith })
|
|
4502
|
-
});
|
|
4503
|
-
if (res && res.ok) {
|
|
4504
|
-
delivered += batch.length;
|
|
4505
|
-
lastDeliveredAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
4506
|
-
} else {
|
|
4507
|
-
failed += batch.length;
|
|
4508
|
-
}
|
|
4509
|
-
} catch {
|
|
4510
|
-
failed += batch.length;
|
|
4511
|
-
}
|
|
4512
|
-
})();
|
|
5477
|
+
flushRequested = true;
|
|
5478
|
+
kick();
|
|
4513
5479
|
};
|
|
4514
5480
|
return {
|
|
4515
5481
|
/**
|
|
@@ -4528,32 +5494,144 @@ function createDetectionReporter(opts) {
|
|
|
4528
5494
|
queue.shift();
|
|
4529
5495
|
dropped++;
|
|
4530
5496
|
}
|
|
5497
|
+
const query = queryKeysOf(detection.path);
|
|
5498
|
+
const method = typeof detection.method === "string" ? capText(detection.method, MAX_METHOD_CHARS) : null;
|
|
5499
|
+
const userAgent = typeof detection.userAgent === "string" && detection.userAgent !== "" ? capText(detection.userAgent, MAX_IDENTIFIER_CHARS) : null;
|
|
5500
|
+
const queryKeys = query.keys.map((name) => capText(name, MAX_PARAMETER_CHARS));
|
|
5501
|
+
const capture = boundCapture(detection.capture);
|
|
5502
|
+
const rawRoute = routeOf(detection.path);
|
|
5503
|
+
const route = typeof rawRoute === "string" ? capText(rawRoute, MAX_ROUTE_CHARS) : { value: rawRoute, truncated: false };
|
|
5504
|
+
const allParameters = ruleParameters(detection.rule);
|
|
5505
|
+
const parameters = allParameters.slice(0, MAX_PARAMETERS).map((name) => capText(name, MAX_PARAMETER_CHARS));
|
|
5506
|
+
const truncated = [];
|
|
5507
|
+
if (route.truncated) truncated.push("route");
|
|
5508
|
+
if (method?.truncated) truncated.push("method");
|
|
5509
|
+
if (userAgent?.truncated) truncated.push("user_agent");
|
|
5510
|
+
if (query.total > queryKeys.length || queryKeys.some((entry) => entry.truncated)) {
|
|
5511
|
+
truncated.push("query_keys");
|
|
5512
|
+
}
|
|
5513
|
+
if (allParameters.length > MAX_PARAMETERS || parameters.some((entry) => entry.truncated)) {
|
|
5514
|
+
truncated.push("parameters");
|
|
5515
|
+
}
|
|
5516
|
+
const id = capText(String(ruleId), MAX_IDENTIFIER_CHARS);
|
|
5517
|
+
const revision = capText(revisionOf(detection.rule) ?? "", MAX_IDENTIFIER_CHARS);
|
|
5518
|
+
const etag = capText(rulesEtag ?? "", MAX_IDENTIFIER_CHARS);
|
|
5519
|
+
for (const [name, field] of [
|
|
5520
|
+
["rule_id", id],
|
|
5521
|
+
["rule_revision", revision],
|
|
5522
|
+
["rules_etag", etag]
|
|
5523
|
+
]) {
|
|
5524
|
+
if (field.truncated) truncated.push(name);
|
|
5525
|
+
}
|
|
4531
5526
|
queue.push({
|
|
4532
|
-
rule_id:
|
|
4533
|
-
route:
|
|
4534
|
-
parameters:
|
|
5527
|
+
rule_id: id.value,
|
|
5528
|
+
route: route.value,
|
|
5529
|
+
parameters: parameters.map((entry) => entry.value),
|
|
5530
|
+
// Present only when something was shortened, so its absence is not a claim of its own.
|
|
5531
|
+
...truncated.length > 0 ? { truncated } : {},
|
|
5532
|
+
// Only when parameters were actually left out. Reporting a total because some OTHER field was
|
|
5533
|
+
// shortened states that parameters were omitted when none were.
|
|
5534
|
+
...truncated.includes("parameters") ? { parameters_total: allParameters.length } : {},
|
|
5535
|
+
method: method === null ? null : method.value,
|
|
5536
|
+
// The rest of the URL, as names only. `route` is the path; together they say what was requested
|
|
5537
|
+
// without saying what was in it.
|
|
5538
|
+
query_keys: queryKeys.map((entry) => entry.value),
|
|
5539
|
+
// Present only when the list is short, so its absence is not a claim of its own.
|
|
5540
|
+
...query.total > queryKeys.length ? { query_keys_total: query.total } : {},
|
|
5541
|
+
// Who asked. Capped, since it is client-supplied text and this is an event with a size bound.
|
|
5542
|
+
user_agent: userAgent === null ? null : userAgent.value,
|
|
4535
5543
|
phase: detection.phase ?? null,
|
|
4536
5544
|
// The state this detection was handled under, which is the whole point: `false` is a rule that
|
|
4537
5545
|
// saw traffic it would have stopped.
|
|
4538
5546
|
enforced: detection.mode === "block",
|
|
4539
|
-
rules_etag: rulesEtag,
|
|
5547
|
+
rules_etag: rulesEtag === null ? null : etag.value,
|
|
4540
5548
|
// The revision of THIS rule, as the bundle delivered it. The bundle identity above answers "which
|
|
4541
5549
|
// bundle", which changes whenever anything in it changes — so it cannot say whether the counts for
|
|
4542
5550
|
// one rule describe the document that rule has now. Passed through untouched, and null when the
|
|
4543
5551
|
// bundle carried none.
|
|
4544
|
-
rule_revision: revisionOf(detection.rule),
|
|
5552
|
+
rule_revision: revisionOf(detection.rule) === null ? null : revision.value,
|
|
5553
|
+
// What the rule was permitted to show, and what it showed. Present only when a plan was derived,
|
|
5554
|
+
// which is to say only for a phase that has a reading to derive from.
|
|
5555
|
+
...capture ? { capture } : {},
|
|
5556
|
+
// The client address and where it came from. `client_ip` is omitted entirely when there is none,
|
|
5557
|
+
// so a present-but-empty field cannot read as a failed lookup of a real address; the provenance is
|
|
5558
|
+
// always present, because "this could not be established" is the part a reader needs.
|
|
5559
|
+
...clientIpFields({ ip: detection.ip ?? null, source: detection.clientIpSource ?? "unavailable" }),
|
|
4545
5560
|
detected_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
4546
5561
|
});
|
|
4547
|
-
|
|
4548
|
-
flush();
|
|
4549
|
-
return;
|
|
4550
|
-
}
|
|
4551
|
-
if (!timer) timer = setTimeout(flush, flushMs);
|
|
5562
|
+
kick();
|
|
4552
5563
|
},
|
|
4553
5564
|
flush,
|
|
5565
|
+
/**
|
|
5566
|
+
* Tell the platform which reporting state this guard settled on.
|
|
5567
|
+
*
|
|
5568
|
+
* The state also travels on the rules fetch, but that request is made BEFORE the fetch decides
|
|
5569
|
+
* whether the rules are the platform's — so a site booting with an empty cache declares that it holds
|
|
5570
|
+
* no managed rules, then receives them. Without this, the corrected state would wait for the next
|
|
5571
|
+
* refresh, and a guard with refreshing switched off has none.
|
|
5572
|
+
*
|
|
5573
|
+
* Goes through the same transport as events, so it inherits the retry, the idempotency key and the
|
|
5574
|
+
* single-flight bound. Only the newest state is kept: calling this twice before a send declares the
|
|
5575
|
+
* second, because what the platform needs is the state now, not how it got there. A declaration that
|
|
5576
|
+
* exhausts its retries is dropped rather than held — the state travels on every rules fetch too, so
|
|
5577
|
+
* the next one corrects it.
|
|
5578
|
+
*
|
|
5579
|
+
* @param {string} state
|
|
5580
|
+
*/
|
|
5581
|
+
announce(state) {
|
|
5582
|
+
if (stopped || typeof fetchImpl !== "function" || typeof state !== "string") return;
|
|
5583
|
+
pendingState = state;
|
|
5584
|
+
queueMicrotask(() => {
|
|
5585
|
+
if (!stopped) kick();
|
|
5586
|
+
});
|
|
5587
|
+
},
|
|
5588
|
+
/**
|
|
5589
|
+
* Stop reporting, leaving nothing stranded and nothing scheduled.
|
|
5590
|
+
*
|
|
5591
|
+
* Three things can be outstanding, and each needs an answer:
|
|
5592
|
+
*
|
|
5593
|
+
* - a batch waiting on a retry timer — it holds the only send slot, so clearing the timer alone would
|
|
5594
|
+
* leave it neither delivered nor counted. It gets one final attempt, with no retry behind it.
|
|
5595
|
+
* - a request in flight — it is aborted, and its completion drains what is left rather than starting
|
|
5596
|
+
* open-ended work.
|
|
5597
|
+
* - events still queued — drained a batch at a time, each attempted once.
|
|
5598
|
+
*
|
|
5599
|
+
* Whatever remains unsent when the drain runs out is counted as dropped, so every recorded event ends
|
|
5600
|
+
* up delivered, refused or dropped, and none simply disappears.
|
|
5601
|
+
*
|
|
5602
|
+
* The drain is asynchronous, so this returns a promise that settles when nothing is outstanding —
|
|
5603
|
+
* which a host shutting down can await instead of racing the last batch against process exit. It is
|
|
5604
|
+
* best-effort and bounded: a runtime that terminates the process regardless, or a drain slower than
|
|
5605
|
+
* `STOP_BUDGET_MS`, still ends the wait. Ignoring the promise behaves exactly as before.
|
|
5606
|
+
*/
|
|
4554
5607
|
stop() {
|
|
5608
|
+
if (stopped) return drainPromise ?? Promise.resolve();
|
|
4555
5609
|
stopped = true;
|
|
4556
|
-
|
|
5610
|
+
drainPromise = new Promise((resolve) => {
|
|
5611
|
+
drainResolve = resolve;
|
|
5612
|
+
});
|
|
5613
|
+
budgetTimer = unattended(setTimeout(() => {
|
|
5614
|
+
budgetTimer = null;
|
|
5615
|
+
terminate();
|
|
5616
|
+
}, STOP_BUDGET_MS));
|
|
5617
|
+
if (timer) {
|
|
5618
|
+
clearTimeout(timer);
|
|
5619
|
+
timer = null;
|
|
5620
|
+
}
|
|
5621
|
+
draining = true;
|
|
5622
|
+
if (retryTimer) {
|
|
5623
|
+
clearTimeout(retryTimer);
|
|
5624
|
+
retryTimer = null;
|
|
5625
|
+
void attempt();
|
|
5626
|
+
return drainPromise;
|
|
5627
|
+
}
|
|
5628
|
+
if (sending) {
|
|
5629
|
+
inFlight?.controller?.abort();
|
|
5630
|
+
return drainPromise;
|
|
5631
|
+
}
|
|
5632
|
+
flushRequested = true;
|
|
5633
|
+
kick();
|
|
5634
|
+
return drainPromise;
|
|
4557
5635
|
},
|
|
4558
5636
|
/**
|
|
4559
5637
|
* Point later events at the bundle now running. Called after an ACCEPTED swap only — a rejected
|
|
@@ -4572,12 +5650,70 @@ function createDetectionReporter(opts) {
|
|
|
4572
5650
|
* Delivery health, counted in events: attempted, acknowledged, refused or unreachable, and dropped
|
|
4573
5651
|
* for queue pressure — plus when a batch was last acknowledged. No request data of any kind.
|
|
4574
5652
|
*/
|
|
4575
|
-
health: () => ({
|
|
5653
|
+
health: () => ({
|
|
5654
|
+
sent,
|
|
5655
|
+
delivered,
|
|
5656
|
+
failed,
|
|
5657
|
+
dropped: droppedTotal + dropped,
|
|
5658
|
+
// Attempts beyond the first, counted in ATTEMPTS rather than events: a path that only ever
|
|
5659
|
+
// succeeds on a second try is working, and is worth telling apart from one that never retries.
|
|
5660
|
+
retried,
|
|
5661
|
+
// Redeliveries the credential path made after a refused token, which are not backoff retries and
|
|
5662
|
+
// would otherwise appear nowhere: a rotated or revoked credential is worth seeing as itself.
|
|
5663
|
+
reauthorized,
|
|
5664
|
+
lastDeliveredAt,
|
|
5665
|
+
// Separate, because a capability announcement delivers no events. Reading zero here alongside a
|
|
5666
|
+
// non-zero `delivered` is a normal state, and so is the reverse.
|
|
5667
|
+
capability: {
|
|
5668
|
+
announced: capabilityAnnounced,
|
|
5669
|
+
acknowledged: capabilityAcknowledged,
|
|
5670
|
+
failed: capabilityFailed,
|
|
5671
|
+
retried: capabilityRetried,
|
|
5672
|
+
lastAcknowledgedAt: lastCapabilityAckAt
|
|
5673
|
+
}
|
|
5674
|
+
})
|
|
4576
5675
|
};
|
|
4577
5676
|
}
|
|
4578
5677
|
|
|
5678
|
+
// src/protect/reporting-state.js
|
|
5679
|
+
var REPORTING_STATES = Object.freeze([
|
|
5680
|
+
/** Enrolled, managed rules, credential present, not opted out. Events are sent. */
|
|
5681
|
+
"on",
|
|
5682
|
+
/** `PATCHSTACK_REPORT_DETECTIONS=0`. An explicit, per-deployment opt-out. */
|
|
5683
|
+
"disabled-by-config",
|
|
5684
|
+
/** `PATCHSTACK_TELEMETRY=0`. The broader switch, which covers this along with everything else. */
|
|
5685
|
+
"disabled-by-telemetry-opt-out",
|
|
5686
|
+
/** No site identity: a local or unenrolled install. Nothing to report against. */
|
|
5687
|
+
"not-enrolled",
|
|
5688
|
+
/**
|
|
5689
|
+
* A site identity, but the rules running are not the platform's — the caller's own bundle, or none.
|
|
5690
|
+
* There is no managed rule document to attribute a detection to, so a report would name a rule id the
|
|
5691
|
+
* platform never issued.
|
|
5692
|
+
*/
|
|
5693
|
+
"no-managed-rules",
|
|
5694
|
+
/** Enrolled with managed rules, but no credential resolved, so a report would be refused. */
|
|
5695
|
+
"unavailable-no-credential"
|
|
5696
|
+
]);
|
|
5697
|
+
function optedOut(value) {
|
|
5698
|
+
if (value === void 0 || value === null || value === "") return false;
|
|
5699
|
+
return /^(0|false|off|no)$/i.test(String(value));
|
|
5700
|
+
}
|
|
5701
|
+
function reportingState(input) {
|
|
5702
|
+
const env = input.env ?? (typeof process !== "undefined" ? process.env : void 0) ?? {};
|
|
5703
|
+
if (input.configOptOut === true) return { state: "disabled-by-config", reports: false };
|
|
5704
|
+
if (optedOut(env.PATCHSTACK_REPORT_DETECTIONS)) return { state: "disabled-by-config", reports: false };
|
|
5705
|
+
if (optedOut(env.PATCHSTACK_TELEMETRY)) return { state: "disabled-by-telemetry-opt-out", reports: false };
|
|
5706
|
+
const siteUuid = input.siteUuid;
|
|
5707
|
+
if (typeof siteUuid !== "string" || siteUuid === "") return { state: "not-enrolled", reports: false };
|
|
5708
|
+
if (input.hasCredential !== true) return { state: "unavailable-no-credential", reports: false };
|
|
5709
|
+
const managed = input.ruleOrigin === "api" || input.ruleOrigin === "cache";
|
|
5710
|
+
if (!managed) return { state: "no-managed-rules", reports: false };
|
|
5711
|
+
return { state: "on", reports: true };
|
|
5712
|
+
}
|
|
5713
|
+
|
|
4579
5714
|
// src/protect/firewall-log.js
|
|
4580
5715
|
var DEFAULT_API_BASE = "https://api.patchstack.com";
|
|
5716
|
+
var STOP_BUDGET_MS2 = 5e3;
|
|
4581
5717
|
var DEFAULT_FLUSH_MS2 = 1e3;
|
|
4582
5718
|
var MAX_BATCH2 = 50;
|
|
4583
5719
|
var TOKEN_SKEW_MS2 = 6e4;
|
|
@@ -4618,9 +5754,7 @@ function createFirewallLogReporter(opts) {
|
|
|
4618
5754
|
const creds = parseApiKey(opts.apiKey);
|
|
4619
5755
|
if (!creds) {
|
|
4620
5756
|
return { record() {
|
|
4621
|
-
}, flush()
|
|
4622
|
-
}, stop() {
|
|
4623
|
-
} };
|
|
5757
|
+
}, flush: () => Promise.resolve(), stop: () => Promise.resolve() };
|
|
4624
5758
|
}
|
|
4625
5759
|
const apiBase = (opts.apiBase ?? DEFAULT_API_BASE).replace(/\/$/, "");
|
|
4626
5760
|
const fetchImpl = opts.fetchImpl ?? globalThis.fetch;
|
|
@@ -4629,9 +5763,13 @@ function createFirewallLogReporter(opts) {
|
|
|
4629
5763
|
let queue = [];
|
|
4630
5764
|
let timer = null;
|
|
4631
5765
|
let stopped = false;
|
|
5766
|
+
const outstanding = /* @__PURE__ */ new Set();
|
|
5767
|
+
let drainPromise = null;
|
|
5768
|
+
const lifetime = typeof AbortController === "function" ? new AbortController() : null;
|
|
5769
|
+
let ended = false;
|
|
4632
5770
|
let cachedToken = null;
|
|
4633
5771
|
let tokenInflight = null;
|
|
4634
|
-
const fetchAccessToken = async () => {
|
|
5772
|
+
const fetchAccessToken = async (signal) => {
|
|
4635
5773
|
if (cachedToken && Date.now() < cachedToken.expiresAt - TOKEN_SKEW_MS2) {
|
|
4636
5774
|
return cachedToken.token;
|
|
4637
5775
|
}
|
|
@@ -4640,6 +5778,7 @@ function createFirewallLogReporter(opts) {
|
|
|
4640
5778
|
try {
|
|
4641
5779
|
const res = await fetchImpl(`${apiBase}/oauth/token`, {
|
|
4642
5780
|
method: "POST",
|
|
5781
|
+
...signal ? { signal } : {},
|
|
4643
5782
|
headers: {
|
|
4644
5783
|
"Content-Type": "application/json",
|
|
4645
5784
|
Accept: "application/json",
|
|
@@ -4672,15 +5811,16 @@ function createFirewallLogReporter(opts) {
|
|
|
4672
5811
|
clearTimeout(timer);
|
|
4673
5812
|
timer = null;
|
|
4674
5813
|
}
|
|
4675
|
-
if (queue.length === 0 || typeof fetchImpl !== "function") return;
|
|
5814
|
+
if (ended || queue.length === 0 || typeof fetchImpl !== "function") return Promise.resolve();
|
|
4676
5815
|
const batch = queue.splice(0, MAX_BATCH2);
|
|
4677
|
-
|
|
4678
|
-
|
|
4679
|
-
if (!token) return;
|
|
4680
|
-
const body = new URLSearchParams();
|
|
4681
|
-
body.set("type", "firewall");
|
|
4682
|
-
body.set("logs", JSON.stringify(batch));
|
|
5816
|
+
let entry;
|
|
5817
|
+
const send = (async () => {
|
|
4683
5818
|
try {
|
|
5819
|
+
const token = await fetchAccessToken(lifetime?.signal);
|
|
5820
|
+
if (!token || ended) return;
|
|
5821
|
+
const body = new URLSearchParams();
|
|
5822
|
+
body.set("type", "firewall");
|
|
5823
|
+
body.set("logs", JSON.stringify(batch));
|
|
4684
5824
|
const p = fetchImpl(`${apiBase}/api/logs/log`, {
|
|
4685
5825
|
method: "POST",
|
|
4686
5826
|
headers: {
|
|
@@ -4690,13 +5830,20 @@ function createFirewallLogReporter(opts) {
|
|
|
4690
5830
|
"User-Agent": "@patchstack/connect",
|
|
4691
5831
|
...sourceHost ? { "Source-Host": sourceHost } : {}
|
|
4692
5832
|
},
|
|
4693
|
-
body
|
|
5833
|
+
body,
|
|
5834
|
+
// Both phases carry it, so a shutdown that runs out of time can end either one.
|
|
5835
|
+
...lifetime ? { signal: lifetime.signal } : {}
|
|
4694
5836
|
});
|
|
4695
|
-
if (p && typeof p.then === "function") p.catch(() => {
|
|
5837
|
+
if (p && typeof p.then === "function") await p.catch(() => {
|
|
4696
5838
|
});
|
|
4697
5839
|
} catch {
|
|
5840
|
+
} finally {
|
|
5841
|
+
outstanding.delete(entry);
|
|
4698
5842
|
}
|
|
4699
5843
|
})();
|
|
5844
|
+
entry = send;
|
|
5845
|
+
outstanding.add(send);
|
|
5846
|
+
return send;
|
|
4700
5847
|
};
|
|
4701
5848
|
return {
|
|
4702
5849
|
/**
|
|
@@ -4727,9 +5874,50 @@ function createFirewallLogReporter(opts) {
|
|
|
4727
5874
|
if (!timer) timer = setTimeout(flush, flushMs);
|
|
4728
5875
|
},
|
|
4729
5876
|
flush,
|
|
5877
|
+
/**
|
|
5878
|
+
* Stop, and hand back a wait for what was outstanding.
|
|
5879
|
+
*
|
|
5880
|
+
* One flush sends at most a batch, so the queue is drained a batch at a time. The loop stops as soon
|
|
5881
|
+
* as a pass cannot shrink the queue — with no usable transport there is nothing to wait for, and
|
|
5882
|
+
* spinning would be worse than leaving the records where they are.
|
|
5883
|
+
*/
|
|
4730
5884
|
stop() {
|
|
5885
|
+
if (drainPromise) return drainPromise;
|
|
4731
5886
|
stopped = true;
|
|
4732
|
-
|
|
5887
|
+
if (timer) {
|
|
5888
|
+
clearTimeout(timer);
|
|
5889
|
+
timer = null;
|
|
5890
|
+
}
|
|
5891
|
+
const terminate = () => {
|
|
5892
|
+
ended = true;
|
|
5893
|
+
lifetime?.abort();
|
|
5894
|
+
queue = [];
|
|
5895
|
+
outstanding.clear();
|
|
5896
|
+
};
|
|
5897
|
+
let budget = null;
|
|
5898
|
+
const spent = new Promise((resolve) => {
|
|
5899
|
+
budget = setTimeout(() => {
|
|
5900
|
+
terminate();
|
|
5901
|
+
resolve();
|
|
5902
|
+
}, STOP_BUDGET_MS2);
|
|
5903
|
+
if (typeof budget.unref === "function") budget.unref();
|
|
5904
|
+
});
|
|
5905
|
+
const work = (async () => {
|
|
5906
|
+
while (!ended) {
|
|
5907
|
+
if (outstanding.size > 0) {
|
|
5908
|
+
await Promise.all([...outstanding]);
|
|
5909
|
+
continue;
|
|
5910
|
+
}
|
|
5911
|
+
if (queue.length === 0) break;
|
|
5912
|
+
const before = queue.length;
|
|
5913
|
+
await flush();
|
|
5914
|
+
if (queue.length >= before) break;
|
|
5915
|
+
}
|
|
5916
|
+
})();
|
|
5917
|
+
drainPromise = Promise.race([work, spent]).then(() => {
|
|
5918
|
+
if (budget) clearTimeout(budget);
|
|
5919
|
+
});
|
|
5920
|
+
return drainPromise;
|
|
4733
5921
|
}
|
|
4734
5922
|
};
|
|
4735
5923
|
}
|
|
@@ -4820,8 +6008,8 @@ async function createProtection(options = {}) {
|
|
|
4820
6008
|
fetchImpl: options.fetchImpl
|
|
4821
6009
|
}) : null;
|
|
4822
6010
|
let detections = null;
|
|
6011
|
+
const ruleIdentity = (rule) => ({ id: rule?.id, category: rule?.category });
|
|
4823
6012
|
const onDetect = (detection) => {
|
|
4824
|
-
notify(userOnDetect, detection, "onDetect");
|
|
4825
6013
|
if (detections) detections.record(detection);
|
|
4826
6014
|
if (firewallLog && detection?.mode === "block") {
|
|
4827
6015
|
firewallLog.record({
|
|
@@ -4832,6 +6020,12 @@ async function createProtection(options = {}) {
|
|
|
4832
6020
|
userAgent: detection.userAgent
|
|
4833
6021
|
});
|
|
4834
6022
|
}
|
|
6023
|
+
const { capture: _evidence, ...forCallback } = detection ?? {};
|
|
6024
|
+
notify(
|
|
6025
|
+
userOnDetect,
|
|
6026
|
+
{ ...forCallback, ...detection?.rule ? { rule: ruleIdentity(detection.rule) } : {} },
|
|
6027
|
+
"onDetect"
|
|
6028
|
+
);
|
|
4835
6029
|
};
|
|
4836
6030
|
const store = makeStore(options);
|
|
4837
6031
|
const bootTimeoutMs = Number(options.bootTimeoutMs) > 0 ? Number(options.bootTimeoutMs) : 5e3;
|
|
@@ -4841,29 +6035,58 @@ async function createProtection(options = {}) {
|
|
|
4841
6035
|
notify(onError, new Error(message), "onError");
|
|
4842
6036
|
console.warn(message);
|
|
4843
6037
|
}
|
|
4844
|
-
const
|
|
4845
|
-
|
|
4846
|
-
|
|
4847
|
-
|
|
4848
|
-
|
|
4849
|
-
|
|
4850
|
-
|
|
4851
|
-
|
|
4852
|
-
|
|
4853
|
-
|
|
6038
|
+
const cachedOrigin = async () => {
|
|
6039
|
+
const prior = await store.read();
|
|
6040
|
+
if (prior?.bundle) return "cache";
|
|
6041
|
+
return options.rules ? "bundled" : "empty";
|
|
6042
|
+
};
|
|
6043
|
+
const stateFor = (origin) => reportingState({
|
|
6044
|
+
siteUuid: options.siteUuid,
|
|
6045
|
+
ruleOrigin: origin,
|
|
6046
|
+
hasCredential: Boolean(pulseAuth),
|
|
6047
|
+
configOptOut: options.reportDetections === false
|
|
6048
|
+
});
|
|
6049
|
+
const preFetchState = stateFor(await cachedOrigin()).state;
|
|
6050
|
+
const bundle = await resolveRules(options, store, {
|
|
6051
|
+
timeoutMs: bootTimeoutMs,
|
|
6052
|
+
pulseAuth,
|
|
6053
|
+
detectionState: preFetchState
|
|
6054
|
+
});
|
|
6055
|
+
let detectionReporting = "not-enrolled";
|
|
6056
|
+
let currentOrigin;
|
|
6057
|
+
const applyReportingState = async (origin) => {
|
|
6058
|
+
currentOrigin = origin;
|
|
6059
|
+
const next = stateFor(origin);
|
|
6060
|
+
const changed = next.state !== detectionReporting;
|
|
6061
|
+
detectionReporting = next.state;
|
|
6062
|
+
if (next.reports && !detections) {
|
|
4854
6063
|
detections = createDetectionReporter({
|
|
4855
6064
|
siteUuid: options.siteUuid,
|
|
4856
6065
|
baseUrl: options.pulseRulesUrl,
|
|
4857
6066
|
pulseAuth,
|
|
4858
|
-
// The bundle the guard is actually running, so a hit can be attributed to the rules that
|
|
4859
|
-
// it rather than to whatever is current when the report is read.
|
|
4860
|
-
// see the refresh tick below.
|
|
6067
|
+
// The bundle the guard is actually running, so a hit can be attributed to the rules that
|
|
6068
|
+
// produced it rather than to whatever is current when the report is read.
|
|
4861
6069
|
rulesEtag: (await store.read())?.etag ?? null,
|
|
4862
6070
|
fetchImpl: options.fetchImpl,
|
|
4863
6071
|
flushMs: options.detectionFlushMs
|
|
4864
6072
|
});
|
|
6073
|
+
} else if (!next.reports && detections) {
|
|
6074
|
+
detections.stop();
|
|
6075
|
+
detections = void 0;
|
|
4865
6076
|
}
|
|
4866
|
-
|
|
6077
|
+
if (changed && next.state === "unavailable-no-credential") {
|
|
6078
|
+
const message = "Patchstack: this site is enrolled and running managed rules, but no API credential resolved, so no security event could be delivered. Reporting is off.";
|
|
6079
|
+
notify(onError, new Error(message), "onError");
|
|
6080
|
+
console.warn(message);
|
|
6081
|
+
}
|
|
6082
|
+
return next;
|
|
6083
|
+
};
|
|
6084
|
+
const applyAndAcknowledge = async (origin, declared) => {
|
|
6085
|
+
const settled = await applyReportingState(origin);
|
|
6086
|
+
if (settled.state !== declared && detections) detections.announce(settled.state);
|
|
6087
|
+
return settled;
|
|
6088
|
+
};
|
|
6089
|
+
await applyAndAcknowledge(bundle.source?.origin, preFetchState);
|
|
4867
6090
|
let mode = resolveMode(options, bundle);
|
|
4868
6091
|
let requestRules;
|
|
4869
6092
|
let responseRules;
|
|
@@ -4910,6 +6133,19 @@ async function createProtection(options = {}) {
|
|
|
4910
6133
|
};
|
|
4911
6134
|
const maskFn = typeof options.maskWith === "function" ? options.maskWith : () => typeof options.maskWith === "string" ? options.maskWith : "[REDACTED]";
|
|
4912
6135
|
const ruleMode = (rule) => rule?.enforcement === "dry-run" ? "dry-run" : mode;
|
|
6136
|
+
const planCache = createPlanCache();
|
|
6137
|
+
const evidenceFrom = (result) => {
|
|
6138
|
+
if (!detections || !result?.rule) return void 0;
|
|
6139
|
+
let entry;
|
|
6140
|
+
try {
|
|
6141
|
+
entry = planCache.for(result.rule);
|
|
6142
|
+
} catch (err) {
|
|
6143
|
+
notify(onError, err, "onError");
|
|
6144
|
+
return void 0;
|
|
6145
|
+
}
|
|
6146
|
+
if (!permitsAnything(entry.plan)) return { plan: entry.reference };
|
|
6147
|
+
return { plan: entry.reference, ...captureValues(entry.plan, result.resolver) };
|
|
6148
|
+
};
|
|
4913
6149
|
const decide = (phase, result, block, allow2, ctx = {}) => {
|
|
4914
6150
|
if (!result || !result.blocked) return allow2();
|
|
4915
6151
|
const effectiveMode = ruleMode(result.rule);
|
|
@@ -4924,7 +6160,12 @@ async function createProtection(options = {}) {
|
|
|
4924
6160
|
method: ctx.method,
|
|
4925
6161
|
path: ctx.path,
|
|
4926
6162
|
ip: ctx.ip,
|
|
4927
|
-
|
|
6163
|
+
// Provenance travels with the address. Without it a consumer cannot tell an observed peer from a
|
|
6164
|
+
// value read out of a forwarded header, and `null` from "there was no address to establish".
|
|
6165
|
+
clientIpSource: ctx.clientIpSource,
|
|
6166
|
+
userAgent: ctx.userAgent,
|
|
6167
|
+
// Derived from the reading this decision was made on, before that reading goes out of scope.
|
|
6168
|
+
capture: evidenceFrom(result)
|
|
4928
6169
|
});
|
|
4929
6170
|
return effectiveMode === "block" ? block() : allow2();
|
|
4930
6171
|
};
|
|
@@ -4947,7 +6188,18 @@ async function createProtection(options = {}) {
|
|
|
4947
6188
|
}
|
|
4948
6189
|
if (!result.blocked) continue;
|
|
4949
6190
|
const responseMode = ruleMode(rule);
|
|
4950
|
-
onDetect({
|
|
6191
|
+
onDetect({
|
|
6192
|
+
phase: "response",
|
|
6193
|
+
mode: responseMode,
|
|
6194
|
+
category: rule.category,
|
|
6195
|
+
rule,
|
|
6196
|
+
message: result.message,
|
|
6197
|
+
...requestMetaFromContext(reqCtx),
|
|
6198
|
+
// A response detection names its capture policy like any other. Response sources are never
|
|
6199
|
+
// capturable, so a response-only rule reports a plan that permitted nothing — which is the
|
|
6200
|
+
// policy, and a different fact from a rule that found nothing.
|
|
6201
|
+
capture: evidenceFrom(result)
|
|
6202
|
+
});
|
|
4951
6203
|
if (responseMode !== "block") continue;
|
|
4952
6204
|
if (redactors && redactors.length) {
|
|
4953
6205
|
const spanRedactors = redactors.filter((r) => !r.jsonPath);
|
|
@@ -4984,17 +6236,88 @@ async function createProtection(options = {}) {
|
|
|
4984
6236
|
for (const rule of headerMutations) applyHeaderMutation(headers, rule);
|
|
4985
6237
|
return { verdict: "redact", body, headers };
|
|
4986
6238
|
};
|
|
4987
|
-
const
|
|
6239
|
+
const shapeExpressRequest = (req) => {
|
|
6240
|
+
const client = resolveClientIp({
|
|
6241
|
+
peer: req?.socket?.remoteAddress,
|
|
6242
|
+
headers: req?.headers ?? {},
|
|
6243
|
+
trustedProxy: options.trustedProxy
|
|
6244
|
+
});
|
|
6245
|
+
return {
|
|
6246
|
+
client,
|
|
6247
|
+
shaped: {
|
|
6248
|
+
// Own copies of everything a rule can address. Listed rather than spread, so a field the engine
|
|
6249
|
+
// gains has to be added here deliberately instead of appearing to work by accident.
|
|
6250
|
+
// Through the same gate as the engine's own normalisation: this projection turns whatever it reads
|
|
6251
|
+
// into an OWN property, so reading a polluted prototype here would launder it into evidence that
|
|
6252
|
+
// no later own-property check could tell from the real thing.
|
|
6253
|
+
method: requestField(req, "method"),
|
|
6254
|
+
url: requestField(req, "url"),
|
|
6255
|
+
originalUrl: requestField(req, "originalUrl"),
|
|
6256
|
+
headers: requestField(req, "headers"),
|
|
6257
|
+
query: requestField(req, "query"),
|
|
6258
|
+
body: requestField(req, "body"),
|
|
6259
|
+
files: requestField(req, "files"),
|
|
6260
|
+
cookies: requestField(req, "cookies"),
|
|
6261
|
+
socket: requestField(req, "socket"),
|
|
6262
|
+
// The verbatim body, when a caller kept one. A `raw` rule reads it directly, and the engine
|
|
6263
|
+
// otherwise reconstructs raw by re-serialising the parsed body — which cannot carry what parsing
|
|
6264
|
+
// did not keep: a body that failed to parse at all, a duplicate key where only the last value
|
|
6265
|
+
// survives, or the exact bytes a signature was written against.
|
|
6266
|
+
//
|
|
6267
|
+
// Own property only. Evidence is what this request actually carried: a value reachable through a
|
|
6268
|
+
// polluted prototype is not, and materialising it here would turn it into evidence indistinguish-
|
|
6269
|
+
// able from real bytes — firing every raw rule that matches it. The adapters that capture real
|
|
6270
|
+
// bytes define `_rawBody` on the request object itself.
|
|
6271
|
+
...Object.hasOwn(req ?? {}, "_rawBody") ? { _rawBody: req._rawBody } : {},
|
|
6272
|
+
// The resolved address, and the resolution itself for the consumers downstream.
|
|
6273
|
+
ip: client.ip ?? "",
|
|
6274
|
+
_clientIp: client
|
|
6275
|
+
}
|
|
6276
|
+
};
|
|
6277
|
+
};
|
|
6278
|
+
const screenFetchRequest = async (request) => {
|
|
6279
|
+
let result;
|
|
6280
|
+
let shaped;
|
|
6281
|
+
try {
|
|
6282
|
+
shaped = await fromFetchRequest(request, { trustedProxy: options.trustedProxy });
|
|
6283
|
+
result = engine.evaluate(shaped);
|
|
6284
|
+
} catch (err) {
|
|
6285
|
+
notify(onError, err, "onError");
|
|
6286
|
+
return { blocked: null, client: void 0 };
|
|
6287
|
+
}
|
|
6288
|
+
const blocked = decide(
|
|
6289
|
+
"request",
|
|
6290
|
+
result,
|
|
6291
|
+
() => blockResponse(result, request),
|
|
6292
|
+
() => null,
|
|
6293
|
+
requestMeta(shaped, request)
|
|
6294
|
+
);
|
|
6295
|
+
return { blocked, client: shaped?._clientIp };
|
|
6296
|
+
};
|
|
6297
|
+
const reqContextFromFetch = (request, client) => {
|
|
4988
6298
|
try {
|
|
4989
6299
|
const u = new URL(request.url);
|
|
4990
6300
|
const headers = headerObject(request.headers);
|
|
4991
6301
|
if (!headers.host) headers.host = u.host;
|
|
4992
|
-
|
|
6302
|
+
const resolved = client ?? { ip: null, source: "unavailable" };
|
|
6303
|
+
return {
|
|
6304
|
+
method: request.method,
|
|
6305
|
+
originalUrl: u.pathname + u.search,
|
|
6306
|
+
headers,
|
|
6307
|
+
ip: resolved.ip ?? "",
|
|
6308
|
+
_clientIp: resolved
|
|
6309
|
+
};
|
|
4993
6310
|
} catch {
|
|
4994
6311
|
return void 0;
|
|
4995
6312
|
}
|
|
4996
6313
|
};
|
|
4997
|
-
const reqContextFromNode = (req) => req ? {
|
|
6314
|
+
const reqContextFromNode = (req, client) => req ? {
|
|
6315
|
+
method: req.method,
|
|
6316
|
+
originalUrl: req.url,
|
|
6317
|
+
headers: req.headers || {},
|
|
6318
|
+
ip: client?.ip ?? "",
|
|
6319
|
+
_clientIp: client ?? { ip: null, source: "unavailable" }
|
|
6320
|
+
} : void 0;
|
|
4998
6321
|
const screenResp = async (response, reqCtx) => {
|
|
4999
6322
|
const read = await readTextResponse(response, screenCap);
|
|
5000
6323
|
if (read.skip) {
|
|
@@ -5121,7 +6444,23 @@ async function createProtection(options = {}) {
|
|
|
5121
6444
|
}
|
|
5122
6445
|
if (!result.blocked) return false;
|
|
5123
6446
|
const egressMode = ruleMode(result.rule);
|
|
5124
|
-
|
|
6447
|
+
let egressPath = null;
|
|
6448
|
+
try {
|
|
6449
|
+
const u = new URL(url);
|
|
6450
|
+
egressPath = u.pathname + u.search;
|
|
6451
|
+
} catch {
|
|
6452
|
+
egressPath = typeof url === "string" ? url : null;
|
|
6453
|
+
}
|
|
6454
|
+
onDetect({
|
|
6455
|
+
phase: "egress",
|
|
6456
|
+
mode: egressMode,
|
|
6457
|
+
category: result.rule?.category,
|
|
6458
|
+
rule: result.rule,
|
|
6459
|
+
message: result.message,
|
|
6460
|
+
method: typeof method === "string" ? method : null,
|
|
6461
|
+
path: egressPath,
|
|
6462
|
+
capture: evidenceFrom(result)
|
|
6463
|
+
});
|
|
5125
6464
|
return egressMode === "block";
|
|
5126
6465
|
};
|
|
5127
6466
|
const protection = {
|
|
@@ -5142,28 +6481,26 @@ async function createProtection(options = {}) {
|
|
|
5142
6481
|
},
|
|
5143
6482
|
// Screen a fetch Response through the response-phase rules (redact/block). Used by
|
|
5144
6483
|
// .fetch(), and by the Supabase guard on its forwarded upstream response.
|
|
5145
|
-
|
|
6484
|
+
// A standalone response screen with no request phase of its own — the Supabase guard's forwarded
|
|
6485
|
+
// upstream response. It resolves once here, which is the only resolution for this call.
|
|
6486
|
+
screenResponse: (response, request) => screenResp(
|
|
6487
|
+
response,
|
|
6488
|
+
request ? reqContextFromFetch(
|
|
6489
|
+
request,
|
|
6490
|
+
resolveClientIp({ headers: headerObject(request.headers), trustedProxy: options.trustedProxy })
|
|
6491
|
+
) : void 0
|
|
6492
|
+
),
|
|
5146
6493
|
// (request) => Response | null (null = allow, caller proceeds). Request phase only.
|
|
5147
6494
|
fetchGuard() {
|
|
5148
|
-
return async (request) =>
|
|
5149
|
-
let result;
|
|
5150
|
-
try {
|
|
5151
|
-
result = engine.evaluate(await fromFetchRequest(request));
|
|
5152
|
-
} catch (err) {
|
|
5153
|
-
notify(onError, err, "onError");
|
|
5154
|
-
return null;
|
|
5155
|
-
}
|
|
5156
|
-
return decide("request", result, () => blockResponse(result, request), () => null, fetchRequestMeta(request));
|
|
5157
|
-
};
|
|
6495
|
+
return async (request) => (await screenFetchRequest(request)).blocked;
|
|
5158
6496
|
},
|
|
5159
6497
|
// Wrap a fetch handler: screens the request, then the response (redact/block).
|
|
5160
6498
|
fetch(handler) {
|
|
5161
|
-
const guard = protection.fetchGuard();
|
|
5162
6499
|
return async (request, ...rest) => {
|
|
5163
|
-
const blocked = await
|
|
6500
|
+
const { blocked, client } = await screenFetchRequest(request);
|
|
5164
6501
|
if (blocked) return blocked;
|
|
5165
6502
|
const response = await handler(request, ...rest);
|
|
5166
|
-
return screenResp(response, reqContextFromFetch(request));
|
|
6503
|
+
return screenResp(response, reqContextFromFetch(request, client));
|
|
5167
6504
|
};
|
|
5168
6505
|
},
|
|
5169
6506
|
// Express middleware (request phase; expects express-parsed req.query/req.body).
|
|
@@ -5171,11 +6508,12 @@ async function createProtection(options = {}) {
|
|
|
5171
6508
|
express(exprOptions = {}) {
|
|
5172
6509
|
return (req, res, next) => {
|
|
5173
6510
|
let result;
|
|
6511
|
+
const { shaped, client } = shapeExpressRequest(req);
|
|
5174
6512
|
try {
|
|
5175
|
-
result = engine.evaluate(
|
|
6513
|
+
result = engine.evaluate(shaped);
|
|
5176
6514
|
} catch (err) {
|
|
5177
6515
|
notify(onError, err, "onError");
|
|
5178
|
-
if (exprOptions.screenResponses) wrapNodeResponse(res, reqContextFromNode(req));
|
|
6516
|
+
if (exprOptions.screenResponses) wrapNodeResponse(res, reqContextFromNode(req, client));
|
|
5179
6517
|
return next();
|
|
5180
6518
|
}
|
|
5181
6519
|
decide(
|
|
@@ -5189,10 +6527,10 @@ async function createProtection(options = {}) {
|
|
|
5189
6527
|
}
|
|
5190
6528
|
},
|
|
5191
6529
|
() => {
|
|
5192
|
-
if (exprOptions.screenResponses) wrapNodeResponse(res, reqContextFromNode(req));
|
|
6530
|
+
if (exprOptions.screenResponses) wrapNodeResponse(res, reqContextFromNode(req, client));
|
|
5193
6531
|
next();
|
|
5194
6532
|
},
|
|
5195
|
-
nodeRequestMeta(req)
|
|
6533
|
+
nodeRequestMeta(req, client)
|
|
5196
6534
|
);
|
|
5197
6535
|
};
|
|
5198
6536
|
},
|
|
@@ -5231,7 +6569,7 @@ async function createProtection(options = {}) {
|
|
|
5231
6569
|
let shaped;
|
|
5232
6570
|
let result;
|
|
5233
6571
|
try {
|
|
5234
|
-
shaped = fromNodeRequest(req, rawBody);
|
|
6572
|
+
shaped = fromNodeRequest(req, rawBody, { trustedProxy: options.trustedProxy });
|
|
5235
6573
|
if (parsedBody !== void 0 && parsedBody !== null) shaped.body = parsedBody;
|
|
5236
6574
|
result = engine.evaluate(shaped);
|
|
5237
6575
|
} catch (err) {
|
|
@@ -5253,10 +6591,10 @@ async function createProtection(options = {}) {
|
|
|
5253
6591
|
},
|
|
5254
6592
|
() => {
|
|
5255
6593
|
if (req.body === void 0) req.body = shaped.body;
|
|
5256
|
-
if (nodeOptions.screenResponses) wrapNodeResponse(res, reqContextFromNode(req));
|
|
6594
|
+
if (nodeOptions.screenResponses) wrapNodeResponse(res, reqContextFromNode(req, shaped?._clientIp));
|
|
5257
6595
|
next();
|
|
5258
6596
|
},
|
|
5259
|
-
nodeRequestMeta(req)
|
|
6597
|
+
nodeRequestMeta(req, shaped?._clientIp)
|
|
5260
6598
|
);
|
|
5261
6599
|
}
|
|
5262
6600
|
}
|
|
@@ -5292,9 +6630,15 @@ async function createProtection(options = {}) {
|
|
|
5292
6630
|
notify(onError, err, "onError");
|
|
5293
6631
|
}
|
|
5294
6632
|
}
|
|
5295
|
-
const
|
|
6633
|
+
const declaredState = stateFor(currentOrigin).state;
|
|
6634
|
+
const next = await resolveRules(options, store, {
|
|
6635
|
+
timeoutMs: options.refreshTimeoutMs,
|
|
6636
|
+
pulseAuth,
|
|
6637
|
+
detectionState: declaredState
|
|
6638
|
+
});
|
|
5296
6639
|
mode = resolveMode(options, next);
|
|
5297
6640
|
applyBundle(next);
|
|
6641
|
+
await applyAndAcknowledge(next.source?.origin, declaredState);
|
|
5298
6642
|
if (detections) detections.setRulesEtag((await store.read())?.etag ?? null);
|
|
5299
6643
|
return next.source ?? { ok: true };
|
|
5300
6644
|
};
|
|
@@ -5305,12 +6649,20 @@ async function createProtection(options = {}) {
|
|
|
5305
6649
|
const loop = options.refreshMs > 0 && live ? startRefresh(runRefreshTick, { refreshMs: options.refreshMs, onError }) : null;
|
|
5306
6650
|
protection.stop = () => {
|
|
5307
6651
|
loop?.stop();
|
|
5308
|
-
firewallLog?.stop()
|
|
5309
|
-
|
|
6652
|
+
const outstanding = [firewallLog?.stop(), detections?.stop()].filter(
|
|
6653
|
+
(wait) => wait && typeof wait.then === "function"
|
|
6654
|
+
);
|
|
6655
|
+
return Promise.all(outstanding).then(() => void 0);
|
|
5310
6656
|
};
|
|
5311
6657
|
protection.stopRefresh = protection.stop;
|
|
5312
|
-
protection
|
|
5313
|
-
|
|
6658
|
+
Object.defineProperty(protection, "detectionReporting", {
|
|
6659
|
+
get: () => detectionReporting,
|
|
6660
|
+
enumerable: true
|
|
6661
|
+
});
|
|
6662
|
+
Object.defineProperty(protection, "detectionHealth", {
|
|
6663
|
+
get: () => detections ? () => detections.health() : void 0,
|
|
6664
|
+
enumerable: true
|
|
6665
|
+
});
|
|
5314
6666
|
return protection;
|
|
5315
6667
|
}
|
|
5316
6668
|
function resolveMode(options, bundle) {
|
|
@@ -5491,6 +6843,8 @@ function extractRedactors(rule) {
|
|
|
5491
6843
|
}
|
|
5492
6844
|
} else if ((m.type === "contains" || m.type === "stripos") && m.value != null) {
|
|
5493
6845
|
out.push({ literal: String(m.value) });
|
|
6846
|
+
} else if (m.type === "jwt_claim_equals" && typeof m.claim === "string") {
|
|
6847
|
+
out.push({ jwtClaim: { claim: m.claim, value: String(m.value ?? "") } });
|
|
5494
6848
|
} else if (m.type === "array_key_value" && m.match && isBodyParam(c.parameter)) {
|
|
5495
6849
|
const keys = Array.isArray(m.key) ? m.key : [m.key];
|
|
5496
6850
|
for (const key of keys) {
|
|
@@ -5509,7 +6863,11 @@ function applyRedactors(body, redactors, mask, transform) {
|
|
|
5509
6863
|
let out = body;
|
|
5510
6864
|
for (const r of redactors) {
|
|
5511
6865
|
if (r.re) out = out.replace(r.re, transform ? (m) => transform(m) : mask);
|
|
5512
|
-
else if (r.
|
|
6866
|
+
else if (r.jwtClaim) {
|
|
6867
|
+
for (const token of jwtClaimSpans(out, r.jwtClaim.claim, r.jwtClaim.value)) {
|
|
6868
|
+
out = out.split(token).join(transform ? transform(token) : mask);
|
|
6869
|
+
}
|
|
6870
|
+
} else if (r.literal) {
|
|
5513
6871
|
const re = new RegExp(r.literal.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), "gi");
|
|
5514
6872
|
out = out.replace(re, (m) => transform ? transform(m) : mask);
|
|
5515
6873
|
}
|
|
@@ -5710,30 +7068,48 @@ function defaultOnDetect({ phase, mode, category, rule, message }) {
|
|
|
5710
7068
|
const tag = mode === "block" ? "BLOCK" : "DETECT (dry-run)";
|
|
5711
7069
|
console.warn(`[patchstack] ${tag} phase=${phase ?? "request"} category=${category ?? "?"} rule=${rule?.id ?? "?"} ${message ?? ""}`.trim());
|
|
5712
7070
|
}
|
|
5713
|
-
function
|
|
5714
|
-
if (!
|
|
5715
|
-
|
|
5716
|
-
try {
|
|
5717
|
-
path7 = new URL(request.url).pathname;
|
|
5718
|
-
} catch {
|
|
5719
|
-
path7 = typeof request.url === "string" ? request.url : null;
|
|
5720
|
-
}
|
|
7071
|
+
function requestMetaFromContext(reqCtx) {
|
|
7072
|
+
if (!reqCtx) return {};
|
|
7073
|
+
const client = reqCtx._clientIp ?? { ip: null, source: "unavailable" };
|
|
5721
7074
|
return {
|
|
5722
|
-
method:
|
|
5723
|
-
|
|
5724
|
-
|
|
5725
|
-
|
|
7075
|
+
method: reqCtx.method ?? null,
|
|
7076
|
+
// Path AND query. The reporter is what drops the query's VALUES, keeping its parameter names, so
|
|
7077
|
+
// trimming it here would leave a Fetch or response detection unable to say what was requested.
|
|
7078
|
+
path: typeof reqCtx.originalUrl === "string" ? reqCtx.originalUrl : null,
|
|
7079
|
+
ip: client.ip,
|
|
7080
|
+
clientIpSource: client.source,
|
|
7081
|
+
userAgent: reqCtx.headers?.["user-agent"] ?? null
|
|
5726
7082
|
};
|
|
5727
7083
|
}
|
|
5728
|
-
function
|
|
7084
|
+
function requestMeta(shaped, request) {
|
|
7085
|
+
const client = shaped?._clientIp ?? { ip: null, source: "unavailable" };
|
|
7086
|
+
let path7 = null;
|
|
7087
|
+
let method = null;
|
|
7088
|
+
let userAgent = null;
|
|
7089
|
+
if (request) {
|
|
7090
|
+
try {
|
|
7091
|
+
const u = new URL(request.url);
|
|
7092
|
+
path7 = u.pathname + u.search;
|
|
7093
|
+
} catch {
|
|
7094
|
+
path7 = typeof request.url === "string" ? request.url : null;
|
|
7095
|
+
}
|
|
7096
|
+
method = request.method ?? null;
|
|
7097
|
+
userAgent = request.headers?.get?.("user-agent") ?? null;
|
|
7098
|
+
}
|
|
7099
|
+
return { method, path: path7, ip: client.ip, clientIpSource: client.source, userAgent };
|
|
7100
|
+
}
|
|
7101
|
+
function nodeRequestMeta(req, client) {
|
|
5729
7102
|
if (!req) return {};
|
|
5730
7103
|
const headers = req.headers ?? {};
|
|
5731
7104
|
const ua = headers["user-agent"] ?? headers["User-Agent"];
|
|
5732
|
-
const
|
|
7105
|
+
const resolved = client ?? { ip: null, source: "unavailable" };
|
|
5733
7106
|
return {
|
|
5734
7107
|
method: req.method ?? null,
|
|
5735
7108
|
path: req.originalUrl || req.url || null,
|
|
5736
|
-
|
|
7109
|
+
// Never `req.ip`: under Express's `trust proxy` that is header-derived by a policy this guard has not
|
|
7110
|
+
// verified, and a second derivation could disagree with the one the engine evaluated.
|
|
7111
|
+
ip: resolved.ip,
|
|
7112
|
+
clientIpSource: resolved.source,
|
|
5737
7113
|
userAgent: typeof ua === "string" ? ua : Array.isArray(ua) ? ua[0] : null
|
|
5738
7114
|
};
|
|
5739
7115
|
}
|