nextjs-secure 0.6.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +185 -1
- package/dist/bot.cjs +1521 -0
- package/dist/bot.cjs.map +1 -0
- package/dist/bot.d.cts +567 -0
- package/dist/bot.d.ts +567 -0
- package/dist/bot.js +1484 -0
- package/dist/bot.js.map +1 -0
- package/dist/index.cjs +1511 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +2 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.js +1479 -2
- package/dist/index.js.map +1 -1
- package/package.json +14 -1
package/dist/bot.cjs
ADDED
|
@@ -0,0 +1,1521 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// src/middleware/bot/user-agent.ts
|
|
4
|
+
var KNOWN_BOT_PATTERNS = [
|
|
5
|
+
// Search Engines - Generally allowed
|
|
6
|
+
{
|
|
7
|
+
name: "Googlebot",
|
|
8
|
+
pattern: /Googlebot|Google-InspectionTool|Storebot-Google|GoogleOther/i,
|
|
9
|
+
category: "search_engine",
|
|
10
|
+
allowed: true,
|
|
11
|
+
description: "Google search crawler"
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
name: "Bingbot",
|
|
15
|
+
pattern: /bingbot|msnbot|BingPreview/i,
|
|
16
|
+
category: "search_engine",
|
|
17
|
+
allowed: true,
|
|
18
|
+
description: "Microsoft Bing crawler"
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
name: "Yahoo Slurp",
|
|
22
|
+
pattern: /Slurp/i,
|
|
23
|
+
category: "search_engine",
|
|
24
|
+
allowed: true,
|
|
25
|
+
description: "Yahoo search crawler"
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
name: "DuckDuckBot",
|
|
29
|
+
pattern: /DuckDuckBot|DuckDuckGo-Favicons-Bot/i,
|
|
30
|
+
category: "search_engine",
|
|
31
|
+
allowed: true,
|
|
32
|
+
description: "DuckDuckGo crawler"
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
name: "Baiduspider",
|
|
36
|
+
pattern: /Baiduspider/i,
|
|
37
|
+
category: "search_engine",
|
|
38
|
+
allowed: true,
|
|
39
|
+
description: "Baidu search crawler"
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
name: "Yandex",
|
|
43
|
+
pattern: /YandexBot|YandexImages|YandexMobileBot/i,
|
|
44
|
+
category: "search_engine",
|
|
45
|
+
allowed: true,
|
|
46
|
+
description: "Yandex search crawler"
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
name: "Applebot",
|
|
50
|
+
pattern: /Applebot/i,
|
|
51
|
+
category: "search_engine",
|
|
52
|
+
allowed: true,
|
|
53
|
+
description: "Apple search crawler"
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
name: "Sogou",
|
|
57
|
+
pattern: /Sogou/i,
|
|
58
|
+
category: "search_engine",
|
|
59
|
+
allowed: true,
|
|
60
|
+
description: "Sogou search crawler"
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
name: "Exabot",
|
|
64
|
+
pattern: /Exabot/i,
|
|
65
|
+
category: "search_engine",
|
|
66
|
+
allowed: true,
|
|
67
|
+
description: "Exalead search crawler"
|
|
68
|
+
},
|
|
69
|
+
// Social Media - Generally allowed
|
|
70
|
+
{
|
|
71
|
+
name: "Facebook",
|
|
72
|
+
pattern: /facebookexternalhit|Facebot|facebookcatalog/i,
|
|
73
|
+
category: "social_media",
|
|
74
|
+
allowed: true,
|
|
75
|
+
description: "Facebook crawler for link previews"
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
name: "Twitter",
|
|
79
|
+
pattern: /Twitterbot/i,
|
|
80
|
+
category: "social_media",
|
|
81
|
+
allowed: true,
|
|
82
|
+
description: "Twitter/X crawler for cards"
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
name: "LinkedIn",
|
|
86
|
+
pattern: /LinkedInBot/i,
|
|
87
|
+
category: "social_media",
|
|
88
|
+
allowed: true,
|
|
89
|
+
description: "LinkedIn crawler for previews"
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
name: "Pinterest",
|
|
93
|
+
pattern: /Pinterest|Pinterestbot/i,
|
|
94
|
+
category: "social_media",
|
|
95
|
+
allowed: true,
|
|
96
|
+
description: "Pinterest crawler"
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
name: "Slack",
|
|
100
|
+
pattern: /Slackbot/i,
|
|
101
|
+
category: "social_media",
|
|
102
|
+
allowed: true,
|
|
103
|
+
description: "Slack link preview bot"
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
name: "Discord",
|
|
107
|
+
pattern: /Discordbot/i,
|
|
108
|
+
category: "social_media",
|
|
109
|
+
allowed: true,
|
|
110
|
+
description: "Discord link preview bot"
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
name: "Telegram",
|
|
114
|
+
pattern: /TelegramBot/i,
|
|
115
|
+
category: "social_media",
|
|
116
|
+
allowed: true,
|
|
117
|
+
description: "Telegram link preview bot"
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
name: "WhatsApp",
|
|
121
|
+
pattern: /WhatsApp/i,
|
|
122
|
+
category: "social_media",
|
|
123
|
+
allowed: true,
|
|
124
|
+
description: "WhatsApp link preview"
|
|
125
|
+
},
|
|
126
|
+
{
|
|
127
|
+
name: "Snapchat",
|
|
128
|
+
pattern: /Snapchat/i,
|
|
129
|
+
category: "social_media",
|
|
130
|
+
allowed: true,
|
|
131
|
+
description: "Snapchat crawler"
|
|
132
|
+
},
|
|
133
|
+
// Monitoring - Generally allowed
|
|
134
|
+
{
|
|
135
|
+
name: "UptimeRobot",
|
|
136
|
+
pattern: /UptimeRobot/i,
|
|
137
|
+
category: "monitoring",
|
|
138
|
+
allowed: true,
|
|
139
|
+
description: "UptimeRobot monitoring"
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
name: "Pingdom",
|
|
143
|
+
pattern: /Pingdom/i,
|
|
144
|
+
category: "monitoring",
|
|
145
|
+
allowed: true,
|
|
146
|
+
description: "Pingdom monitoring"
|
|
147
|
+
},
|
|
148
|
+
{
|
|
149
|
+
name: "StatusCake",
|
|
150
|
+
pattern: /StatusCake/i,
|
|
151
|
+
category: "monitoring",
|
|
152
|
+
allowed: true,
|
|
153
|
+
description: "StatusCake monitoring"
|
|
154
|
+
},
|
|
155
|
+
{
|
|
156
|
+
name: "Site24x7",
|
|
157
|
+
pattern: /Site24x7/i,
|
|
158
|
+
category: "monitoring",
|
|
159
|
+
allowed: true,
|
|
160
|
+
description: "Site24x7 monitoring"
|
|
161
|
+
},
|
|
162
|
+
{
|
|
163
|
+
name: "Datadog",
|
|
164
|
+
pattern: /Datadog/i,
|
|
165
|
+
category: "monitoring",
|
|
166
|
+
allowed: true,
|
|
167
|
+
description: "Datadog synthetic monitoring"
|
|
168
|
+
},
|
|
169
|
+
{
|
|
170
|
+
name: "New Relic",
|
|
171
|
+
pattern: /NewRelicPinger/i,
|
|
172
|
+
category: "monitoring",
|
|
173
|
+
allowed: true,
|
|
174
|
+
description: "New Relic synthetic monitoring"
|
|
175
|
+
},
|
|
176
|
+
{
|
|
177
|
+
name: "Checkly",
|
|
178
|
+
pattern: /Checkly/i,
|
|
179
|
+
category: "monitoring",
|
|
180
|
+
allowed: true,
|
|
181
|
+
description: "Checkly monitoring"
|
|
182
|
+
},
|
|
183
|
+
// SEO Tools - Usually allowed but can be blocked
|
|
184
|
+
{
|
|
185
|
+
name: "Ahrefs",
|
|
186
|
+
pattern: /AhrefsBot|AhrefsSiteAudit/i,
|
|
187
|
+
category: "seo",
|
|
188
|
+
allowed: false,
|
|
189
|
+
description: "Ahrefs SEO crawler"
|
|
190
|
+
},
|
|
191
|
+
{
|
|
192
|
+
name: "Semrush",
|
|
193
|
+
pattern: /SemrushBot/i,
|
|
194
|
+
category: "seo",
|
|
195
|
+
allowed: false,
|
|
196
|
+
description: "Semrush SEO crawler"
|
|
197
|
+
},
|
|
198
|
+
{
|
|
199
|
+
name: "Moz",
|
|
200
|
+
pattern: /rogerbot|DotBot/i,
|
|
201
|
+
category: "seo",
|
|
202
|
+
allowed: false,
|
|
203
|
+
description: "Moz SEO crawler"
|
|
204
|
+
},
|
|
205
|
+
{
|
|
206
|
+
name: "Majestic",
|
|
207
|
+
pattern: /MJ12bot/i,
|
|
208
|
+
category: "seo",
|
|
209
|
+
allowed: false,
|
|
210
|
+
description: "Majestic SEO crawler"
|
|
211
|
+
},
|
|
212
|
+
{
|
|
213
|
+
name: "Screaming Frog",
|
|
214
|
+
pattern: /Screaming Frog/i,
|
|
215
|
+
category: "seo",
|
|
216
|
+
allowed: false,
|
|
217
|
+
description: "Screaming Frog SEO Spider"
|
|
218
|
+
},
|
|
219
|
+
// AI Crawlers - Configurable
|
|
220
|
+
{
|
|
221
|
+
name: "GPTBot",
|
|
222
|
+
pattern: /GPTBot/i,
|
|
223
|
+
category: "ai_crawler",
|
|
224
|
+
allowed: false,
|
|
225
|
+
description: "OpenAI GPT training crawler"
|
|
226
|
+
},
|
|
227
|
+
{
|
|
228
|
+
name: "ChatGPT-User",
|
|
229
|
+
pattern: /ChatGPT-User/i,
|
|
230
|
+
category: "ai_crawler",
|
|
231
|
+
allowed: false,
|
|
232
|
+
description: "ChatGPT user browsing"
|
|
233
|
+
},
|
|
234
|
+
{
|
|
235
|
+
name: "Claude-Web",
|
|
236
|
+
pattern: /Claude-Web|ClaudeBot|anthropic-ai/i,
|
|
237
|
+
category: "ai_crawler",
|
|
238
|
+
allowed: false,
|
|
239
|
+
description: "Anthropic Claude crawler"
|
|
240
|
+
},
|
|
241
|
+
{
|
|
242
|
+
name: "Bytespider",
|
|
243
|
+
pattern: /Bytespider/i,
|
|
244
|
+
category: "ai_crawler",
|
|
245
|
+
allowed: false,
|
|
246
|
+
description: "ByteDance AI crawler"
|
|
247
|
+
},
|
|
248
|
+
{
|
|
249
|
+
name: "CCBot",
|
|
250
|
+
pattern: /CCBot/i,
|
|
251
|
+
category: "ai_crawler",
|
|
252
|
+
allowed: false,
|
|
253
|
+
description: "Common Crawl bot"
|
|
254
|
+
},
|
|
255
|
+
{
|
|
256
|
+
name: "Google-Extended",
|
|
257
|
+
pattern: /Google-Extended/i,
|
|
258
|
+
category: "ai_crawler",
|
|
259
|
+
allowed: false,
|
|
260
|
+
description: "Google AI training crawler"
|
|
261
|
+
},
|
|
262
|
+
{
|
|
263
|
+
name: "Cohere-ai",
|
|
264
|
+
pattern: /cohere-ai/i,
|
|
265
|
+
category: "ai_crawler",
|
|
266
|
+
allowed: false,
|
|
267
|
+
description: "Cohere AI crawler"
|
|
268
|
+
},
|
|
269
|
+
{
|
|
270
|
+
name: "PerplexityBot",
|
|
271
|
+
pattern: /PerplexityBot/i,
|
|
272
|
+
category: "ai_crawler",
|
|
273
|
+
allowed: false,
|
|
274
|
+
description: "Perplexity AI crawler"
|
|
275
|
+
},
|
|
276
|
+
// Feed Readers - Generally allowed
|
|
277
|
+
{
|
|
278
|
+
name: "Feedly",
|
|
279
|
+
pattern: /Feedly/i,
|
|
280
|
+
category: "feed_reader",
|
|
281
|
+
allowed: true,
|
|
282
|
+
description: "Feedly RSS reader"
|
|
283
|
+
},
|
|
284
|
+
{
|
|
285
|
+
name: "Feedbin",
|
|
286
|
+
pattern: /Feedbin/i,
|
|
287
|
+
category: "feed_reader",
|
|
288
|
+
allowed: true,
|
|
289
|
+
description: "Feedbin RSS reader"
|
|
290
|
+
},
|
|
291
|
+
{
|
|
292
|
+
name: "NewsBlur",
|
|
293
|
+
pattern: /NewsBlur/i,
|
|
294
|
+
category: "feed_reader",
|
|
295
|
+
allowed: true,
|
|
296
|
+
description: "NewsBlur RSS reader"
|
|
297
|
+
},
|
|
298
|
+
// Link Previews - Generally allowed
|
|
299
|
+
{
|
|
300
|
+
name: "Embedly",
|
|
301
|
+
pattern: /Embedly/i,
|
|
302
|
+
category: "preview",
|
|
303
|
+
allowed: true,
|
|
304
|
+
description: "Embedly link preview"
|
|
305
|
+
},
|
|
306
|
+
{
|
|
307
|
+
name: "Iframely",
|
|
308
|
+
pattern: /Iframely/i,
|
|
309
|
+
category: "preview",
|
|
310
|
+
allowed: true,
|
|
311
|
+
description: "Iframely link preview"
|
|
312
|
+
},
|
|
313
|
+
// Security Scanners - Block by default
|
|
314
|
+
{
|
|
315
|
+
name: "Nessus",
|
|
316
|
+
pattern: /Nessus/i,
|
|
317
|
+
category: "security",
|
|
318
|
+
allowed: false,
|
|
319
|
+
description: "Nessus vulnerability scanner"
|
|
320
|
+
},
|
|
321
|
+
{
|
|
322
|
+
name: "Nikto",
|
|
323
|
+
pattern: /Nikto/i,
|
|
324
|
+
category: "security",
|
|
325
|
+
allowed: false,
|
|
326
|
+
description: "Nikto web scanner"
|
|
327
|
+
},
|
|
328
|
+
{
|
|
329
|
+
name: "sqlmap",
|
|
330
|
+
pattern: /sqlmap/i,
|
|
331
|
+
category: "security",
|
|
332
|
+
allowed: false,
|
|
333
|
+
description: "sqlmap SQL injection tool"
|
|
334
|
+
},
|
|
335
|
+
{
|
|
336
|
+
name: "WPScan",
|
|
337
|
+
pattern: /WPScan/i,
|
|
338
|
+
category: "security",
|
|
339
|
+
allowed: false,
|
|
340
|
+
description: "WordPress vulnerability scanner"
|
|
341
|
+
},
|
|
342
|
+
{
|
|
343
|
+
name: "Acunetix",
|
|
344
|
+
pattern: /Acunetix/i,
|
|
345
|
+
category: "security",
|
|
346
|
+
allowed: false,
|
|
347
|
+
description: "Acunetix vulnerability scanner"
|
|
348
|
+
},
|
|
349
|
+
// Known Scrapers - Block
|
|
350
|
+
{
|
|
351
|
+
name: "Scrapy",
|
|
352
|
+
pattern: /Scrapy/i,
|
|
353
|
+
category: "scraper",
|
|
354
|
+
allowed: false,
|
|
355
|
+
description: "Scrapy web scraper"
|
|
356
|
+
},
|
|
357
|
+
{
|
|
358
|
+
name: "HTTrack",
|
|
359
|
+
pattern: /HTTrack/i,
|
|
360
|
+
category: "scraper",
|
|
361
|
+
allowed: false,
|
|
362
|
+
description: "HTTrack website copier"
|
|
363
|
+
},
|
|
364
|
+
{
|
|
365
|
+
name: "WebCopier",
|
|
366
|
+
pattern: /WebCopier/i,
|
|
367
|
+
category: "scraper",
|
|
368
|
+
allowed: false,
|
|
369
|
+
description: "WebCopier tool"
|
|
370
|
+
},
|
|
371
|
+
{
|
|
372
|
+
name: "SiteSnagger",
|
|
373
|
+
pattern: /SiteSnagger/i,
|
|
374
|
+
category: "scraper",
|
|
375
|
+
allowed: false,
|
|
376
|
+
description: "SiteSnagger downloader"
|
|
377
|
+
},
|
|
378
|
+
// Spam Bots - Always block
|
|
379
|
+
{
|
|
380
|
+
name: "Spam Bot",
|
|
381
|
+
pattern: /spam|harvester|extractor|collect/i,
|
|
382
|
+
category: "spam",
|
|
383
|
+
allowed: false,
|
|
384
|
+
description: "Generic spam bot pattern"
|
|
385
|
+
},
|
|
386
|
+
{
|
|
387
|
+
name: "Email Harvester",
|
|
388
|
+
pattern: /email.*harvest|harvest.*email/i,
|
|
389
|
+
category: "spam",
|
|
390
|
+
allowed: false,
|
|
391
|
+
description: "Email harvesting bot"
|
|
392
|
+
},
|
|
393
|
+
// Malicious - Always block
|
|
394
|
+
{
|
|
395
|
+
name: "Malicious Generic",
|
|
396
|
+
pattern: /masscan|ZmEu|morfeus|nmap/i,
|
|
397
|
+
category: "malicious",
|
|
398
|
+
allowed: false,
|
|
399
|
+
description: "Known malicious tools"
|
|
400
|
+
},
|
|
401
|
+
{
|
|
402
|
+
name: "Vulnerability Scanner",
|
|
403
|
+
pattern: /havij|w3af|webscarab/i,
|
|
404
|
+
category: "malicious",
|
|
405
|
+
allowed: false,
|
|
406
|
+
description: "Vulnerability scanning tools"
|
|
407
|
+
},
|
|
408
|
+
// Generic Bot Pattern
|
|
409
|
+
{
|
|
410
|
+
name: "Generic Bot",
|
|
411
|
+
pattern: /bot|crawl|spider|scrape|fetch/i,
|
|
412
|
+
category: "unknown",
|
|
413
|
+
allowed: false,
|
|
414
|
+
description: "Generic bot pattern"
|
|
415
|
+
},
|
|
416
|
+
{
|
|
417
|
+
name: "HTTP Library",
|
|
418
|
+
pattern: /python-requests|python-urllib|curl|wget|axios|node-fetch|got\//i,
|
|
419
|
+
category: "unknown",
|
|
420
|
+
allowed: false,
|
|
421
|
+
description: "HTTP library user agent"
|
|
422
|
+
}
|
|
423
|
+
];
|
|
424
|
+
var DEFAULT_ALLOWED_CATEGORIES = [
|
|
425
|
+
"search_engine",
|
|
426
|
+
"social_media",
|
|
427
|
+
"monitoring",
|
|
428
|
+
"feed_reader",
|
|
429
|
+
"preview"
|
|
430
|
+
];
|
|
431
|
+
var DEFAULT_ALLOWED_BOTS = [
|
|
432
|
+
"Googlebot",
|
|
433
|
+
"Bingbot",
|
|
434
|
+
"Yahoo Slurp",
|
|
435
|
+
"DuckDuckBot",
|
|
436
|
+
"Applebot",
|
|
437
|
+
"Facebook",
|
|
438
|
+
"Twitter",
|
|
439
|
+
"LinkedIn",
|
|
440
|
+
"Slack",
|
|
441
|
+
"Discord",
|
|
442
|
+
"UptimeRobot",
|
|
443
|
+
"Pingdom"
|
|
444
|
+
];
|
|
445
|
+
function analyzeUserAgent(userAgent, options = {}) {
|
|
446
|
+
const {
|
|
447
|
+
blockAllBots = false,
|
|
448
|
+
allowCategories = DEFAULT_ALLOWED_CATEGORIES,
|
|
449
|
+
allowList = DEFAULT_ALLOWED_BOTS,
|
|
450
|
+
blockList = [],
|
|
451
|
+
customPatterns = [],
|
|
452
|
+
blockEmptyUA = true,
|
|
453
|
+
blockSuspiciousUA = true
|
|
454
|
+
} = options;
|
|
455
|
+
if (!userAgent || userAgent.trim() === "") {
|
|
456
|
+
return {
|
|
457
|
+
isBot: blockEmptyUA,
|
|
458
|
+
category: "unknown",
|
|
459
|
+
confidence: blockEmptyUA ? 0.9 : 0,
|
|
460
|
+
reason: "Empty User-Agent",
|
|
461
|
+
userAgent: userAgent || ""
|
|
462
|
+
};
|
|
463
|
+
}
|
|
464
|
+
const allPatterns = [...customPatterns, ...KNOWN_BOT_PATTERNS];
|
|
465
|
+
let matchedPattern = null;
|
|
466
|
+
for (const pattern of allPatterns) {
|
|
467
|
+
if (pattern.pattern.test(userAgent)) {
|
|
468
|
+
matchedPattern = pattern;
|
|
469
|
+
break;
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
if (!matchedPattern && blockSuspiciousUA && isSuspiciousUA(userAgent)) {
|
|
473
|
+
return {
|
|
474
|
+
isBot: true,
|
|
475
|
+
category: "unknown",
|
|
476
|
+
confidence: 0.8,
|
|
477
|
+
reason: "Suspicious User-Agent pattern",
|
|
478
|
+
userAgent
|
|
479
|
+
};
|
|
480
|
+
}
|
|
481
|
+
if (!matchedPattern) {
|
|
482
|
+
return {
|
|
483
|
+
isBot: false,
|
|
484
|
+
confidence: 0.1,
|
|
485
|
+
reason: "No bot pattern matched",
|
|
486
|
+
userAgent
|
|
487
|
+
};
|
|
488
|
+
}
|
|
489
|
+
if (blockList.includes(matchedPattern.name)) {
|
|
490
|
+
return {
|
|
491
|
+
isBot: true,
|
|
492
|
+
category: matchedPattern.category,
|
|
493
|
+
name: matchedPattern.name,
|
|
494
|
+
confidence: 0.95,
|
|
495
|
+
reason: `Blocked bot: ${matchedPattern.name}`,
|
|
496
|
+
userAgent
|
|
497
|
+
};
|
|
498
|
+
}
|
|
499
|
+
if (blockAllBots) {
|
|
500
|
+
return {
|
|
501
|
+
isBot: true,
|
|
502
|
+
category: matchedPattern.category,
|
|
503
|
+
name: matchedPattern.name,
|
|
504
|
+
confidence: 0.9,
|
|
505
|
+
reason: `Bot blocked (blockAllBots mode): ${matchedPattern.name}`,
|
|
506
|
+
userAgent
|
|
507
|
+
};
|
|
508
|
+
}
|
|
509
|
+
if (allowList.includes(matchedPattern.name)) {
|
|
510
|
+
return {
|
|
511
|
+
isBot: true,
|
|
512
|
+
category: matchedPattern.category,
|
|
513
|
+
name: matchedPattern.name,
|
|
514
|
+
confidence: 0.95,
|
|
515
|
+
reason: `Allowed bot: ${matchedPattern.name}`,
|
|
516
|
+
userAgent
|
|
517
|
+
};
|
|
518
|
+
}
|
|
519
|
+
if (allowCategories.includes(matchedPattern.category)) {
|
|
520
|
+
return {
|
|
521
|
+
isBot: true,
|
|
522
|
+
category: matchedPattern.category,
|
|
523
|
+
name: matchedPattern.name,
|
|
524
|
+
confidence: 0.9,
|
|
525
|
+
reason: `Allowed category: ${matchedPattern.category}`,
|
|
526
|
+
userAgent
|
|
527
|
+
};
|
|
528
|
+
}
|
|
529
|
+
return {
|
|
530
|
+
isBot: true,
|
|
531
|
+
category: matchedPattern.category,
|
|
532
|
+
name: matchedPattern.name,
|
|
533
|
+
confidence: 0.85,
|
|
534
|
+
reason: matchedPattern.allowed ? `Allowed bot: ${matchedPattern.name}` : `Blocked bot: ${matchedPattern.name}`,
|
|
535
|
+
userAgent
|
|
536
|
+
};
|
|
537
|
+
}
|
|
538
|
+
function isSuspiciousUA(userAgent) {
|
|
539
|
+
if (userAgent.length < 10) {
|
|
540
|
+
return true;
|
|
541
|
+
}
|
|
542
|
+
if (/^[0-9a-f]{8,}$/i.test(userAgent)) {
|
|
543
|
+
return true;
|
|
544
|
+
}
|
|
545
|
+
const hasBrowserIndicator = /Mozilla|Chrome|Safari|Firefox|Edge|Opera|MSIE|Trident/i.test(userAgent);
|
|
546
|
+
const hasOSIndicator = /Windows|Mac|Linux|Android|iOS|iPhone|iPad/i.test(userAgent);
|
|
547
|
+
if (hasBrowserIndicator && !hasOSIndicator && userAgent.length < 50) {
|
|
548
|
+
return true;
|
|
549
|
+
}
|
|
550
|
+
if (/Chrome\/[0-4]\./i.test(userAgent) || /Firefox\/[0-3]\./i.test(userAgent)) {
|
|
551
|
+
return true;
|
|
552
|
+
}
|
|
553
|
+
return false;
|
|
554
|
+
}
|
|
555
|
+
function isBotAllowed(botName, options = {}) {
|
|
556
|
+
const {
|
|
557
|
+
blockAllBots = false,
|
|
558
|
+
allowCategories = DEFAULT_ALLOWED_CATEGORIES,
|
|
559
|
+
allowList = DEFAULT_ALLOWED_BOTS,
|
|
560
|
+
blockList = []
|
|
561
|
+
} = options;
|
|
562
|
+
if (blockList.includes(botName)) {
|
|
563
|
+
return false;
|
|
564
|
+
}
|
|
565
|
+
if (blockAllBots) {
|
|
566
|
+
return false;
|
|
567
|
+
}
|
|
568
|
+
if (allowList.includes(botName)) {
|
|
569
|
+
return true;
|
|
570
|
+
}
|
|
571
|
+
const pattern = KNOWN_BOT_PATTERNS.find((p) => p.name === botName);
|
|
572
|
+
if (pattern && allowCategories.includes(pattern.category)) {
|
|
573
|
+
return true;
|
|
574
|
+
}
|
|
575
|
+
return pattern?.allowed ?? false;
|
|
576
|
+
}
|
|
577
|
+
function getBotsByCategory(category) {
|
|
578
|
+
return KNOWN_BOT_PATTERNS.filter((p) => p.category === category);
|
|
579
|
+
}
|
|
580
|
+
function createBotPattern(name, pattern, category, allowed = false, description) {
|
|
581
|
+
return {
|
|
582
|
+
name,
|
|
583
|
+
pattern: typeof pattern === "string" ? new RegExp(pattern, "i") : pattern,
|
|
584
|
+
category,
|
|
585
|
+
allowed,
|
|
586
|
+
description
|
|
587
|
+
};
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
// src/middleware/bot/honeypot.ts
|
|
591
|
+
var DEFAULT_HONEYPOT_FIELDS = [
|
|
592
|
+
"_hp_email",
|
|
593
|
+
"_hp_name",
|
|
594
|
+
"_hp_website",
|
|
595
|
+
"_hp_phone",
|
|
596
|
+
"_hp_address",
|
|
597
|
+
"email_confirm",
|
|
598
|
+
"website_url",
|
|
599
|
+
"fax_number"
|
|
600
|
+
];
|
|
601
|
+
var DEFAULT_HONEYPOT_OPTIONS = {
|
|
602
|
+
fieldName: "_hp_email",
|
|
603
|
+
additionalFields: [],
|
|
604
|
+
checkIn: ["body", "query"],
|
|
605
|
+
validate: void 0
|
|
606
|
+
};
|
|
607
|
+
async function checkHoneypot(req, options = {}) {
|
|
608
|
+
const {
|
|
609
|
+
fieldName = DEFAULT_HONEYPOT_OPTIONS.fieldName,
|
|
610
|
+
additionalFields = DEFAULT_HONEYPOT_OPTIONS.additionalFields,
|
|
611
|
+
checkIn = DEFAULT_HONEYPOT_OPTIONS.checkIn,
|
|
612
|
+
validate
|
|
613
|
+
} = options;
|
|
614
|
+
const allFields = [fieldName, ...additionalFields];
|
|
615
|
+
const filledFields = [];
|
|
616
|
+
if (checkIn.includes("query")) {
|
|
617
|
+
const url = new URL(req.url);
|
|
618
|
+
for (const field of allFields) {
|
|
619
|
+
const value = url.searchParams.get(field);
|
|
620
|
+
if (value !== null && value !== "") {
|
|
621
|
+
filledFields.push(`query:${field}`);
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
if (checkIn.includes("body") && hasBody(req)) {
|
|
626
|
+
try {
|
|
627
|
+
const body = await getRequestBody(req);
|
|
628
|
+
if (body && typeof body === "object") {
|
|
629
|
+
for (const field of allFields) {
|
|
630
|
+
const value = body[field];
|
|
631
|
+
if (value !== void 0 && value !== null && value !== "") {
|
|
632
|
+
if (validate && !validate(value)) {
|
|
633
|
+
continue;
|
|
634
|
+
}
|
|
635
|
+
filledFields.push(`body:${field}`);
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
} catch {
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
if (checkIn.includes("headers")) {
|
|
643
|
+
for (const field of allFields) {
|
|
644
|
+
const headerName = `x-${field.replace(/_/g, "-")}`;
|
|
645
|
+
const value = req.headers.get(headerName);
|
|
646
|
+
if (value !== null && value !== "") {
|
|
647
|
+
filledFields.push(`header:${headerName}`);
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
if (filledFields.length > 0) {
|
|
652
|
+
return {
|
|
653
|
+
isBot: true,
|
|
654
|
+
category: "spam",
|
|
655
|
+
confidence: 0.95,
|
|
656
|
+
reason: `Honeypot triggered: ${filledFields.join(", ")}`,
|
|
657
|
+
ip: getClientIP(req)
|
|
658
|
+
};
|
|
659
|
+
}
|
|
660
|
+
return {
|
|
661
|
+
isBot: false,
|
|
662
|
+
confidence: 0,
|
|
663
|
+
reason: "Honeypot check passed"
|
|
664
|
+
};
|
|
665
|
+
}
|
|
666
|
+
function withHoneypot(handler, options = {}) {
|
|
667
|
+
return async (req, ctx) => {
|
|
668
|
+
const result = await checkHoneypot(req, options);
|
|
669
|
+
if (result.isBot) {
|
|
670
|
+
return new Response(
|
|
671
|
+
JSON.stringify({
|
|
672
|
+
success: false,
|
|
673
|
+
error: "Request rejected"
|
|
674
|
+
}),
|
|
675
|
+
{
|
|
676
|
+
status: 403,
|
|
677
|
+
headers: { "Content-Type": "application/json" }
|
|
678
|
+
}
|
|
679
|
+
);
|
|
680
|
+
}
|
|
681
|
+
return handler(req, ctx);
|
|
682
|
+
};
|
|
683
|
+
}
|
|
684
|
+
function generateHoneypotHTML(options = {}) {
|
|
685
|
+
const {
|
|
686
|
+
fieldName = DEFAULT_HONEYPOT_OPTIONS.fieldName,
|
|
687
|
+
additionalFields = []
|
|
688
|
+
} = options;
|
|
689
|
+
const allFields = [fieldName, ...additionalFields];
|
|
690
|
+
const fields = allFields.map((field) => {
|
|
691
|
+
const style = getRandomHidingStyle();
|
|
692
|
+
const labelText = humanizeFieldName(field);
|
|
693
|
+
return `
|
|
694
|
+
<div style="${style}" aria-hidden="true" tabindex="-1">
|
|
695
|
+
<label for="${field}">${labelText}</label>
|
|
696
|
+
<input
|
|
697
|
+
type="text"
|
|
698
|
+
id="${field}"
|
|
699
|
+
name="${field}"
|
|
700
|
+
autocomplete="off"
|
|
701
|
+
tabindex="-1"
|
|
702
|
+
/>
|
|
703
|
+
</div>`;
|
|
704
|
+
}).join("\n");
|
|
705
|
+
return `<!-- Honeypot fields - Do not fill these -->
|
|
706
|
+
${fields}`;
|
|
707
|
+
}
|
|
708
|
+
function generateHoneypotCSS(options = {}) {
|
|
709
|
+
const {
|
|
710
|
+
fieldName = DEFAULT_HONEYPOT_OPTIONS.fieldName,
|
|
711
|
+
additionalFields = []
|
|
712
|
+
} = options;
|
|
713
|
+
const allFields = [fieldName, ...additionalFields];
|
|
714
|
+
const selectors = allFields.map((f) => `#${f}`).join(", ");
|
|
715
|
+
return `
|
|
716
|
+
/* Honeypot field hiding */
|
|
717
|
+
${selectors} {
|
|
718
|
+
position: absolute !important;
|
|
719
|
+
left: -9999px !important;
|
|
720
|
+
top: -9999px !important;
|
|
721
|
+
opacity: 0 !important;
|
|
722
|
+
height: 0 !important;
|
|
723
|
+
width: 0 !important;
|
|
724
|
+
z-index: -1 !important;
|
|
725
|
+
pointer-events: none !important;
|
|
726
|
+
}
|
|
727
|
+
`.trim();
|
|
728
|
+
}
|
|
729
|
+
function hasBody(req) {
|
|
730
|
+
const method = req.method.toUpperCase();
|
|
731
|
+
return ["POST", "PUT", "PATCH", "DELETE"].includes(method);
|
|
732
|
+
}
|
|
733
|
+
async function getRequestBody(req) {
|
|
734
|
+
try {
|
|
735
|
+
const contentType = req.headers.get("content-type") || "";
|
|
736
|
+
if (contentType.includes("application/json")) {
|
|
737
|
+
const cloned = req.clone();
|
|
738
|
+
return await cloned.json();
|
|
739
|
+
}
|
|
740
|
+
if (contentType.includes("application/x-www-form-urlencoded")) {
|
|
741
|
+
const cloned = req.clone();
|
|
742
|
+
const text = await cloned.text();
|
|
743
|
+
return Object.fromEntries(new URLSearchParams(text));
|
|
744
|
+
}
|
|
745
|
+
if (contentType.includes("multipart/form-data")) {
|
|
746
|
+
const cloned = req.clone();
|
|
747
|
+
const formData = await cloned.formData();
|
|
748
|
+
const obj = {};
|
|
749
|
+
formData.forEach((value, key) => {
|
|
750
|
+
obj[key] = value;
|
|
751
|
+
});
|
|
752
|
+
return obj;
|
|
753
|
+
}
|
|
754
|
+
return null;
|
|
755
|
+
} catch {
|
|
756
|
+
return null;
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
function getClientIP(req) {
|
|
760
|
+
return req.headers.get("x-forwarded-for")?.split(",")[0]?.trim() || req.headers.get("x-real-ip") || req.headers.get("cf-connecting-ip") || "unknown";
|
|
761
|
+
}
|
|
762
|
+
function getRandomHidingStyle() {
|
|
763
|
+
const styles = [
|
|
764
|
+
"position: absolute; left: -9999px; top: -9999px;",
|
|
765
|
+
"position: fixed; left: -100vw; visibility: hidden;",
|
|
766
|
+
"opacity: 0; height: 0; width: 0; overflow: hidden;",
|
|
767
|
+
"clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0;",
|
|
768
|
+
"transform: scale(0); position: absolute;"
|
|
769
|
+
];
|
|
770
|
+
return styles[Math.floor(Math.random() * styles.length)];
|
|
771
|
+
}
|
|
772
|
+
function humanizeFieldName(field) {
|
|
773
|
+
return field.replace(/^_hp_/, "").replace(/_/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
// src/middleware/bot/behavior.ts
|
|
777
|
+
var DEFAULT_BEHAVIOR_OPTIONS = {
|
|
778
|
+
minRequestInterval: 100,
|
|
779
|
+
maxRequestsPerSecond: 10,
|
|
780
|
+
windowMs: 6e4,
|
|
781
|
+
store: void 0,
|
|
782
|
+
identifier: void 0,
|
|
783
|
+
patterns: {
|
|
784
|
+
sequentialAccess: true,
|
|
785
|
+
regularTiming: true,
|
|
786
|
+
missingHeaders: true
|
|
787
|
+
}
|
|
788
|
+
};
|
|
789
|
+
var MemoryBehaviorStore = class {
|
|
790
|
+
records = /* @__PURE__ */ new Map();
|
|
791
|
+
maxIdentifiers;
|
|
792
|
+
accessOrder = [];
|
|
793
|
+
constructor(options = {}) {
|
|
794
|
+
this.maxIdentifiers = options.maxIdentifiers || 1e4;
|
|
795
|
+
}
|
|
796
|
+
async record(identifier, timestamp, path) {
|
|
797
|
+
if (!this.records.has(identifier) && this.records.size >= this.maxIdentifiers) {
|
|
798
|
+
const oldest = this.accessOrder.shift();
|
|
799
|
+
if (oldest) {
|
|
800
|
+
this.records.delete(oldest);
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
const idx = this.accessOrder.indexOf(identifier);
|
|
804
|
+
if (idx > -1) {
|
|
805
|
+
this.accessOrder.splice(idx, 1);
|
|
806
|
+
}
|
|
807
|
+
this.accessOrder.push(identifier);
|
|
808
|
+
const records = this.records.get(identifier) || [];
|
|
809
|
+
records.push({ timestamp, path });
|
|
810
|
+
this.records.set(identifier, records);
|
|
811
|
+
}
|
|
812
|
+
async getHistory(identifier, windowMs) {
|
|
813
|
+
const records = this.records.get(identifier) || [];
|
|
814
|
+
const cutoff = Date.now() - windowMs;
|
|
815
|
+
const filtered = records.filter((r) => r.timestamp > cutoff);
|
|
816
|
+
if (filtered.length !== records.length) {
|
|
817
|
+
this.records.set(identifier, filtered);
|
|
818
|
+
}
|
|
819
|
+
return filtered;
|
|
820
|
+
}
|
|
821
|
+
async cleanup(maxAge) {
|
|
822
|
+
const cutoff = Date.now() - maxAge;
|
|
823
|
+
for (const [identifier, records] of this.records.entries()) {
|
|
824
|
+
const filtered = records.filter((r) => r.timestamp > cutoff);
|
|
825
|
+
if (filtered.length === 0) {
|
|
826
|
+
this.records.delete(identifier);
|
|
827
|
+
const idx = this.accessOrder.indexOf(identifier);
|
|
828
|
+
if (idx > -1) {
|
|
829
|
+
this.accessOrder.splice(idx, 1);
|
|
830
|
+
}
|
|
831
|
+
} else {
|
|
832
|
+
this.records.set(identifier, filtered);
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
/**
|
|
837
|
+
* Get store statistics
|
|
838
|
+
*/
|
|
839
|
+
getStats() {
|
|
840
|
+
let totalRecords = 0;
|
|
841
|
+
for (const records of this.records.values()) {
|
|
842
|
+
totalRecords += records.length;
|
|
843
|
+
}
|
|
844
|
+
return {
|
|
845
|
+
identifiers: this.records.size,
|
|
846
|
+
totalRecords
|
|
847
|
+
};
|
|
848
|
+
}
|
|
849
|
+
/**
|
|
850
|
+
* Clear all records
|
|
851
|
+
*/
|
|
852
|
+
clear() {
|
|
853
|
+
this.records.clear();
|
|
854
|
+
this.accessOrder = [];
|
|
855
|
+
}
|
|
856
|
+
};
|
|
857
|
+
async function analyzeBehavior(req, history, options = {}) {
|
|
858
|
+
const {
|
|
859
|
+
minRequestInterval = DEFAULT_BEHAVIOR_OPTIONS.minRequestInterval,
|
|
860
|
+
maxRequestsPerSecond = DEFAULT_BEHAVIOR_OPTIONS.maxRequestsPerSecond,
|
|
861
|
+
patterns = DEFAULT_BEHAVIOR_OPTIONS.patterns
|
|
862
|
+
} = options;
|
|
863
|
+
const reasons = [];
|
|
864
|
+
let score = 0;
|
|
865
|
+
const now = Date.now();
|
|
866
|
+
const requestCount = history.length;
|
|
867
|
+
const intervals = [];
|
|
868
|
+
for (let i = 1; i < history.length; i++) {
|
|
869
|
+
intervals.push(history[i].timestamp - history[i - 1].timestamp);
|
|
870
|
+
}
|
|
871
|
+
const avgInterval = intervals.length > 0 ? intervals.reduce((a, b) => a + b, 0) / intervals.length : Infinity;
|
|
872
|
+
const oneSecondAgo = now - 1e3;
|
|
873
|
+
const requestsLastSecond = history.filter((r) => r.timestamp > oneSecondAgo).length;
|
|
874
|
+
if (requestsLastSecond > maxRequestsPerSecond) {
|
|
875
|
+
score += 0.3;
|
|
876
|
+
reasons.push(`High request rate: ${requestsLastSecond}/s (max: ${maxRequestsPerSecond})`);
|
|
877
|
+
}
|
|
878
|
+
const hasRapidRequests = intervals.some((i) => i < minRequestInterval);
|
|
879
|
+
if (hasRapidRequests) {
|
|
880
|
+
score += 0.25;
|
|
881
|
+
reasons.push(`Rapid requests detected: interval < ${minRequestInterval}ms`);
|
|
882
|
+
}
|
|
883
|
+
if (patterns?.regularTiming && intervals.length >= 5) {
|
|
884
|
+
const variance = calculateVariance(intervals);
|
|
885
|
+
const coefficientOfVariation = Math.sqrt(variance) / avgInterval;
|
|
886
|
+
if (coefficientOfVariation < 0.1) {
|
|
887
|
+
score += 0.2;
|
|
888
|
+
reasons.push("Suspiciously regular request timing");
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
if (patterns?.sequentialAccess && history.length >= 5) {
|
|
892
|
+
const paths = history.map((r) => r.path);
|
|
893
|
+
if (isSequentialPattern(paths)) {
|
|
894
|
+
score += 0.15;
|
|
895
|
+
reasons.push("Sequential URL access pattern detected");
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
if (patterns?.missingHeaders) {
|
|
899
|
+
const missingScore = checkMissingHeaders(req);
|
|
900
|
+
if (missingScore > 0) {
|
|
901
|
+
score += missingScore;
|
|
902
|
+
reasons.push("Missing typical browser headers");
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
score = Math.min(1, score);
|
|
906
|
+
return {
|
|
907
|
+
suspicious: score >= 0.5,
|
|
908
|
+
score,
|
|
909
|
+
reasons,
|
|
910
|
+
requestCount,
|
|
911
|
+
avgInterval
|
|
912
|
+
};
|
|
913
|
+
}
|
|
914
|
+
async function checkBehavior(req, options = {}) {
|
|
915
|
+
const {
|
|
916
|
+
store = new MemoryBehaviorStore(),
|
|
917
|
+
windowMs = DEFAULT_BEHAVIOR_OPTIONS.windowMs,
|
|
918
|
+
identifier: getIdentifier
|
|
919
|
+
} = options;
|
|
920
|
+
const identifier = getIdentifier ? await getIdentifier(req) : getClientIP2(req);
|
|
921
|
+
const history = await store.getHistory(identifier, windowMs);
|
|
922
|
+
const now = Date.now();
|
|
923
|
+
const path = new URL(req.url).pathname;
|
|
924
|
+
await store.record(identifier, now, path);
|
|
925
|
+
const updatedHistory = [...history, { timestamp: now, path }];
|
|
926
|
+
const analysis = await analyzeBehavior(req, updatedHistory, options);
|
|
927
|
+
return {
|
|
928
|
+
isBot: analysis.suspicious,
|
|
929
|
+
confidence: analysis.score,
|
|
930
|
+
reason: analysis.reasons.join("; ") || "Behavior analysis passed",
|
|
931
|
+
ip: identifier
|
|
932
|
+
};
|
|
933
|
+
}
|
|
934
|
+
var globalBehaviorStore;
|
|
935
|
+
function getGlobalBehaviorStore() {
|
|
936
|
+
if (!globalBehaviorStore) {
|
|
937
|
+
globalBehaviorStore = new MemoryBehaviorStore();
|
|
938
|
+
}
|
|
939
|
+
return globalBehaviorStore;
|
|
940
|
+
}
|
|
941
|
+
function withBehaviorAnalysis(handler, options = {}) {
|
|
942
|
+
const store = options.store || getGlobalBehaviorStore();
|
|
943
|
+
const mergedOptions = { ...options, store };
|
|
944
|
+
return async (req, ctx) => {
|
|
945
|
+
const result = await checkBehavior(req, mergedOptions);
|
|
946
|
+
if (result.isBot && result.confidence >= 0.5) {
|
|
947
|
+
return new Response(
|
|
948
|
+
JSON.stringify({
|
|
949
|
+
error: "Too Many Requests",
|
|
950
|
+
message: "Unusual request pattern detected",
|
|
951
|
+
retryAfter: 60
|
|
952
|
+
}),
|
|
953
|
+
{
|
|
954
|
+
status: 429,
|
|
955
|
+
headers: {
|
|
956
|
+
"Content-Type": "application/json",
|
|
957
|
+
"Retry-After": "60"
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
);
|
|
961
|
+
}
|
|
962
|
+
return handler(req, ctx);
|
|
963
|
+
};
|
|
964
|
+
}
|
|
965
|
+
function getClientIP2(req) {
|
|
966
|
+
return req.headers.get("x-forwarded-for")?.split(",")[0]?.trim() || req.headers.get("x-real-ip") || req.headers.get("cf-connecting-ip") || "unknown";
|
|
967
|
+
}
|
|
968
|
+
function calculateVariance(numbers) {
|
|
969
|
+
if (numbers.length === 0) return 0;
|
|
970
|
+
const mean = numbers.reduce((a, b) => a + b, 0) / numbers.length;
|
|
971
|
+
const squaredDiffs = numbers.map((n) => Math.pow(n - mean, 2));
|
|
972
|
+
return squaredDiffs.reduce((a, b) => a + b, 0) / numbers.length;
|
|
973
|
+
}
|
|
974
|
+
function isSequentialPattern(paths) {
|
|
975
|
+
const numbers = paths.map((p) => {
|
|
976
|
+
const match = p.match(/(\d+)/);
|
|
977
|
+
return match ? parseInt(match[1], 10) : null;
|
|
978
|
+
}).filter((n) => n !== null);
|
|
979
|
+
if (numbers.length < 3) return false;
|
|
980
|
+
let sequential = 0;
|
|
981
|
+
for (let i = 1; i < numbers.length; i++) {
|
|
982
|
+
if (numbers[i] === numbers[i - 1] + 1) {
|
|
983
|
+
sequential++;
|
|
984
|
+
}
|
|
985
|
+
}
|
|
986
|
+
return sequential >= numbers.length * 0.6;
|
|
987
|
+
}
|
|
988
|
+
function checkMissingHeaders(req) {
|
|
989
|
+
let score = 0;
|
|
990
|
+
const typicalHeaders = [
|
|
991
|
+
"accept",
|
|
992
|
+
"accept-language",
|
|
993
|
+
"accept-encoding"
|
|
994
|
+
];
|
|
995
|
+
for (const header of typicalHeaders) {
|
|
996
|
+
if (!req.headers.get(header)) {
|
|
997
|
+
score += 0.05;
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
const accept = req.headers.get("accept");
|
|
1001
|
+
if (accept && !accept.includes("text/html") && !accept.includes("application/json") && !accept.includes("*/*")) {
|
|
1002
|
+
score += 0.05;
|
|
1003
|
+
}
|
|
1004
|
+
const referer = req.headers.get("referer");
|
|
1005
|
+
const path = new URL(req.url).pathname;
|
|
1006
|
+
if (!referer && path !== "/" && !path.includes("/api/")) {
|
|
1007
|
+
score += 0.03;
|
|
1008
|
+
}
|
|
1009
|
+
return score;
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
// src/middleware/bot/captcha.ts
|
|
1013
|
+
var CAPTCHA_VERIFY_URLS = {
|
|
1014
|
+
recaptcha: "https://www.google.com/recaptcha/api/siteverify",
|
|
1015
|
+
hcaptcha: "https://hcaptcha.com/siteverify",
|
|
1016
|
+
turnstile: "https://challenges.cloudflare.com/turnstile/v0/siteverify"
|
|
1017
|
+
};
|
|
1018
|
+
var DEFAULT_TOKEN_FIELDS = {
|
|
1019
|
+
recaptcha: "g-recaptcha-response",
|
|
1020
|
+
hcaptcha: "h-captcha-response",
|
|
1021
|
+
turnstile: "cf-turnstile-response"
|
|
1022
|
+
};
|
|
1023
|
+
async function verifyCaptcha(token, options) {
|
|
1024
|
+
const { provider, secretKey, action } = options;
|
|
1025
|
+
const verifyUrl = CAPTCHA_VERIFY_URLS[provider];
|
|
1026
|
+
const formData = new URLSearchParams();
|
|
1027
|
+
formData.append("secret", secretKey);
|
|
1028
|
+
formData.append("response", token);
|
|
1029
|
+
try {
|
|
1030
|
+
const response = await fetch(verifyUrl, {
|
|
1031
|
+
method: "POST",
|
|
1032
|
+
headers: {
|
|
1033
|
+
"Content-Type": "application/x-www-form-urlencoded"
|
|
1034
|
+
},
|
|
1035
|
+
body: formData.toString()
|
|
1036
|
+
});
|
|
1037
|
+
if (!response.ok) {
|
|
1038
|
+
return {
|
|
1039
|
+
success: false,
|
|
1040
|
+
errorCodes: [`HTTP ${response.status}`]
|
|
1041
|
+
};
|
|
1042
|
+
}
|
|
1043
|
+
const data = await response.json();
|
|
1044
|
+
return parseCaptchaResponse(data, provider, action);
|
|
1045
|
+
} catch (error) {
|
|
1046
|
+
return {
|
|
1047
|
+
success: false,
|
|
1048
|
+
errorCodes: ["verification-failed", String(error)]
|
|
1049
|
+
};
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
1052
|
+
function parseCaptchaResponse(data, provider, expectedAction) {
|
|
1053
|
+
switch (provider) {
|
|
1054
|
+
case "recaptcha":
|
|
1055
|
+
return parseRecaptchaResponse(data, expectedAction);
|
|
1056
|
+
case "hcaptcha":
|
|
1057
|
+
return parseHCaptchaResponse(data);
|
|
1058
|
+
case "turnstile":
|
|
1059
|
+
return parseTurnstileResponse(data);
|
|
1060
|
+
default:
|
|
1061
|
+
return {
|
|
1062
|
+
success: false,
|
|
1063
|
+
errorCodes: ["unknown-provider"]
|
|
1064
|
+
};
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
1067
|
+
function parseRecaptchaResponse(data, expectedAction) {
|
|
1068
|
+
const result = {
|
|
1069
|
+
success: data.success === true,
|
|
1070
|
+
score: typeof data.score === "number" ? data.score : void 0,
|
|
1071
|
+
action: typeof data.action === "string" ? data.action : void 0,
|
|
1072
|
+
hostname: typeof data.hostname === "string" ? data.hostname : void 0,
|
|
1073
|
+
challengeTs: typeof data.challenge_ts === "string" ? data.challenge_ts : void 0,
|
|
1074
|
+
errorCodes: Array.isArray(data["error-codes"]) ? data["error-codes"] : void 0
|
|
1075
|
+
};
|
|
1076
|
+
if (result.success && expectedAction && result.action !== expectedAction) {
|
|
1077
|
+
result.success = false;
|
|
1078
|
+
result.errorCodes = ["action-mismatch"];
|
|
1079
|
+
}
|
|
1080
|
+
return result;
|
|
1081
|
+
}
|
|
1082
|
+
function parseHCaptchaResponse(data) {
|
|
1083
|
+
return {
|
|
1084
|
+
success: data.success === true,
|
|
1085
|
+
hostname: typeof data.hostname === "string" ? data.hostname : void 0,
|
|
1086
|
+
challengeTs: typeof data.challenge_ts === "string" ? data.challenge_ts : void 0,
|
|
1087
|
+
errorCodes: Array.isArray(data["error-codes"]) ? data["error-codes"] : void 0
|
|
1088
|
+
};
|
|
1089
|
+
}
|
|
1090
|
+
function parseTurnstileResponse(data) {
|
|
1091
|
+
return {
|
|
1092
|
+
success: data.success === true,
|
|
1093
|
+
hostname: typeof data.hostname === "string" ? data.hostname : void 0,
|
|
1094
|
+
challengeTs: typeof data.challenge_ts === "string" ? data.challenge_ts : void 0,
|
|
1095
|
+
action: typeof data.action === "string" ? data.action : void 0,
|
|
1096
|
+
errorCodes: Array.isArray(data["error-codes"]) ? data["error-codes"] : void 0
|
|
1097
|
+
};
|
|
1098
|
+
}
|
|
1099
|
+
async function extractCaptchaToken(req, options) {
|
|
1100
|
+
const { provider, tokenField } = options;
|
|
1101
|
+
const fieldName = tokenField || DEFAULT_TOKEN_FIELDS[provider];
|
|
1102
|
+
const url = new URL(req.url);
|
|
1103
|
+
const queryToken = url.searchParams.get(fieldName);
|
|
1104
|
+
if (queryToken) {
|
|
1105
|
+
return queryToken;
|
|
1106
|
+
}
|
|
1107
|
+
if (hasBody2(req)) {
|
|
1108
|
+
try {
|
|
1109
|
+
const body = await getRequestBody2(req);
|
|
1110
|
+
if (body && typeof body === "object") {
|
|
1111
|
+
const bodyToken = body[fieldName];
|
|
1112
|
+
if (typeof bodyToken === "string") {
|
|
1113
|
+
return bodyToken;
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1116
|
+
} catch {
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1119
|
+
const headerToken = req.headers.get(`x-${fieldName}`);
|
|
1120
|
+
if (headerToken) {
|
|
1121
|
+
return headerToken;
|
|
1122
|
+
}
|
|
1123
|
+
return null;
|
|
1124
|
+
}
|
|
1125
|
+
async function checkCaptcha(req, options) {
|
|
1126
|
+
const { threshold = 0.5, skip } = options;
|
|
1127
|
+
if (skip && await skip(req)) {
|
|
1128
|
+
return {
|
|
1129
|
+
isBot: false,
|
|
1130
|
+
confidence: 0,
|
|
1131
|
+
reason: "CAPTCHA check skipped"
|
|
1132
|
+
};
|
|
1133
|
+
}
|
|
1134
|
+
const token = await extractCaptchaToken(req, options);
|
|
1135
|
+
if (!token) {
|
|
1136
|
+
return {
|
|
1137
|
+
isBot: true,
|
|
1138
|
+
confidence: 0.9,
|
|
1139
|
+
reason: "CAPTCHA token missing",
|
|
1140
|
+
ip: getClientIP3(req)
|
|
1141
|
+
};
|
|
1142
|
+
}
|
|
1143
|
+
const result = await verifyCaptcha(token, options);
|
|
1144
|
+
if (!result.success) {
|
|
1145
|
+
return {
|
|
1146
|
+
isBot: true,
|
|
1147
|
+
confidence: 0.95,
|
|
1148
|
+
reason: `CAPTCHA verification failed: ${result.errorCodes?.join(", ") || "unknown"}`,
|
|
1149
|
+
ip: getClientIP3(req)
|
|
1150
|
+
};
|
|
1151
|
+
}
|
|
1152
|
+
if (result.score !== void 0 && result.score < threshold) {
|
|
1153
|
+
return {
|
|
1154
|
+
isBot: true,
|
|
1155
|
+
confidence: 1 - result.score,
|
|
1156
|
+
reason: `CAPTCHA score too low: ${result.score} (threshold: ${threshold})`,
|
|
1157
|
+
ip: getClientIP3(req)
|
|
1158
|
+
};
|
|
1159
|
+
}
|
|
1160
|
+
return {
|
|
1161
|
+
isBot: false,
|
|
1162
|
+
confidence: result.score !== void 0 ? 1 - result.score : 0.1,
|
|
1163
|
+
reason: "CAPTCHA verification passed"
|
|
1164
|
+
};
|
|
1165
|
+
}
|
|
1166
|
+
function withCaptcha(handler, options) {
|
|
1167
|
+
return async (req, ctx) => {
|
|
1168
|
+
const result = await checkCaptcha(req, options);
|
|
1169
|
+
if (result.isBot) {
|
|
1170
|
+
return new Response(
|
|
1171
|
+
JSON.stringify({
|
|
1172
|
+
error: "CAPTCHA Required",
|
|
1173
|
+
message: result.reason,
|
|
1174
|
+
code: "CAPTCHA_FAILED"
|
|
1175
|
+
}),
|
|
1176
|
+
{
|
|
1177
|
+
status: 403,
|
|
1178
|
+
headers: { "Content-Type": "application/json" }
|
|
1179
|
+
}
|
|
1180
|
+
);
|
|
1181
|
+
}
|
|
1182
|
+
return handler(req, ctx);
|
|
1183
|
+
};
|
|
1184
|
+
}
|
|
1185
|
+
function generateRecaptchaV2(siteKey, options = {}) {
|
|
1186
|
+
const { theme = "light", size = "normal" } = options;
|
|
1187
|
+
return `
|
|
1188
|
+
<script src="https://www.google.com/recaptcha/api.js" async defer></script>
|
|
1189
|
+
<div class="g-recaptcha" data-sitekey="${siteKey}" data-theme="${theme}" data-size="${size}"></div>
|
|
1190
|
+
`.trim();
|
|
1191
|
+
}
|
|
1192
|
+
function generateRecaptchaV3(siteKey, action = "submit") {
|
|
1193
|
+
return `
|
|
1194
|
+
<script src="https://www.google.com/recaptcha/api.js?render=${siteKey}"></script>
|
|
1195
|
+
<script>
|
|
1196
|
+
function getRecaptchaToken() {
|
|
1197
|
+
return new Promise((resolve, reject) => {
|
|
1198
|
+
grecaptcha.ready(() => {
|
|
1199
|
+
grecaptcha.execute('${siteKey}', { action: '${action}' })
|
|
1200
|
+
.then(resolve)
|
|
1201
|
+
.catch(reject);
|
|
1202
|
+
});
|
|
1203
|
+
});
|
|
1204
|
+
}
|
|
1205
|
+
</script>
|
|
1206
|
+
`.trim();
|
|
1207
|
+
}
|
|
1208
|
+
function generateHCaptcha(siteKey, options = {}) {
|
|
1209
|
+
const { theme = "light", size = "normal" } = options;
|
|
1210
|
+
return `
|
|
1211
|
+
<script src="https://js.hcaptcha.com/1/api.js" async defer></script>
|
|
1212
|
+
<div class="h-captcha" data-sitekey="${siteKey}" data-theme="${theme}" data-size="${size}"></div>
|
|
1213
|
+
`.trim();
|
|
1214
|
+
}
|
|
1215
|
+
function generateTurnstile(siteKey, options = {}) {
|
|
1216
|
+
const { theme = "auto", size = "normal" } = options;
|
|
1217
|
+
return `
|
|
1218
|
+
<script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script>
|
|
1219
|
+
<div class="cf-turnstile" data-sitekey="${siteKey}" data-theme="${theme}" data-size="${size}"></div>
|
|
1220
|
+
`.trim();
|
|
1221
|
+
}
|
|
1222
|
+
function hasBody2(req) {
|
|
1223
|
+
const method = req.method.toUpperCase();
|
|
1224
|
+
return ["POST", "PUT", "PATCH", "DELETE"].includes(method);
|
|
1225
|
+
}
|
|
1226
|
+
async function getRequestBody2(req) {
|
|
1227
|
+
try {
|
|
1228
|
+
const contentType = req.headers.get("content-type") || "";
|
|
1229
|
+
if (contentType.includes("application/json")) {
|
|
1230
|
+
const cloned = req.clone();
|
|
1231
|
+
return await cloned.json();
|
|
1232
|
+
}
|
|
1233
|
+
if (contentType.includes("application/x-www-form-urlencoded")) {
|
|
1234
|
+
const cloned = req.clone();
|
|
1235
|
+
const text = await cloned.text();
|
|
1236
|
+
return Object.fromEntries(new URLSearchParams(text));
|
|
1237
|
+
}
|
|
1238
|
+
if (contentType.includes("multipart/form-data")) {
|
|
1239
|
+
const cloned = req.clone();
|
|
1240
|
+
const formData = await cloned.formData();
|
|
1241
|
+
const obj = {};
|
|
1242
|
+
formData.forEach((value, key) => {
|
|
1243
|
+
obj[key] = value;
|
|
1244
|
+
});
|
|
1245
|
+
return obj;
|
|
1246
|
+
}
|
|
1247
|
+
return null;
|
|
1248
|
+
} catch {
|
|
1249
|
+
return null;
|
|
1250
|
+
}
|
|
1251
|
+
}
|
|
1252
|
+
function getClientIP3(req) {
|
|
1253
|
+
return req.headers.get("x-forwarded-for")?.split(",")[0]?.trim() || req.headers.get("x-real-ip") || req.headers.get("cf-connecting-ip") || "unknown";
|
|
1254
|
+
}
|
|
1255
|
+
|
|
1256
|
+
// src/middleware/bot/middleware.ts
|
|
1257
|
+
function defaultBotResponse(result) {
|
|
1258
|
+
return new Response(
|
|
1259
|
+
JSON.stringify({
|
|
1260
|
+
error: "Forbidden",
|
|
1261
|
+
message: result.reason || "Request blocked",
|
|
1262
|
+
code: "BOT_DETECTED",
|
|
1263
|
+
category: result.category
|
|
1264
|
+
}),
|
|
1265
|
+
{
|
|
1266
|
+
status: 403,
|
|
1267
|
+
headers: {
|
|
1268
|
+
"Content-Type": "application/json",
|
|
1269
|
+
"X-Bot-Detection": "true"
|
|
1270
|
+
}
|
|
1271
|
+
}
|
|
1272
|
+
);
|
|
1273
|
+
}
|
|
1274
|
+
async function detectBot(req, options = {}) {
|
|
1275
|
+
const results = [];
|
|
1276
|
+
if (options.userAgent !== false) {
|
|
1277
|
+
const uaOptions = options.userAgent === true ? {} : options.userAgent || {};
|
|
1278
|
+
const userAgent = req.headers.get("user-agent");
|
|
1279
|
+
const uaResult = analyzeUserAgent(userAgent, uaOptions);
|
|
1280
|
+
if (uaResult.isBot && !isAllowedBot(uaResult, uaOptions)) {
|
|
1281
|
+
results.push(uaResult);
|
|
1282
|
+
}
|
|
1283
|
+
}
|
|
1284
|
+
if (options.honeypot !== false && hasBody3(req)) {
|
|
1285
|
+
const hpOptions = options.honeypot === true ? {} : options.honeypot || {};
|
|
1286
|
+
const hpResult = await checkHoneypot(req, hpOptions);
|
|
1287
|
+
if (hpResult.isBot) {
|
|
1288
|
+
results.push(hpResult);
|
|
1289
|
+
}
|
|
1290
|
+
}
|
|
1291
|
+
if (options.behavior !== false) {
|
|
1292
|
+
const behaviorOptions = options.behavior === true ? {} : options.behavior || {};
|
|
1293
|
+
if (!behaviorOptions.store) {
|
|
1294
|
+
behaviorOptions.store = getGlobalBehaviorStore();
|
|
1295
|
+
}
|
|
1296
|
+
const behaviorResult = await checkBehavior(req, behaviorOptions);
|
|
1297
|
+
if (behaviorResult.isBot) {
|
|
1298
|
+
results.push(behaviorResult);
|
|
1299
|
+
}
|
|
1300
|
+
}
|
|
1301
|
+
if (options.captcha) {
|
|
1302
|
+
const captchaResult = await checkCaptcha(req, options.captcha);
|
|
1303
|
+
if (captchaResult.isBot) {
|
|
1304
|
+
results.push(captchaResult);
|
|
1305
|
+
}
|
|
1306
|
+
}
|
|
1307
|
+
if (results.length === 0) {
|
|
1308
|
+
return {
|
|
1309
|
+
isBot: false,
|
|
1310
|
+
confidence: 0,
|
|
1311
|
+
reason: "All checks passed",
|
|
1312
|
+
ip: getClientIP4(req),
|
|
1313
|
+
userAgent: req.headers.get("user-agent") || void 0
|
|
1314
|
+
};
|
|
1315
|
+
}
|
|
1316
|
+
const highestConfidence = results.reduce(
|
|
1317
|
+
(prev, curr) => curr.confidence > prev.confidence ? curr : prev
|
|
1318
|
+
);
|
|
1319
|
+
const allReasons = results.map((r) => r.reason).filter(Boolean).join("; ");
|
|
1320
|
+
return {
|
|
1321
|
+
isBot: true,
|
|
1322
|
+
category: highestConfidence.category,
|
|
1323
|
+
name: highestConfidence.name,
|
|
1324
|
+
confidence: highestConfidence.confidence,
|
|
1325
|
+
reason: allReasons,
|
|
1326
|
+
ip: getClientIP4(req),
|
|
1327
|
+
userAgent: req.headers.get("user-agent") || void 0
|
|
1328
|
+
};
|
|
1329
|
+
}
|
|
1330
|
+
function isAllowedBot(result, options) {
|
|
1331
|
+
const {
|
|
1332
|
+
allowCategories = DEFAULT_ALLOWED_CATEGORIES,
|
|
1333
|
+
allowList = DEFAULT_ALLOWED_BOTS,
|
|
1334
|
+
blockList = [],
|
|
1335
|
+
blockAllBots = false
|
|
1336
|
+
} = options;
|
|
1337
|
+
if (result.name && blockList.includes(result.name)) {
|
|
1338
|
+
return false;
|
|
1339
|
+
}
|
|
1340
|
+
if (blockAllBots) {
|
|
1341
|
+
return false;
|
|
1342
|
+
}
|
|
1343
|
+
if (result.name && allowList.includes(result.name)) {
|
|
1344
|
+
return true;
|
|
1345
|
+
}
|
|
1346
|
+
if (result.category && allowCategories.includes(result.category)) {
|
|
1347
|
+
return true;
|
|
1348
|
+
}
|
|
1349
|
+
return false;
|
|
1350
|
+
}
|
|
1351
|
+
function withBotProtection(handler, options = {}) {
|
|
1352
|
+
const {
|
|
1353
|
+
skip,
|
|
1354
|
+
onBot,
|
|
1355
|
+
log,
|
|
1356
|
+
mode = "block"
|
|
1357
|
+
} = options;
|
|
1358
|
+
return async (req, ctx) => {
|
|
1359
|
+
if (skip && await skip(req)) {
|
|
1360
|
+
return handler(req, { ...ctx, bot: void 0 });
|
|
1361
|
+
}
|
|
1362
|
+
const result = await detectBot(req, options);
|
|
1363
|
+
if (log) {
|
|
1364
|
+
if (typeof log === "function") {
|
|
1365
|
+
log(result);
|
|
1366
|
+
} else if (result.isBot) {
|
|
1367
|
+
console.log("[Bot Detection]", JSON.stringify(result));
|
|
1368
|
+
}
|
|
1369
|
+
}
|
|
1370
|
+
if (result.isBot) {
|
|
1371
|
+
if (mode === "block") {
|
|
1372
|
+
if (onBot) {
|
|
1373
|
+
return onBot(req, result);
|
|
1374
|
+
}
|
|
1375
|
+
return defaultBotResponse(result);
|
|
1376
|
+
}
|
|
1377
|
+
}
|
|
1378
|
+
const extendedCtx = {
|
|
1379
|
+
...ctx,
|
|
1380
|
+
bot: result
|
|
1381
|
+
};
|
|
1382
|
+
return handler(req, extendedCtx);
|
|
1383
|
+
};
|
|
1384
|
+
}
|
|
1385
|
+
function withUserAgentProtection(handler, options = {}) {
|
|
1386
|
+
return withBotProtection(handler, {
|
|
1387
|
+
userAgent: options,
|
|
1388
|
+
honeypot: false,
|
|
1389
|
+
behavior: false
|
|
1390
|
+
});
|
|
1391
|
+
}
|
|
1392
|
+
function withHoneypotProtection(handler, options = {}) {
|
|
1393
|
+
return withBotProtection(handler, {
|
|
1394
|
+
userAgent: false,
|
|
1395
|
+
honeypot: options,
|
|
1396
|
+
behavior: false
|
|
1397
|
+
});
|
|
1398
|
+
}
|
|
1399
|
+
function withBehaviorProtection(handler, options = {}) {
|
|
1400
|
+
return withBotProtection(handler, {
|
|
1401
|
+
userAgent: false,
|
|
1402
|
+
honeypot: false,
|
|
1403
|
+
behavior: options
|
|
1404
|
+
});
|
|
1405
|
+
}
|
|
1406
|
+
function withCaptchaProtection(handler, options) {
|
|
1407
|
+
return withBotProtection(handler, {
|
|
1408
|
+
userAgent: false,
|
|
1409
|
+
honeypot: false,
|
|
1410
|
+
behavior: false,
|
|
1411
|
+
captcha: options
|
|
1412
|
+
});
|
|
1413
|
+
}
|
|
1414
|
+
var BOT_PROTECTION_PRESETS = {
|
|
1415
|
+
/**
|
|
1416
|
+
* Relaxed - Only blocks obvious bots
|
|
1417
|
+
*/
|
|
1418
|
+
relaxed: {
|
|
1419
|
+
userAgent: {
|
|
1420
|
+
blockAllBots: false,
|
|
1421
|
+
allowCategories: ["search_engine", "social_media", "monitoring", "feed_reader", "preview", "seo"]
|
|
1422
|
+
},
|
|
1423
|
+
honeypot: false,
|
|
1424
|
+
behavior: false
|
|
1425
|
+
},
|
|
1426
|
+
/**
|
|
1427
|
+
* Standard - Good balance of protection
|
|
1428
|
+
*/
|
|
1429
|
+
standard: {
|
|
1430
|
+
userAgent: {
|
|
1431
|
+
blockAllBots: false,
|
|
1432
|
+
allowCategories: ["search_engine", "social_media", "monitoring"]
|
|
1433
|
+
},
|
|
1434
|
+
honeypot: true,
|
|
1435
|
+
behavior: {
|
|
1436
|
+
maxRequestsPerSecond: 10
|
|
1437
|
+
}
|
|
1438
|
+
},
|
|
1439
|
+
/**
|
|
1440
|
+
* Strict - Maximum protection
|
|
1441
|
+
*/
|
|
1442
|
+
strict: {
|
|
1443
|
+
userAgent: {
|
|
1444
|
+
blockAllBots: false,
|
|
1445
|
+
allowCategories: ["search_engine"],
|
|
1446
|
+
blockEmptyUA: true,
|
|
1447
|
+
blockSuspiciousUA: true
|
|
1448
|
+
},
|
|
1449
|
+
honeypot: {
|
|
1450
|
+
additionalFields: ["_hp_name", "_hp_phone"]
|
|
1451
|
+
},
|
|
1452
|
+
behavior: {
|
|
1453
|
+
maxRequestsPerSecond: 5,
|
|
1454
|
+
minRequestInterval: 200
|
|
1455
|
+
}
|
|
1456
|
+
},
|
|
1457
|
+
/**
|
|
1458
|
+
* API - For API endpoints
|
|
1459
|
+
*/
|
|
1460
|
+
api: {
|
|
1461
|
+
userAgent: {
|
|
1462
|
+
blockAllBots: true,
|
|
1463
|
+
blockEmptyUA: true
|
|
1464
|
+
},
|
|
1465
|
+
honeypot: false,
|
|
1466
|
+
behavior: {
|
|
1467
|
+
maxRequestsPerSecond: 20
|
|
1468
|
+
}
|
|
1469
|
+
}
|
|
1470
|
+
};
|
|
1471
|
+
function withBotProtectionPreset(handler, preset, overrides = {}) {
|
|
1472
|
+
const presetOptions = BOT_PROTECTION_PRESETS[preset];
|
|
1473
|
+
const mergedOptions = { ...presetOptions, ...overrides };
|
|
1474
|
+
return withBotProtection(handler, mergedOptions);
|
|
1475
|
+
}
|
|
1476
|
+
function hasBody3(req) {
|
|
1477
|
+
const method = req.method.toUpperCase();
|
|
1478
|
+
return ["POST", "PUT", "PATCH", "DELETE"].includes(method);
|
|
1479
|
+
}
|
|
1480
|
+
function getClientIP4(req) {
|
|
1481
|
+
return req.headers.get("x-forwarded-for")?.split(",")[0]?.trim() || req.headers.get("x-real-ip") || req.headers.get("cf-connecting-ip") || "unknown";
|
|
1482
|
+
}
|
|
1483
|
+
|
|
1484
|
+
exports.BOT_PROTECTION_PRESETS = BOT_PROTECTION_PRESETS;
|
|
1485
|
+
exports.DEFAULT_ALLOWED_BOTS = DEFAULT_ALLOWED_BOTS;
|
|
1486
|
+
exports.DEFAULT_ALLOWED_CATEGORIES = DEFAULT_ALLOWED_CATEGORIES;
|
|
1487
|
+
exports.DEFAULT_BEHAVIOR_OPTIONS = DEFAULT_BEHAVIOR_OPTIONS;
|
|
1488
|
+
exports.DEFAULT_HONEYPOT_FIELDS = DEFAULT_HONEYPOT_FIELDS;
|
|
1489
|
+
exports.DEFAULT_HONEYPOT_OPTIONS = DEFAULT_HONEYPOT_OPTIONS;
|
|
1490
|
+
exports.KNOWN_BOT_PATTERNS = KNOWN_BOT_PATTERNS;
|
|
1491
|
+
exports.MemoryBehaviorStore = MemoryBehaviorStore;
|
|
1492
|
+
exports.analyzeBehavior = analyzeBehavior;
|
|
1493
|
+
exports.analyzeUserAgent = analyzeUserAgent;
|
|
1494
|
+
exports.checkBehavior = checkBehavior;
|
|
1495
|
+
exports.checkCaptcha = checkCaptcha;
|
|
1496
|
+
exports.checkHoneypot = checkHoneypot;
|
|
1497
|
+
exports.createBotPattern = createBotPattern;
|
|
1498
|
+
exports.detectBot = detectBot;
|
|
1499
|
+
exports.extractCaptchaToken = extractCaptchaToken;
|
|
1500
|
+
exports.generateHCaptcha = generateHCaptcha;
|
|
1501
|
+
exports.generateHoneypotCSS = generateHoneypotCSS;
|
|
1502
|
+
exports.generateHoneypotHTML = generateHoneypotHTML;
|
|
1503
|
+
exports.generateRecaptchaV2 = generateRecaptchaV2;
|
|
1504
|
+
exports.generateRecaptchaV3 = generateRecaptchaV3;
|
|
1505
|
+
exports.generateTurnstile = generateTurnstile;
|
|
1506
|
+
exports.getBotsByCategory = getBotsByCategory;
|
|
1507
|
+
exports.getGlobalBehaviorStore = getGlobalBehaviorStore;
|
|
1508
|
+
exports.isBotAllowed = isBotAllowed;
|
|
1509
|
+
exports.isSuspiciousUA = isSuspiciousUA;
|
|
1510
|
+
exports.verifyCaptcha = verifyCaptcha;
|
|
1511
|
+
exports.withBehaviorAnalysis = withBehaviorAnalysis;
|
|
1512
|
+
exports.withBehaviorProtection = withBehaviorProtection;
|
|
1513
|
+
exports.withBotProtection = withBotProtection;
|
|
1514
|
+
exports.withBotProtectionPreset = withBotProtectionPreset;
|
|
1515
|
+
exports.withCaptcha = withCaptcha;
|
|
1516
|
+
exports.withCaptchaProtection = withCaptchaProtection;
|
|
1517
|
+
exports.withHoneypot = withHoneypot;
|
|
1518
|
+
exports.withHoneypotProtection = withHoneypotProtection;
|
|
1519
|
+
exports.withUserAgentProtection = withUserAgentProtection;
|
|
1520
|
+
//# sourceMappingURL=bot.cjs.map
|
|
1521
|
+
//# sourceMappingURL=bot.cjs.map
|