is-antibot 1.3.6 → 1.4.1
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 +5 -4
- package/package.json +1 -1
- package/src/index.js +160 -126
package/README.md
CHANGED
|
@@ -67,14 +67,14 @@ const isAntibot = require('is-antibot')
|
|
|
67
67
|
const response = await fetch('https://www.linkedin.com/in/kikobeats/')
|
|
68
68
|
const html = await response.text()
|
|
69
69
|
|
|
70
|
-
const { detected, provider } = isAntibot({
|
|
70
|
+
const { detected, provider, detection } = isAntibot({
|
|
71
71
|
headers: response.headers,
|
|
72
72
|
html,
|
|
73
73
|
url: response.url
|
|
74
74
|
})
|
|
75
75
|
|
|
76
76
|
if (detected) {
|
|
77
|
-
console.log(`Antibot detected: ${provider}`)
|
|
77
|
+
console.log(`Antibot detected: ${provider} via ${detection}`)
|
|
78
78
|
}
|
|
79
79
|
```
|
|
80
80
|
|
|
@@ -84,10 +84,10 @@ It also works with [got](https://github.com/sindresorhus/got) or any library whe
|
|
|
84
84
|
const response = await got('https://www.linkedin.com/in/kikobeats/')
|
|
85
85
|
.catch(error => errorresponse)
|
|
86
86
|
|
|
87
|
-
const { detected, provider } = isAntibot(response)
|
|
87
|
+
const { detected, provider, detection } = isAntibot(response)
|
|
88
88
|
|
|
89
89
|
if (detected) {
|
|
90
|
-
console.log(`Antibot detected: ${provider}`)
|
|
90
|
+
console.log(`Antibot detected: ${provider} via ${detection}`)
|
|
91
91
|
}
|
|
92
92
|
```
|
|
93
93
|
|
|
@@ -95,6 +95,7 @@ The library returns an object with the following properties:
|
|
|
95
95
|
|
|
96
96
|
- `detected` (boolean): Whether an antibot challenge was detected
|
|
97
97
|
- `provider` (string|null): The name of the detected provider (e.g., 'cloudflare', 'recaptcha')
|
|
98
|
+
- `detection` (string|null): Where the signal came from: `'headers'`, `'cookies'`, `'html'`, or `'url'`
|
|
98
99
|
|
|
99
100
|
## License
|
|
100
101
|
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "is-antibot",
|
|
3
3
|
"description": "Identify if a response is an antibot challenge from CloudFlare, Akamai, DataDome, Vercel, PerimeterX, Shape Security, and more, including CAPTCHA providers like reCAPTCHA and hCaptcha.",
|
|
4
4
|
"homepage": "https://github.com/microlinkhq/is-antibot",
|
|
5
|
-
"version": "1.
|
|
5
|
+
"version": "1.4.1",
|
|
6
6
|
"exports": {
|
|
7
7
|
".": "./src/index.js"
|
|
8
8
|
},
|
package/src/index.js
CHANGED
|
@@ -3,6 +3,13 @@
|
|
|
3
3
|
const { splitSetCookieString } = require('cookie-es')
|
|
4
4
|
const debug = require('debug-logfmt')('is-antibot')
|
|
5
5
|
|
|
6
|
+
const DETECTION = {
|
|
7
|
+
HEADERS: 'headers',
|
|
8
|
+
COOKIES: 'cookies',
|
|
9
|
+
HTML: 'html',
|
|
10
|
+
URL: 'url'
|
|
11
|
+
}
|
|
12
|
+
|
|
6
13
|
const createGetHeader = headers =>
|
|
7
14
|
typeof headers.get === 'function'
|
|
8
15
|
? name => headers.get(name)
|
|
@@ -12,6 +19,14 @@ const createTestPattern = value => {
|
|
|
12
19
|
if (!value) return () => false
|
|
13
20
|
const lowerValue = value.toLowerCase()
|
|
14
21
|
return (pattern, isRegex = false) => {
|
|
22
|
+
if (pattern instanceof RegExp) {
|
|
23
|
+
try {
|
|
24
|
+
return pattern.test(value)
|
|
25
|
+
} catch {
|
|
26
|
+
return false
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
15
30
|
if (isRegex) {
|
|
16
31
|
try {
|
|
17
32
|
return new RegExp(pattern, 'i').test(value)
|
|
@@ -23,9 +38,9 @@ const createTestPattern = value => {
|
|
|
23
38
|
}
|
|
24
39
|
}
|
|
25
40
|
|
|
26
|
-
const createResult = (detected, provider) => {
|
|
27
|
-
debug({ detected, provider })
|
|
28
|
-
return { detected, provider }
|
|
41
|
+
const createResult = (detected, provider, detection = null) => {
|
|
42
|
+
debug({ detected, provider, detection })
|
|
43
|
+
return { detected, provider, detection }
|
|
29
44
|
}
|
|
30
45
|
|
|
31
46
|
const createHasCookie = headers => {
|
|
@@ -36,351 +51,370 @@ const createHasCookie = headers => {
|
|
|
36
51
|
)
|
|
37
52
|
}
|
|
38
53
|
|
|
54
|
+
const getHeaderNames = headers =>
|
|
55
|
+
typeof headers.keys === 'function'
|
|
56
|
+
? Array.from(headers.keys())
|
|
57
|
+
: Object.keys(headers)
|
|
58
|
+
|
|
39
59
|
const detect = ({ headers = {}, html = '', url = '' } = {}) => {
|
|
40
60
|
const getHeader = createGetHeader(headers)
|
|
41
61
|
const hasCookie = createHasCookie(headers)
|
|
42
62
|
const htmlHas = createTestPattern(html)
|
|
43
63
|
const urlHas = createTestPattern(url)
|
|
44
64
|
|
|
65
|
+
const hasAnyHeader = headerNames =>
|
|
66
|
+
headerNames.some(headerName => getHeader(headerName))
|
|
67
|
+
|
|
68
|
+
const hasAnyCookie = cookieNames =>
|
|
69
|
+
cookieNames.some(cookieName => hasCookie(cookieName))
|
|
70
|
+
|
|
71
|
+
const hasAnyHtml = patterns => patterns.some(pattern => htmlHas(pattern))
|
|
72
|
+
|
|
73
|
+
const hasAnyUrl = patterns => patterns.some(pattern => urlHas(pattern))
|
|
74
|
+
|
|
75
|
+
const byHeaders = provider => createResult(true, provider, DETECTION.HEADERS)
|
|
76
|
+
|
|
77
|
+
const byCookies = provider => createResult(true, provider, DETECTION.COOKIES)
|
|
78
|
+
|
|
79
|
+
const byHtml = provider => createResult(true, provider, DETECTION.HTML)
|
|
80
|
+
|
|
81
|
+
const byUrl = provider => createResult(true, provider, DETECTION.URL)
|
|
82
|
+
|
|
45
83
|
// CloudFlare: Check for cf-mitigated header with 'challenge' value
|
|
46
84
|
// Official docs: https://developers.cloudflare.com/cloudflare-challenges/challenge-types/challenge-pages/detect-response/
|
|
47
85
|
if (getHeader('cf-mitigated') === 'challenge') {
|
|
48
|
-
return
|
|
86
|
+
return byHeaders('cloudflare')
|
|
49
87
|
}
|
|
50
88
|
|
|
51
89
|
// Cloudflare: cf_clearance cookie indicates Cloudflare challenge flow
|
|
52
|
-
if (
|
|
53
|
-
return
|
|
90
|
+
if (hasAnyCookie(['cf_clearance='])) {
|
|
91
|
+
return byCookies('cloudflare')
|
|
54
92
|
}
|
|
55
93
|
|
|
56
94
|
// Vercel: Check for x-vercel-mitigated header with 'challenge' value
|
|
57
95
|
// Solver reference: https://github.com/glizzykingdreko/Vercel-Attack-Mode-Solver
|
|
58
96
|
if (getHeader('x-vercel-mitigated') === 'challenge') {
|
|
59
|
-
return
|
|
97
|
+
return byHeaders('vercel')
|
|
60
98
|
}
|
|
61
99
|
|
|
62
100
|
// Akamai: Check for akamai-cache-status header starting with 'Error'
|
|
63
101
|
// Official docs: https://techdocs.akamai.com/property-mgr/docs/return-cache-status
|
|
64
102
|
if (getHeader('akamai-cache-status')?.startsWith('Error')) {
|
|
65
|
-
return
|
|
103
|
+
return byHeaders('akamai')
|
|
66
104
|
}
|
|
67
105
|
|
|
68
106
|
// Akamai: Check for additional identifying headers (akamai-grn, x-akamai-session-info)
|
|
69
107
|
// Reference: https://github.com/scrapfly/Antibot-Detector/blob/main/detectors/antibot/detect-akamai.json
|
|
70
|
-
if (
|
|
71
|
-
return
|
|
108
|
+
if (hasAnyHeader(['akamai-grn', 'x-akamai-session-info'])) {
|
|
109
|
+
return byHeaders('akamai')
|
|
72
110
|
}
|
|
73
111
|
|
|
74
112
|
// Akamai: _abck bot manager tracking cookie
|
|
75
|
-
if (
|
|
76
|
-
return
|
|
113
|
+
if (hasAnyCookie(['_abck='])) {
|
|
114
|
+
return byCookies('akamai')
|
|
77
115
|
}
|
|
78
116
|
|
|
79
117
|
// Akamai: Bot Manager API namespace (bmak) in html
|
|
80
|
-
if (
|
|
81
|
-
return
|
|
118
|
+
if (hasAnyHtml(['bmak.'])) {
|
|
119
|
+
return byHtml('akamai')
|
|
82
120
|
}
|
|
83
121
|
|
|
84
122
|
// DataDome: Check for x-dd-b header with values '1' (soft challenge) or '2' (hard challenge/CAPTCHA)
|
|
85
123
|
// Official docs: https://docs.datadome.co/reference/validate-request
|
|
86
124
|
if (['1', '2'].includes(getHeader('x-dd-b'))) {
|
|
87
|
-
return
|
|
125
|
+
return byHeaders('datadome')
|
|
88
126
|
}
|
|
89
127
|
|
|
90
|
-
// DataDome:
|
|
91
|
-
//
|
|
92
|
-
|
|
93
|
-
|
|
128
|
+
// DataDome: x-datadome header presence.
|
|
129
|
+
// Note: `x-datadome: protected` can appear on successful responses.
|
|
130
|
+
const xDatadome = getHeader('x-datadome')
|
|
131
|
+
if (xDatadome && String(xDatadome).toLowerCase() !== 'protected') {
|
|
132
|
+
return byHeaders('datadome')
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// DataDome: x-datadome-cid header presence
|
|
136
|
+
if (hasAnyHeader(['x-datadome-cid'])) {
|
|
137
|
+
return byHeaders('datadome')
|
|
94
138
|
}
|
|
95
139
|
|
|
96
140
|
// DataDome: datadome tracking cookie
|
|
97
|
-
if (
|
|
98
|
-
return
|
|
141
|
+
if (hasAnyCookie(['datadome='])) {
|
|
142
|
+
return byCookies('datadome')
|
|
99
143
|
}
|
|
100
144
|
|
|
101
145
|
// PerimeterX: Check for X-PX-Authorization header (primary indicator)
|
|
102
146
|
// Reference: https://github.com/scrapfly/Antibot-Detector/blob/main/detectors/antibot/detect-perimeterx.json
|
|
103
147
|
if (getHeader('x-px-authorization')) {
|
|
104
|
-
return
|
|
148
|
+
return byHeaders('perimeterx')
|
|
105
149
|
}
|
|
106
150
|
|
|
107
151
|
// PerimeterX: Check for window._pxAppId, pxInit, or _pxAction in html
|
|
108
152
|
// Reference: https://github.com/scrapfly/Antibot-Detector/blob/main/detectors/antibot/detect-perimeterx.json
|
|
109
|
-
if (
|
|
110
|
-
return
|
|
153
|
+
if (hasAnyHtml(['window._pxAppId', 'pxInit', '_pxAction'])) {
|
|
154
|
+
return byHtml('perimeterx')
|
|
111
155
|
}
|
|
112
156
|
|
|
113
157
|
// PerimeterX: _px3 or _pxhd cookies
|
|
114
|
-
if (
|
|
115
|
-
return
|
|
158
|
+
if (hasAnyCookie(['_px3=', '_pxhd='])) {
|
|
159
|
+
return byCookies('perimeterx')
|
|
116
160
|
}
|
|
117
161
|
|
|
118
162
|
// Shape Security: Check for dynamic header patterns x-[8chars]-[abcdfz]
|
|
119
163
|
// These headers use 8 random characters followed by suffixes like -a, -b, -c, -d, -f, or -z
|
|
120
164
|
// Reference: https://github.com/scrapfly/Antibot-Detector/blob/main/detectors/antibot/detect-shapesecurity.json
|
|
121
|
-
const headerNames =
|
|
165
|
+
const headerNames = getHeaderNames(headers)
|
|
122
166
|
for (const name of headerNames) {
|
|
123
167
|
if (/^x-[a-z0-9]{8}-[abcdfz]$/i.test(name)) {
|
|
124
|
-
return
|
|
168
|
+
return byHeaders('shapesecurity')
|
|
125
169
|
}
|
|
126
170
|
}
|
|
127
171
|
|
|
128
172
|
// Shape Security: Check for 'shapesecurity' text in response html
|
|
129
173
|
// Reference: https://github.com/scrapfly/Antibot-Detector/blob/main/detectors/antibot/detect-shapesecurity.json
|
|
130
|
-
if (
|
|
131
|
-
return
|
|
174
|
+
if (hasAnyHtml(['shapesecurity'])) {
|
|
175
|
+
return byHtml('shapesecurity')
|
|
132
176
|
}
|
|
133
177
|
|
|
134
178
|
// Kasada: Check for x-kasada or x-kasada-challenge headers
|
|
135
179
|
// Reference: https://github.com/scrapfly/Antibot-Detector/blob/main/detectors/antibot/detect-kasada.json
|
|
136
|
-
if (
|
|
137
|
-
return
|
|
180
|
+
if (hasAnyHeader(['x-kasada', 'x-kasada-challenge'])) {
|
|
181
|
+
return byHeaders('kasada')
|
|
138
182
|
}
|
|
139
183
|
|
|
140
184
|
// Kasada: Check for __kasada global object or kasada.js script in html
|
|
141
185
|
// Reference: https://github.com/scrapfly/Antibot-Detector/blob/main/detectors/antibot/detect-kasada.json
|
|
142
|
-
if (
|
|
143
|
-
return
|
|
186
|
+
if (hasAnyHtml(['__kasada', 'kasada.js'])) {
|
|
187
|
+
return byHtml('kasada')
|
|
144
188
|
}
|
|
145
189
|
|
|
146
190
|
// Imperva/Incapsula: Check for x-cdn header with 'Incapsula' value or x-iinfo header
|
|
147
191
|
// Reference: https://github.com/scrapfly/Antibot-Detector/blob/main/detectors/antibot/detect-incapsula.json
|
|
148
|
-
if (getHeader('x-cdn') === 'Incapsula' ||
|
|
149
|
-
return
|
|
192
|
+
if (getHeader('x-cdn') === 'Incapsula' || hasAnyHeader(['x-iinfo'])) {
|
|
193
|
+
return byHeaders('imperva')
|
|
150
194
|
}
|
|
151
195
|
|
|
152
196
|
// Imperva/Incapsula: Check for 'incapsula' or 'imperva' text in response html
|
|
153
197
|
// Reference: https://github.com/scrapfly/Antibot-Detector/blob/main/detectors/antibot/detect-incapsula.json
|
|
154
|
-
if (
|
|
155
|
-
return
|
|
198
|
+
if (hasAnyHtml(['incapsula', 'imperva'])) {
|
|
199
|
+
return byHtml('imperva')
|
|
156
200
|
}
|
|
157
201
|
|
|
158
202
|
// Imperva/Incapsula: incap_ses_, visid_incap_, or reese84 cookies
|
|
159
203
|
// Reference: https://github.com/scrapfly/Antibot-Detector/blob/main/detectors/antibot/detect-incapsula.json
|
|
160
|
-
if (
|
|
161
|
-
|
|
162
|
-
hasCookie('visid_incap_') ||
|
|
163
|
-
hasCookie('reese84=')
|
|
164
|
-
) {
|
|
165
|
-
return createResult(true, 'imperva')
|
|
204
|
+
if (hasAnyCookie(['incap_ses_', 'visid_incap_', 'reese84='])) {
|
|
205
|
+
return byCookies('imperva')
|
|
166
206
|
}
|
|
167
207
|
|
|
168
208
|
// Reblaze: rbzid or rbzsessionid cookies
|
|
169
209
|
// Reference: https://github.com/scrapfly/Antibot-Detector/blob/main/detectors/antibot/detect-reblaze.json
|
|
170
|
-
if (
|
|
171
|
-
return
|
|
210
|
+
if (hasAnyCookie(['rbzid=', 'rbzsessionid='])) {
|
|
211
|
+
return byCookies('reblaze')
|
|
172
212
|
}
|
|
173
213
|
|
|
174
214
|
// Reblaze: Check for 'reblaze' text in response html
|
|
175
|
-
if (
|
|
176
|
-
return
|
|
215
|
+
if (hasAnyHtml(['reblaze'])) {
|
|
216
|
+
return byHtml('reblaze')
|
|
177
217
|
}
|
|
178
218
|
|
|
179
219
|
// Cheq: Check for CheqSdk or cheqzone.com in html
|
|
180
220
|
// Reference: https://github.com/scrapfly/Antibot-Detector/blob/main/detectors/antibot/detect-cheq.json
|
|
181
|
-
if (
|
|
182
|
-
return
|
|
221
|
+
if (hasAnyHtml(['CheqSdk', 'cheqzone.com'])) {
|
|
222
|
+
return byHtml('cheq')
|
|
183
223
|
}
|
|
184
224
|
|
|
185
225
|
// Cheq: Check for cheqzone.com or cheq.ai in URL
|
|
186
|
-
if (
|
|
187
|
-
return
|
|
226
|
+
if (hasAnyUrl([/cheqzone\.com/i, /cheq\.ai/i])) {
|
|
227
|
+
return byUrl('cheq')
|
|
188
228
|
}
|
|
189
229
|
|
|
190
230
|
// Sucuri: Check for 'sucuri' text in response html
|
|
191
231
|
// Reference: https://github.com/scrapfly/Antibot-Detector/blob/main/detectors/antibot/detect-sucuri.json
|
|
192
|
-
if (
|
|
193
|
-
return
|
|
232
|
+
if (hasAnyHtml(['sucuri'])) {
|
|
233
|
+
return byHtml('sucuri')
|
|
194
234
|
}
|
|
195
235
|
|
|
196
236
|
// ThreatMetrix: Check for 'ThreatMetrix' in html
|
|
197
237
|
// Reference: https://github.com/scrapfly/Antibot-Detector/blob/main/detectors/antibot/detect-threatmetrix.json
|
|
198
|
-
if (
|
|
199
|
-
return
|
|
238
|
+
if (hasAnyHtml(['ThreatMetrix'])) {
|
|
239
|
+
return byHtml('threatmetrix')
|
|
200
240
|
}
|
|
201
241
|
|
|
202
242
|
// ThreatMetrix: Check for fp/check.js fingerprint endpoint in URL
|
|
203
|
-
if (
|
|
204
|
-
return
|
|
243
|
+
if (hasAnyUrl(['fp/check.js'])) {
|
|
244
|
+
return byUrl('threatmetrix')
|
|
205
245
|
}
|
|
206
246
|
|
|
207
247
|
// Meetrics: Check for 'meetrics' text in response html
|
|
208
248
|
// Reference: https://github.com/scrapfly/Antibot-Detector/blob/main/detectors/antibot/detect-meetrics.json
|
|
209
|
-
if (
|
|
210
|
-
return
|
|
249
|
+
if (hasAnyHtml(['meetrics'])) {
|
|
250
|
+
return byHtml('meetrics')
|
|
211
251
|
}
|
|
212
252
|
|
|
213
253
|
// Meetrics: Check for meetrics.com in URL
|
|
214
|
-
if (
|
|
215
|
-
return
|
|
254
|
+
if (hasAnyUrl([/meetrics\.com/i])) {
|
|
255
|
+
return byUrl('meetrics')
|
|
216
256
|
}
|
|
217
257
|
|
|
218
258
|
// Ocule: Check for ocule.co.uk in html
|
|
219
259
|
// Reference: https://github.com/scrapfly/Antibot-Detector/blob/main/detectors/antibot/detect-ocule.json
|
|
220
|
-
if (
|
|
221
|
-
return
|
|
260
|
+
if (hasAnyHtml(['ocule.co.uk'])) {
|
|
261
|
+
return byHtml('ocule')
|
|
222
262
|
}
|
|
223
263
|
|
|
224
264
|
// Ocule: Check for ocule.co.uk in URL
|
|
225
|
-
if (
|
|
226
|
-
return
|
|
265
|
+
if (hasAnyUrl([/ocule\.co\.uk/i])) {
|
|
266
|
+
return byUrl('ocule')
|
|
227
267
|
}
|
|
228
268
|
|
|
229
269
|
// reCAPTCHA: Check for recaptcha/api, google.com/recaptcha, gstatic.com/recaptcha, or recaptcha.net in URL
|
|
230
270
|
// Reference: https://github.com/scrapfly/Antibot-Detector/blob/main/detectors/captcha/detect-recaptcha.json
|
|
231
271
|
if (
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
urlHas('gstatic.com/recaptcha') ||
|
|
235
|
-
urlHas('recaptcha.net')
|
|
272
|
+
hasAnyUrl(['recaptcha/api', 'gstatic.com/recaptcha', 'recaptcha.net']) ||
|
|
273
|
+
hasAnyUrl([/google\.com\/recaptcha/i])
|
|
236
274
|
) {
|
|
237
|
-
return
|
|
275
|
+
return byUrl('recaptcha')
|
|
238
276
|
}
|
|
239
277
|
|
|
240
278
|
// reCAPTCHA: Check for grecaptcha API usage in html (JavaScript indicator)
|
|
241
279
|
// Note: plain "grecaptcha" is too broad (e.g. ".grecaptcha-badge" CSS appears on normal YouTube pages)
|
|
242
280
|
if (
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
htmlHas('\\b__grecaptcha_cfg\\b', true)
|
|
281
|
+
hasAnyHtml([
|
|
282
|
+
/\b(?:window\.)?grecaptcha\s*\.(?:execute|render|ready|getResponse|enterprise)\b/i,
|
|
283
|
+
/\b(?:window\.)?grecaptcha\s*\(/i,
|
|
284
|
+
/\b__grecaptcha_cfg\b/i
|
|
285
|
+
])
|
|
249
286
|
) {
|
|
250
|
-
return
|
|
287
|
+
return byHtml('recaptcha')
|
|
251
288
|
}
|
|
252
289
|
|
|
253
290
|
// reCAPTCHA: Check for g-recaptcha container class in html
|
|
254
291
|
// Reference: https://github.com/scrapfly/Antibot-Detector/blob/main/detectors/captcha/detect-recaptcha.json
|
|
255
|
-
if (
|
|
256
|
-
return
|
|
292
|
+
if (hasAnyHtml(['g-recaptcha'])) {
|
|
293
|
+
return byHtml('recaptcha')
|
|
257
294
|
}
|
|
258
295
|
|
|
259
296
|
// hCaptcha: Check for hcaptcha.com domain in URL
|
|
260
297
|
// Reference: https://github.com/scrapfly/Antibot-Detector/blob/main/detectors/captcha/detect-hcaptcha.json
|
|
261
|
-
if (
|
|
262
|
-
return
|
|
298
|
+
if (hasAnyUrl([/hcaptcha\.com/i])) {
|
|
299
|
+
return byUrl('hcaptcha')
|
|
263
300
|
}
|
|
264
301
|
|
|
265
302
|
// hCaptcha: Check for hcaptcha.com API domain or h-captcha container class in html
|
|
266
303
|
// Reference: https://github.com/scrapfly/Antibot-Detector/blob/main/detectors/captcha/detect-hcaptcha.json
|
|
267
304
|
// Note: bare 'hcaptcha' matches too broadly (could appear in articles discussing hCaptcha)
|
|
268
|
-
if (
|
|
269
|
-
return
|
|
305
|
+
if (hasAnyHtml(['hcaptcha.com', 'h-captcha'])) {
|
|
306
|
+
return byHtml('hcaptcha')
|
|
270
307
|
}
|
|
271
308
|
|
|
272
309
|
// FunCaptcha (Arkose Labs): Check for arkoselabs.com or funcaptcha in URL
|
|
273
310
|
// Reference: https://github.com/scrapfly/Antibot-Detector/blob/main/detectors/captcha/detect-funcaptcha.json
|
|
274
|
-
if (
|
|
275
|
-
return
|
|
311
|
+
if (hasAnyUrl([/arkoselabs\.com/i]) || hasAnyUrl(['funcaptcha'])) {
|
|
312
|
+
return byUrl('funcaptcha')
|
|
276
313
|
}
|
|
277
314
|
|
|
278
315
|
// FunCaptcha (Arkose Labs): Check for arkoselabs.com API domain or funcaptcha in html
|
|
279
316
|
// Reference: https://github.com/scrapfly/Antibot-Detector/blob/main/detectors/captcha/detect-funcaptcha.json
|
|
280
317
|
// Note: bare 'arkose' matches too broadly (e.g. Facebook bundles Arkose SDK for login without blocking content)
|
|
281
|
-
if (
|
|
282
|
-
return
|
|
318
|
+
if (hasAnyHtml(['arkoselabs.com', 'funcaptcha'])) {
|
|
319
|
+
return byHtml('funcaptcha')
|
|
283
320
|
}
|
|
284
321
|
|
|
285
322
|
// GeeTest: Check for geetest.com domain in URL
|
|
286
323
|
// Reference: https://github.com/scrapfly/Antibot-Detector/blob/main/detectors/captcha/detect-geetest.json
|
|
287
|
-
if (
|
|
288
|
-
return
|
|
324
|
+
if (hasAnyUrl([/geetest\.com/i])) {
|
|
325
|
+
return byUrl('geetest')
|
|
289
326
|
}
|
|
290
327
|
|
|
291
328
|
// GeeTest: Check for geetest object or text in html
|
|
292
329
|
// Reference: https://github.com/scrapfly/Antibot-Detector/blob/main/detectors/captcha/detect-geetest.json
|
|
293
330
|
// Note: bare 'gt.js' removed (too generic, any script named gt.js would match)
|
|
294
|
-
if (
|
|
295
|
-
return
|
|
331
|
+
if (hasAnyHtml(['geetest'])) {
|
|
332
|
+
return byHtml('geetest')
|
|
296
333
|
}
|
|
297
334
|
|
|
298
335
|
// Cloudflare Turnstile: Check for challenges.cloudflare.com/turnstile in URL
|
|
299
|
-
if (
|
|
300
|
-
return
|
|
336
|
+
if (hasAnyUrl([/challenges\.cloudflare\.com\/turnstile/i])) {
|
|
337
|
+
return byUrl('cloudflare-turnstile')
|
|
301
338
|
}
|
|
302
339
|
|
|
303
340
|
// Cloudflare Turnstile: Check for cf-turnstile class or turnstile API script in html
|
|
304
341
|
// Note: bare 'turnstile' matches too broadly (common English word)
|
|
305
|
-
if (
|
|
306
|
-
|
|
307
|
-
htmlHas('challenges.cloudflare.com/turnstile')
|
|
308
|
-
) {
|
|
309
|
-
return createResult(true, 'cloudflare-turnstile')
|
|
342
|
+
if (hasAnyHtml(['cf-turnstile', 'challenges.cloudflare.com/turnstile'])) {
|
|
343
|
+
return byHtml('cloudflare-turnstile')
|
|
310
344
|
}
|
|
311
345
|
|
|
312
346
|
// Friendly Captcha: Check for friendlycaptcha.com in URL
|
|
313
347
|
// Reference: https://github.com/scrapfly/Antibot-Detector/blob/main/detectors/captcha/detect-friendlycaptcha.json
|
|
314
|
-
if (
|
|
315
|
-
return
|
|
348
|
+
if (hasAnyUrl([/friendlycaptcha\.com/i])) {
|
|
349
|
+
return byUrl('friendly-captcha')
|
|
316
350
|
}
|
|
317
351
|
|
|
318
352
|
// Friendly Captcha: Check for frc-captcha container or friendlyChallenge object in html
|
|
319
|
-
if (
|
|
320
|
-
return
|
|
353
|
+
if (hasAnyHtml(['frc-captcha', 'friendlyChallenge'])) {
|
|
354
|
+
return byHtml('friendly-captcha')
|
|
321
355
|
}
|
|
322
356
|
|
|
323
357
|
// Captcha.eu: Check for captcha.eu in URL
|
|
324
358
|
// Reference: https://github.com/scrapfly/Antibot-Detector/blob/main/detectors/captcha/detect-captchaeu.json
|
|
325
|
-
if (
|
|
326
|
-
return
|
|
359
|
+
if (hasAnyUrl([/captcha\.eu/i])) {
|
|
360
|
+
return byUrl('captcha-eu')
|
|
327
361
|
}
|
|
328
362
|
|
|
329
363
|
// Captcha.eu: Check for CaptchaEU or captchaeu in html
|
|
330
|
-
if (
|
|
331
|
-
return
|
|
364
|
+
if (hasAnyHtml(['CaptchaEU', 'captchaeu'])) {
|
|
365
|
+
return byHtml('captcha-eu')
|
|
332
366
|
}
|
|
333
367
|
|
|
334
368
|
// QCloud Captcha (Tencent): Check for turing.captcha.qcloud.com in URL
|
|
335
369
|
// Reference: https://github.com/scrapfly/Antibot-Detector/blob/main/detectors/captcha/detect-qcloud.json
|
|
336
|
-
if (
|
|
337
|
-
return
|
|
370
|
+
if (hasAnyUrl([/turing\.captcha\.qcloud\.com/i])) {
|
|
371
|
+
return byUrl('qcloud-captcha')
|
|
338
372
|
}
|
|
339
373
|
|
|
340
374
|
// QCloud Captcha: Check for TencentCaptcha or turing.captcha in html
|
|
341
|
-
if (
|
|
342
|
-
return
|
|
375
|
+
if (hasAnyHtml(['TencentCaptcha', 'turing.captcha'])) {
|
|
376
|
+
return byHtml('qcloud-captcha')
|
|
343
377
|
}
|
|
344
378
|
|
|
345
379
|
// AliExpress CAPTCHA: Check for punish?x5secdata in URL
|
|
346
380
|
// Reference: https://github.com/scrapfly/Antibot-Detector/blob/main/detectors/captcha/detect-aliexpress.json
|
|
347
|
-
if (
|
|
348
|
-
return
|
|
381
|
+
if (hasAnyUrl([/punish\?x5secdata/i])) {
|
|
382
|
+
return byUrl('aliexpress-captcha')
|
|
349
383
|
}
|
|
350
384
|
|
|
351
385
|
// AliExpress CAPTCHA: Check for x5secdata in html
|
|
352
|
-
if (
|
|
353
|
-
return
|
|
386
|
+
if (hasAnyHtml(['x5secdata'])) {
|
|
387
|
+
return byHtml('aliexpress-captcha')
|
|
354
388
|
}
|
|
355
389
|
|
|
356
390
|
// LinkedIn: trkCode=bf cookie ("bot filter") is set when LinkedIn blocks a request
|
|
357
|
-
if (
|
|
358
|
-
return
|
|
391
|
+
if (hasAnyCookie(['trkCode=bf'])) {
|
|
392
|
+
return byCookies('linkedin')
|
|
359
393
|
}
|
|
360
394
|
|
|
361
395
|
// YouTube: empty title pattern indicates a degraded response requiring BotGuard JS attestation
|
|
362
396
|
// Normal pages have `<title>Video Title - YouTube</title>`, bots get `<title> - YouTube</title>`
|
|
363
|
-
if (
|
|
364
|
-
return
|
|
397
|
+
if (hasAnyHtml([/<title>\s*-\s*YouTube<\/title>/i])) {
|
|
398
|
+
return byHtml('youtube')
|
|
365
399
|
}
|
|
366
400
|
|
|
367
401
|
// AWS WAF: Check for x-amzn-waf-action or x-amzn-requestid headers
|
|
368
402
|
// Reference: https://github.com/scrapfly/Antibot-Detector/blob/main/detectors/antibot/detect-aws-waf.json
|
|
369
|
-
if (
|
|
370
|
-
return
|
|
403
|
+
if (hasAnyHeader(['x-amzn-waf-action', 'x-amzn-requestid'])) {
|
|
404
|
+
return byHeaders('aws-waf')
|
|
371
405
|
}
|
|
372
406
|
|
|
373
407
|
// AWS WAF: Check for aws-waf or awswaf text in html
|
|
374
|
-
if (
|
|
375
|
-
return
|
|
408
|
+
if (hasAnyHtml(['aws-waf', 'awswaf'])) {
|
|
409
|
+
return byHtml('aws-waf')
|
|
376
410
|
}
|
|
377
411
|
|
|
378
412
|
// AWS WAF: aws-waf-token cookie
|
|
379
|
-
if (
|
|
380
|
-
return
|
|
413
|
+
if (hasAnyCookie(['aws-waf-token='])) {
|
|
414
|
+
return byCookies('aws-waf')
|
|
381
415
|
}
|
|
382
416
|
|
|
383
|
-
return createResult(false, null)
|
|
417
|
+
return createResult(false, null, null)
|
|
384
418
|
}
|
|
385
419
|
|
|
386
420
|
const isAntibot = (input = {}) => {
|