@blamejs/core 0.7.18 → 0.7.19
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/CHANGELOG.md +425 -423
- package/README.md +150 -150
- package/bin/blamejs.js +0 -0
- package/index.js +310 -308
- package/lib/api-key.js +660 -660
- package/lib/api-snapshot.js +338 -338
- package/lib/app-shutdown.js +385 -385
- package/lib/app.js +365 -365
- package/lib/archive.js +250 -250
- package/lib/atomic-file.js +544 -544
- package/lib/audit-chain.js +177 -177
- package/lib/audit-sign.js +344 -344
- package/lib/audit-tools.js +677 -677
- package/lib/audit.js +766 -766
- package/lib/auth/jwt-external.js +365 -0
- package/lib/auth/jwt.js +337 -311
- package/lib/auth/lockout.js +436 -436
- package/lib/auth/oauth.js +721 -721
- package/lib/auth/passkey.js +181 -181
- package/lib/auth/password.js +628 -594
- package/lib/backup/bundle.js +217 -217
- package/lib/backup/crypto.js +176 -176
- package/lib/backup/index.js +515 -515
- package/lib/backup/manifest.js +282 -282
- package/lib/break-glass.js +1338 -1338
- package/lib/bundler.js +441 -441
- package/lib/cache-redis.js +256 -256
- package/lib/cache.js +1206 -1206
- package/lib/canonical-json.js +115 -115
- package/lib/chain-writer.js +234 -234
- package/lib/cli-helpers.js +206 -206
- package/lib/cli.js +2334 -2334
- package/lib/cluster-provider-db.js +317 -317
- package/lib/cluster-storage.js +226 -226
- package/lib/cluster.js +703 -703
- package/lib/config-drift.js +301 -301
- package/lib/consent.js +222 -222
- package/lib/constants.js +191 -191
- package/lib/cookies.js +315 -315
- package/lib/credential-hash.js +322 -322
- package/lib/crypto.js +266 -266
- package/lib/csv.js +275 -275
- package/lib/db-declare-row-policy.js +267 -267
- package/lib/db-declare-view.js +420 -420
- package/lib/db-query.js +406 -406
- package/lib/db-schema.js +319 -319
- package/lib/db.js +1288 -1288
- package/lib/deprecate.js +222 -222
- package/lib/dev.js +335 -335
- package/lib/dual-control.js +473 -473
- package/lib/error-page.js +420 -420
- package/lib/external-db-migrate.js +441 -441
- package/lib/external-db.js +1061 -1061
- package/lib/file-type.js +273 -273
- package/lib/forms.js +422 -422
- package/lib/framework-error.js +293 -293
- package/lib/framework-schema.js +717 -717
- package/lib/handlers.js +350 -350
- package/lib/http-client-cookie-jar.js +508 -508
- package/lib/http-client.js +1195 -1195
- package/lib/i18n.js +878 -878
- package/lib/jobs.js +185 -185
- package/lib/log-stream-cloudwatch.js +369 -369
- package/lib/log-stream-local.js +146 -146
- package/lib/log-stream-otlp-grpc.js +410 -410
- package/lib/log-stream-otlp.js +286 -286
- package/lib/log-stream-syslog.js +302 -302
- package/lib/log-stream-webhook.js +199 -199
- package/lib/log-stream.js +330 -330
- package/lib/log.js +500 -500
- package/lib/mail-bounce.js +528 -528
- package/lib/mail-dkim.js +369 -369
- package/lib/mail.js +981 -981
- package/lib/metrics.js +683 -683
- package/lib/middleware/api-encrypt.js +936 -936
- package/lib/middleware/attach-user.js +157 -157
- package/lib/middleware/bearer-auth.js +152 -0
- package/lib/middleware/body-parser.js +1170 -1170
- package/lib/middleware/bot-guard.js +178 -178
- package/lib/middleware/compression.js +452 -452
- package/lib/middleware/cors.js +314 -314
- package/lib/middleware/csp-nonce.js +348 -348
- package/lib/middleware/csrf-protect.js +316 -316
- package/lib/middleware/db-role-for.js +264 -264
- package/lib/middleware/health.js +392 -392
- package/lib/middleware/index.js +82 -79
- package/lib/middleware/rate-limit.js +358 -358
- package/lib/middleware/request-id.js +61 -61
- package/lib/middleware/request-log.js +168 -168
- package/lib/middleware/require-auth.js +104 -104
- package/lib/middleware/security-headers.js +116 -116
- package/lib/middleware/sse.js +166 -166
- package/lib/migrations.js +383 -383
- package/lib/mtls-ca.js +518 -518
- package/lib/mtls-engine-default.js +481 -481
- package/lib/network-dns.js +632 -632
- package/lib/network-heartbeat.js +290 -290
- package/lib/network-nts.js +574 -574
- package/lib/network-proxy.js +265 -265
- package/lib/network-tls.js +328 -328
- package/lib/network.js +233 -233
- package/lib/notify.js +612 -612
- package/lib/ntp-check.js +229 -229
- package/lib/numeric-bounds.js +111 -111
- package/lib/object-store/azure-blob-bucket-ops.js +349 -349
- package/lib/object-store/azure-blob.js +488 -488
- package/lib/object-store/gcs-bucket-ops.js +351 -351
- package/lib/object-store/gcs.js +519 -519
- package/lib/object-store/http-put.js +153 -153
- package/lib/object-store/index.js +197 -197
- package/lib/object-store/sigv4-bucket-ops.js +1092 -1092
- package/lib/object-store/sigv4.js +903 -903
- package/lib/observability.js +151 -151
- package/lib/otel-export.js +269 -269
- package/lib/pagination.js +464 -464
- package/lib/parsers/index.js +80 -80
- package/lib/parsers/safe-env.js +642 -642
- package/lib/parsers/safe-ini.js +292 -292
- package/lib/parsers/safe-toml.js +784 -784
- package/lib/parsers/safe-xml.js +390 -390
- package/lib/parsers/safe-yaml.js +1015 -1015
- package/lib/permissions.js +708 -708
- package/lib/pqc-agent.js +87 -87
- package/lib/pqc-gate.js +279 -279
- package/lib/protobuf-encoder.js +190 -190
- package/lib/protocol-dispatcher.js +161 -161
- package/lib/pubsub-redis.js +167 -167
- package/lib/pubsub.js +429 -429
- package/lib/queue-local.js +476 -476
- package/lib/queue-redis.js +745 -745
- package/lib/queue-sqs.js +319 -319
- package/lib/queue.js +695 -695
- package/lib/redis-client.js +519 -519
- package/lib/request-helpers.js +340 -340
- package/lib/restore-bundle.js +237 -237
- package/lib/restore-rollback.js +259 -259
- package/lib/restore.js +409 -409
- package/lib/retry.js +376 -376
- package/lib/router.js +748 -748
- package/lib/safe-async.js +735 -735
- package/lib/safe-buffer.js +237 -237
- package/lib/safe-json.js +541 -541
- package/lib/safe-schema.js +1266 -1266
- package/lib/safe-url.js +159 -159
- package/lib/scheduler.js +706 -706
- package/lib/security-assert.js +373 -373
- package/lib/seeders.js +618 -618
- package/lib/session.js +535 -478
- package/lib/slug.js +269 -269
- package/lib/ssrf-guard.js +401 -401
- package/lib/storage.js +471 -471
- package/lib/subject.js +281 -281
- package/lib/template.js +791 -791
- package/lib/testing.js +798 -798
- package/lib/time.js +310 -310
- package/lib/totp.js +302 -302
- package/lib/tracing.js +494 -494
- package/lib/uuid.js +132 -132
- package/lib/validate-opts.js +340 -340
- package/lib/vault/index.js +308 -308
- package/lib/vault/rotate.js +784 -784
- package/lib/vault/wrap.js +296 -296
- package/lib/vendor/noble-ciphers.cjs +9 -9
- package/lib/webhook.js +595 -595
- package/lib/websocket.js +1048 -1048
- package/package.json +77 -77
- package/sbom.cyclonedx.json +7 -7
|
@@ -1,178 +1,178 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
/**
|
|
3
|
-
* Bot-guard middleware — fingerprint-based detection of obviously-non-
|
|
4
|
-
* browser requests. Cheap heuristics; not a substitute for proper
|
|
5
|
-
* authentication, but catches drive-by scrapers and most low-effort bots.
|
|
6
|
-
*
|
|
7
|
-
* Heuristics (all combined):
|
|
8
|
-
* - Missing Accept-Language header (real browsers always send one)
|
|
9
|
-
* - Missing Sec-Fetch-Mode header (modern browsers send these on every
|
|
10
|
-
* navigation; absence is suspicious for HTML routes but not API)
|
|
11
|
-
* - User-Agent matches known automation libraries (curl, wget, python-
|
|
12
|
-
* requests, axios, Go-http-client) — operators can add or remove
|
|
13
|
-
* entries via config
|
|
14
|
-
*
|
|
15
|
-
* Options:
|
|
16
|
-
* {
|
|
17
|
-
* mode: 'block' | 'tag' (default 'block')
|
|
18
|
-
* onlyForHtml: true (skip checks for /api/*)
|
|
19
|
-
* allowedAgents: ['<regex>', ...] (allow-list overrides)
|
|
20
|
-
* blockedAgents: ['<regex>', ...] (extra deny-list)
|
|
21
|
-
* skipPaths: ['/healthz', '/api/...'] (always skip)
|
|
22
|
-
* statusOnBlock: 403
|
|
23
|
-
* bodyOnBlock: 'Forbidden'
|
|
24
|
-
* }
|
|
25
|
-
*
|
|
26
|
-
* In 'tag' mode, suspected bots get req.suspectedBot = true and the
|
|
27
|
-
* request continues — apps can rate-limit them differently.
|
|
28
|
-
*
|
|
29
|
-
* Audit: every block emits system.botguard.block with the matched
|
|
30
|
-
* heuristic; every tag emits system.botguard.tag.
|
|
31
|
-
*/
|
|
32
|
-
var DEFAULT_BLOCKED_AGENTS = [
|
|
33
|
-
/^curl\//i,
|
|
34
|
-
/^wget\//i,
|
|
35
|
-
/^python-requests\//i,
|
|
36
|
-
/^python-urllib\//i,
|
|
37
|
-
/^axios\//i,
|
|
38
|
-
/^Go-http-client\//i,
|
|
39
|
-
/^node-fetch\//i,
|
|
40
|
-
/^okhttp\//i,
|
|
41
|
-
/^java\//i,
|
|
42
|
-
/^libwww-perl\//i,
|
|
43
|
-
/^Ruby$/i,
|
|
44
|
-
/^Apache-HttpClient\//i,
|
|
45
|
-
];
|
|
46
|
-
|
|
47
|
-
var lazyRequire = require("../lazy-require");
|
|
48
|
-
var requestHelpers = require("../request-helpers");
|
|
49
|
-
var validateOpts = require("../validate-opts");
|
|
50
|
-
var { defineClass } = require("../framework-error");
|
|
51
|
-
var audit = lazyRequire(function () { return require("../audit"); });
|
|
52
|
-
|
|
53
|
-
var BotGuardError = defineClass("BotGuardError", { alwaysPermanent: true });
|
|
54
|
-
|
|
55
|
-
// allowedAgents / blockedAgents are operator-supplied at create() time
|
|
56
|
-
// (NOT request bytes). The framework requires RegExp instances rather
|
|
57
|
-
// than strings — string-to-regex compilation from an operator-supplied
|
|
58
|
-
// value is an avoidable ReDoS vector at framework boot. Operators
|
|
59
|
-
// constructing patterns dynamically compile at their own call site so
|
|
60
|
-
// the pattern source is visible in their code.
|
|
61
|
-
function _coerceAgentPattern(r, where) {
|
|
62
|
-
if (r instanceof RegExp) return r;
|
|
63
|
-
throw new BotGuardError("bot-guard/bad-pattern",
|
|
64
|
-
where + " must be a RegExp instance; got " + (typeof r) +
|
|
65
|
-
" (compile the pattern at the call site so the source is visible " +
|
|
66
|
-
"in operator code)");
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
// Bot-guard's "trust the proxy header" semantics for actor.ip — the
|
|
70
|
-
// audit event records the apparent source even when behind a CDN, but
|
|
71
|
-
// only when the operator opts in to trustProxy. Without the opt, we
|
|
72
|
-
// stick to socket.remoteAddress so an attacker-forged XFF can't
|
|
73
|
-
// pollute audit attribution.
|
|
74
|
-
function _xffIpFor(trustProxy) {
|
|
75
|
-
return function (req) {
|
|
76
|
-
return requestHelpers.clientIp(req, { trustProxy: trustProxy });
|
|
77
|
-
};
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
function create(opts) {
|
|
81
|
-
opts = opts || {};
|
|
82
|
-
validateOpts(opts, [
|
|
83
|
-
"mode", "onlyForHtml", "allowedAgents", "blockedAgents",
|
|
84
|
-
"skipPaths", "statusOnBlock", "bodyOnBlock", "trustProxy",
|
|
85
|
-
], "middleware.botGuard");
|
|
86
|
-
var trustProxy = opts.trustProxy === true || typeof opts.trustProxy === "number"
|
|
87
|
-
? opts.trustProxy : false;
|
|
88
|
-
var _xffIp = _xffIpFor(trustProxy);
|
|
89
|
-
var mode = opts.mode || "block";
|
|
90
|
-
var onlyForHtml = opts.onlyForHtml !== false;
|
|
91
|
-
var allowedAgents = (opts.allowedAgents || []).map(function (r, i) {
|
|
92
|
-
return _coerceAgentPattern(r, "middleware.botGuard: allowedAgents[" + i + "]");
|
|
93
|
-
});
|
|
94
|
-
var blockedAgents = DEFAULT_BLOCKED_AGENTS.concat((opts.blockedAgents || []).map(function (r, i) {
|
|
95
|
-
return _coerceAgentPattern(r, "middleware.botGuard: blockedAgents[" + i + "]");
|
|
96
|
-
}));
|
|
97
|
-
var skipPaths = opts.skipPaths || [];
|
|
98
|
-
var statusOnBlock = opts.statusOnBlock || 403;
|
|
99
|
-
var bodyOnBlock = opts.bodyOnBlock !== undefined ? opts.bodyOnBlock : "Forbidden";
|
|
100
|
-
|
|
101
|
-
function _shouldSkip(req) {
|
|
102
|
-
var path = req.pathname || req.url || "/";
|
|
103
|
-
for (var i = 0; i < skipPaths.length; i++) {
|
|
104
|
-
if (typeof skipPaths[i] === "string" ? path.indexOf(skipPaths[i]) === 0 : skipPaths[i].test(path)) {
|
|
105
|
-
return true;
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
return false;
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
function _looksLikeApi(req) {
|
|
112
|
-
var path = req.pathname || req.url || "/";
|
|
113
|
-
return /^\/api\//.test(path);
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
function _checkHeuristics(req) {
|
|
117
|
-
var headers = req.headers || {};
|
|
118
|
-
var ua = headers["user-agent"] || "";
|
|
119
|
-
// User-agent allow-list overrides everything
|
|
120
|
-
for (var i = 0; i < allowedAgents.length; i++) {
|
|
121
|
-
if (allowedAgents[i].test(ua)) return null;
|
|
122
|
-
}
|
|
123
|
-
for (var j = 0; j < blockedAgents.length; j++) {
|
|
124
|
-
if (blockedAgents[j].test(ua)) return "blocked-agent";
|
|
125
|
-
}
|
|
126
|
-
if (onlyForHtml && _looksLikeApi(req)) {
|
|
127
|
-
// Skip browser-fingerprint checks for API routes
|
|
128
|
-
return null;
|
|
129
|
-
}
|
|
130
|
-
if (!headers["accept-language"]) return "missing-accept-language";
|
|
131
|
-
if (req.method === "GET" && !headers["sec-fetch-mode"]) return "missing-sec-fetch-mode";
|
|
132
|
-
return null;
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
return function botGuard(req, res, next) {
|
|
136
|
-
if (_shouldSkip(req)) return next();
|
|
137
|
-
var hit = _checkHeuristics(req);
|
|
138
|
-
if (!hit) return next();
|
|
139
|
-
|
|
140
|
-
if (mode === "tag") {
|
|
141
|
-
req.suspectedBot = hit;
|
|
142
|
-
try {
|
|
143
|
-
audit().emit({
|
|
144
|
-
actor: requestHelpers.extractActorContext(req, { ip: _xffIp(req) }),
|
|
145
|
-
action: "system.botguard.tag",
|
|
146
|
-
outcome: "denied",
|
|
147
|
-
reason: hit,
|
|
148
|
-
metadata: { method: req.method, path: req.pathname || req.url, requestId: req.requestId },
|
|
149
|
-
requestId: req.requestId,
|
|
150
|
-
});
|
|
151
|
-
} catch (_e) { /* audit best-effort */ }
|
|
152
|
-
return next();
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
try {
|
|
156
|
-
audit().emit({
|
|
157
|
-
actor: requestHelpers.extractActorContext(req, { ip: _xffIp(req) }),
|
|
158
|
-
action: "system.botguard.block",
|
|
159
|
-
outcome: "denied",
|
|
160
|
-
reason: hit,
|
|
161
|
-
metadata: { method: req.method, path: req.pathname || req.url, requestId: req.requestId },
|
|
162
|
-
requestId: req.requestId,
|
|
163
|
-
});
|
|
164
|
-
} catch (_e) { /* audit best-effort */ }
|
|
165
|
-
|
|
166
|
-
if (res.writableEnded) return;
|
|
167
|
-
if (typeof res.writeHead === "function") {
|
|
168
|
-
res.writeHead(statusOnBlock, { "Content-Type": "text/plain" });
|
|
169
|
-
res.end(bodyOnBlock);
|
|
170
|
-
}
|
|
171
|
-
// Don't call next() — terminate the chain
|
|
172
|
-
};
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
module.exports = {
|
|
176
|
-
create: create,
|
|
177
|
-
DEFAULT_BLOCKED_AGENTS: DEFAULT_BLOCKED_AGENTS,
|
|
178
|
-
};
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Bot-guard middleware — fingerprint-based detection of obviously-non-
|
|
4
|
+
* browser requests. Cheap heuristics; not a substitute for proper
|
|
5
|
+
* authentication, but catches drive-by scrapers and most low-effort bots.
|
|
6
|
+
*
|
|
7
|
+
* Heuristics (all combined):
|
|
8
|
+
* - Missing Accept-Language header (real browsers always send one)
|
|
9
|
+
* - Missing Sec-Fetch-Mode header (modern browsers send these on every
|
|
10
|
+
* navigation; absence is suspicious for HTML routes but not API)
|
|
11
|
+
* - User-Agent matches known automation libraries (curl, wget, python-
|
|
12
|
+
* requests, axios, Go-http-client) — operators can add or remove
|
|
13
|
+
* entries via config
|
|
14
|
+
*
|
|
15
|
+
* Options:
|
|
16
|
+
* {
|
|
17
|
+
* mode: 'block' | 'tag' (default 'block')
|
|
18
|
+
* onlyForHtml: true (skip checks for /api/*)
|
|
19
|
+
* allowedAgents: ['<regex>', ...] (allow-list overrides)
|
|
20
|
+
* blockedAgents: ['<regex>', ...] (extra deny-list)
|
|
21
|
+
* skipPaths: ['/healthz', '/api/...'] (always skip)
|
|
22
|
+
* statusOnBlock: 403
|
|
23
|
+
* bodyOnBlock: 'Forbidden'
|
|
24
|
+
* }
|
|
25
|
+
*
|
|
26
|
+
* In 'tag' mode, suspected bots get req.suspectedBot = true and the
|
|
27
|
+
* request continues — apps can rate-limit them differently.
|
|
28
|
+
*
|
|
29
|
+
* Audit: every block emits system.botguard.block with the matched
|
|
30
|
+
* heuristic; every tag emits system.botguard.tag.
|
|
31
|
+
*/
|
|
32
|
+
var DEFAULT_BLOCKED_AGENTS = [
|
|
33
|
+
/^curl\//i,
|
|
34
|
+
/^wget\//i,
|
|
35
|
+
/^python-requests\//i,
|
|
36
|
+
/^python-urllib\//i,
|
|
37
|
+
/^axios\//i,
|
|
38
|
+
/^Go-http-client\//i,
|
|
39
|
+
/^node-fetch\//i,
|
|
40
|
+
/^okhttp\//i,
|
|
41
|
+
/^java\//i,
|
|
42
|
+
/^libwww-perl\//i,
|
|
43
|
+
/^Ruby$/i,
|
|
44
|
+
/^Apache-HttpClient\//i,
|
|
45
|
+
];
|
|
46
|
+
|
|
47
|
+
var lazyRequire = require("../lazy-require");
|
|
48
|
+
var requestHelpers = require("../request-helpers");
|
|
49
|
+
var validateOpts = require("../validate-opts");
|
|
50
|
+
var { defineClass } = require("../framework-error");
|
|
51
|
+
var audit = lazyRequire(function () { return require("../audit"); });
|
|
52
|
+
|
|
53
|
+
var BotGuardError = defineClass("BotGuardError", { alwaysPermanent: true });
|
|
54
|
+
|
|
55
|
+
// allowedAgents / blockedAgents are operator-supplied at create() time
|
|
56
|
+
// (NOT request bytes). The framework requires RegExp instances rather
|
|
57
|
+
// than strings — string-to-regex compilation from an operator-supplied
|
|
58
|
+
// value is an avoidable ReDoS vector at framework boot. Operators
|
|
59
|
+
// constructing patterns dynamically compile at their own call site so
|
|
60
|
+
// the pattern source is visible in their code.
|
|
61
|
+
function _coerceAgentPattern(r, where) {
|
|
62
|
+
if (r instanceof RegExp) return r;
|
|
63
|
+
throw new BotGuardError("bot-guard/bad-pattern",
|
|
64
|
+
where + " must be a RegExp instance; got " + (typeof r) +
|
|
65
|
+
" (compile the pattern at the call site so the source is visible " +
|
|
66
|
+
"in operator code)");
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Bot-guard's "trust the proxy header" semantics for actor.ip — the
|
|
70
|
+
// audit event records the apparent source even when behind a CDN, but
|
|
71
|
+
// only when the operator opts in to trustProxy. Without the opt, we
|
|
72
|
+
// stick to socket.remoteAddress so an attacker-forged XFF can't
|
|
73
|
+
// pollute audit attribution.
|
|
74
|
+
function _xffIpFor(trustProxy) {
|
|
75
|
+
return function (req) {
|
|
76
|
+
return requestHelpers.clientIp(req, { trustProxy: trustProxy });
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function create(opts) {
|
|
81
|
+
opts = opts || {};
|
|
82
|
+
validateOpts(opts, [
|
|
83
|
+
"mode", "onlyForHtml", "allowedAgents", "blockedAgents",
|
|
84
|
+
"skipPaths", "statusOnBlock", "bodyOnBlock", "trustProxy",
|
|
85
|
+
], "middleware.botGuard");
|
|
86
|
+
var trustProxy = opts.trustProxy === true || typeof opts.trustProxy === "number"
|
|
87
|
+
? opts.trustProxy : false;
|
|
88
|
+
var _xffIp = _xffIpFor(trustProxy);
|
|
89
|
+
var mode = opts.mode || "block";
|
|
90
|
+
var onlyForHtml = opts.onlyForHtml !== false;
|
|
91
|
+
var allowedAgents = (opts.allowedAgents || []).map(function (r, i) {
|
|
92
|
+
return _coerceAgentPattern(r, "middleware.botGuard: allowedAgents[" + i + "]");
|
|
93
|
+
});
|
|
94
|
+
var blockedAgents = DEFAULT_BLOCKED_AGENTS.concat((opts.blockedAgents || []).map(function (r, i) {
|
|
95
|
+
return _coerceAgentPattern(r, "middleware.botGuard: blockedAgents[" + i + "]");
|
|
96
|
+
}));
|
|
97
|
+
var skipPaths = opts.skipPaths || [];
|
|
98
|
+
var statusOnBlock = opts.statusOnBlock || 403;
|
|
99
|
+
var bodyOnBlock = opts.bodyOnBlock !== undefined ? opts.bodyOnBlock : "Forbidden";
|
|
100
|
+
|
|
101
|
+
function _shouldSkip(req) {
|
|
102
|
+
var path = req.pathname || req.url || "/";
|
|
103
|
+
for (var i = 0; i < skipPaths.length; i++) {
|
|
104
|
+
if (typeof skipPaths[i] === "string" ? path.indexOf(skipPaths[i]) === 0 : skipPaths[i].test(path)) {
|
|
105
|
+
return true;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return false;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function _looksLikeApi(req) {
|
|
112
|
+
var path = req.pathname || req.url || "/";
|
|
113
|
+
return /^\/api\//.test(path);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function _checkHeuristics(req) {
|
|
117
|
+
var headers = req.headers || {};
|
|
118
|
+
var ua = headers["user-agent"] || "";
|
|
119
|
+
// User-agent allow-list overrides everything
|
|
120
|
+
for (var i = 0; i < allowedAgents.length; i++) {
|
|
121
|
+
if (allowedAgents[i].test(ua)) return null;
|
|
122
|
+
}
|
|
123
|
+
for (var j = 0; j < blockedAgents.length; j++) {
|
|
124
|
+
if (blockedAgents[j].test(ua)) return "blocked-agent";
|
|
125
|
+
}
|
|
126
|
+
if (onlyForHtml && _looksLikeApi(req)) {
|
|
127
|
+
// Skip browser-fingerprint checks for API routes
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
130
|
+
if (!headers["accept-language"]) return "missing-accept-language";
|
|
131
|
+
if (req.method === "GET" && !headers["sec-fetch-mode"]) return "missing-sec-fetch-mode";
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return function botGuard(req, res, next) {
|
|
136
|
+
if (_shouldSkip(req)) return next();
|
|
137
|
+
var hit = _checkHeuristics(req);
|
|
138
|
+
if (!hit) return next();
|
|
139
|
+
|
|
140
|
+
if (mode === "tag") {
|
|
141
|
+
req.suspectedBot = hit;
|
|
142
|
+
try {
|
|
143
|
+
audit().emit({
|
|
144
|
+
actor: requestHelpers.extractActorContext(req, { ip: _xffIp(req) }),
|
|
145
|
+
action: "system.botguard.tag",
|
|
146
|
+
outcome: "denied",
|
|
147
|
+
reason: hit,
|
|
148
|
+
metadata: { method: req.method, path: req.pathname || req.url, requestId: req.requestId },
|
|
149
|
+
requestId: req.requestId,
|
|
150
|
+
});
|
|
151
|
+
} catch (_e) { /* audit best-effort */ }
|
|
152
|
+
return next();
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
try {
|
|
156
|
+
audit().emit({
|
|
157
|
+
actor: requestHelpers.extractActorContext(req, { ip: _xffIp(req) }),
|
|
158
|
+
action: "system.botguard.block",
|
|
159
|
+
outcome: "denied",
|
|
160
|
+
reason: hit,
|
|
161
|
+
metadata: { method: req.method, path: req.pathname || req.url, requestId: req.requestId },
|
|
162
|
+
requestId: req.requestId,
|
|
163
|
+
});
|
|
164
|
+
} catch (_e) { /* audit best-effort */ }
|
|
165
|
+
|
|
166
|
+
if (res.writableEnded) return;
|
|
167
|
+
if (typeof res.writeHead === "function") {
|
|
168
|
+
res.writeHead(statusOnBlock, { "Content-Type": "text/plain" });
|
|
169
|
+
res.end(bodyOnBlock);
|
|
170
|
+
}
|
|
171
|
+
// Don't call next() — terminate the chain
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
module.exports = {
|
|
176
|
+
create: create,
|
|
177
|
+
DEFAULT_BLOCKED_AGENTS: DEFAULT_BLOCKED_AGENTS,
|
|
178
|
+
};
|