@rester159/blacktip 0.1.0 → 0.4.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/CHANGELOG.md +190 -0
- package/README.md +95 -0
- package/dist/behavioral/parsers.d.ts +89 -0
- package/dist/behavioral/parsers.d.ts.map +1 -0
- package/dist/behavioral/parsers.js +223 -0
- package/dist/behavioral/parsers.js.map +1 -0
- package/dist/blacktip.d.ts +86 -0
- package/dist/blacktip.d.ts.map +1 -1
- package/dist/blacktip.js +193 -0
- package/dist/blacktip.js.map +1 -1
- package/dist/browser-core.d.ts.map +1 -1
- package/dist/browser-core.js +125 -33
- package/dist/browser-core.js.map +1 -1
- package/dist/diagnostics.d.ts +150 -0
- package/dist/diagnostics.d.ts.map +1 -0
- package/dist/diagnostics.js +389 -0
- package/dist/diagnostics.js.map +1 -0
- package/dist/identity-pool.d.ts +160 -0
- package/dist/identity-pool.d.ts.map +1 -0
- package/dist/identity-pool.js +288 -0
- package/dist/identity-pool.js.map +1 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -1
- package/dist/tls-side-channel.d.ts +82 -0
- package/dist/tls-side-channel.d.ts.map +1 -0
- package/dist/tls-side-channel.js +241 -0
- package/dist/tls-side-channel.js.map +1 -0
- package/dist/types.d.ts +26 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/docs/akamai-bypass.md +257 -0
- package/docs/anti-bot-validation.md +84 -0
- package/docs/calibration-validation.md +93 -0
- package/docs/identity-pool.md +176 -0
- package/docs/tls-side-channel.md +83 -0
- package/native/tls-client/go.mod +21 -0
- package/native/tls-client/go.sum +36 -0
- package/native/tls-client/main.go +216 -0
- package/package.json +8 -2
- package/scripts/fit-cmu-keystroke.mjs +186 -0
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Diagnostic primitives for stealth validation.
|
|
3
|
+
*
|
|
4
|
+
* Captures the actual TLS / HTTP2 / HTTP header fingerprint that BlackTip
|
|
5
|
+
* is sending, and queries free IP reputation services to score the
|
|
6
|
+
* current network. Used to diagnose why a specific anti-bot target is
|
|
7
|
+
* blocking us — see docs/akamai-bypass.md for the full methodology.
|
|
8
|
+
*
|
|
9
|
+
* The primitives here run via the active BlackTip browser session — they
|
|
10
|
+
* don't make HTTP requests directly from Node, because the whole point
|
|
11
|
+
* is to capture what the browser actually sends, not what Node would send.
|
|
12
|
+
*/
|
|
13
|
+
import type { BlackTip } from './blacktip.js';
|
|
14
|
+
export interface FingerprintSnapshot {
|
|
15
|
+
capturedAt: string;
|
|
16
|
+
ip: string | null;
|
|
17
|
+
tls: {
|
|
18
|
+
ja3: string | null;
|
|
19
|
+
ja3Hash: string | null;
|
|
20
|
+
ja4: string | null;
|
|
21
|
+
firstCipher: string | null;
|
|
22
|
+
cipherCount: number | null;
|
|
23
|
+
firstExtension: string | null;
|
|
24
|
+
extensionCount: number | null;
|
|
25
|
+
tlsVersion: string | null;
|
|
26
|
+
/** True if first cipher is GREASE — Chrome's signature */
|
|
27
|
+
hasGreaseCipher: boolean;
|
|
28
|
+
/** True if first extension is GREASE — Chrome's signature */
|
|
29
|
+
hasGreaseExtension: boolean;
|
|
30
|
+
/** True if JA4 starts with t13d — TLS 1.3 with Chrome's count signature */
|
|
31
|
+
isChromeLikeJa4: boolean;
|
|
32
|
+
};
|
|
33
|
+
http2: {
|
|
34
|
+
akamaiFingerprint: string | null;
|
|
35
|
+
akamaiFingerprintHash: string | null;
|
|
36
|
+
/** Sequence of frame types sent by the client (Chrome: SETTINGS, WINDOW_UPDATE, HEADERS) */
|
|
37
|
+
sentFrames: string[] | null;
|
|
38
|
+
};
|
|
39
|
+
headers: {
|
|
40
|
+
userAgent: string | null;
|
|
41
|
+
secChUa: string | null;
|
|
42
|
+
secChUaMobile: string | null;
|
|
43
|
+
secChUaPlatform: string | null;
|
|
44
|
+
acceptLanguage: string | null;
|
|
45
|
+
acceptEncoding: string | null;
|
|
46
|
+
secFetchSite: string | null;
|
|
47
|
+
secFetchMode: string | null;
|
|
48
|
+
secFetchDest: string | null;
|
|
49
|
+
secFetchUser: string | null;
|
|
50
|
+
upgradeInsecureRequests: string | null;
|
|
51
|
+
/** Chrome version parsed from User-Agent (e.g. 125 from "Chrome/125.0.0.0") */
|
|
52
|
+
uaChromeVersion: number | null;
|
|
53
|
+
/** Chrome version parsed from Sec-Ch-Ua (e.g. 146 from `"Google Chrome";v="146"`) */
|
|
54
|
+
secChUaChromeVersion: number | null;
|
|
55
|
+
/**
|
|
56
|
+
* THE CRITICAL CHECK: do User-Agent and Sec-Ch-Ua report the same Chrome
|
|
57
|
+
* version? If false, Akamai / DataDome / PerimeterX will catch you. This
|
|
58
|
+
* is the L016 fingerprint consistency signal.
|
|
59
|
+
*/
|
|
60
|
+
uaConsistent: boolean;
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
export interface IpReputationResult {
|
|
64
|
+
ip: string | null;
|
|
65
|
+
hostname: string | null;
|
|
66
|
+
asn: string | null;
|
|
67
|
+
org: string | null;
|
|
68
|
+
city: string | null;
|
|
69
|
+
region: string | null;
|
|
70
|
+
country: string | null;
|
|
71
|
+
loc: string | null;
|
|
72
|
+
timezone: string | null;
|
|
73
|
+
/** Heuristic: ASN matches a well-known datacenter / cloud provider */
|
|
74
|
+
isDatacenter: boolean;
|
|
75
|
+
/** Heuristic: ASN matches a residential ISP */
|
|
76
|
+
isResidential: boolean;
|
|
77
|
+
/** Free-form notes from the heuristic checks */
|
|
78
|
+
notes: string[];
|
|
79
|
+
}
|
|
80
|
+
export interface AkamaiTestResult {
|
|
81
|
+
url: string;
|
|
82
|
+
/** True if the page loaded normally (not the Akamai Access Denied error) */
|
|
83
|
+
passed: boolean;
|
|
84
|
+
finalUrl: string;
|
|
85
|
+
title: string;
|
|
86
|
+
/** Akamai's reference number from the block page (null if not blocked) */
|
|
87
|
+
akamaiReference: string | null;
|
|
88
|
+
/** First 300 chars of body — useful for diagnosis */
|
|
89
|
+
bodyPreview: string;
|
|
90
|
+
/** Suggested next step based on what we observed */
|
|
91
|
+
suggestion: string;
|
|
92
|
+
durationMs: number;
|
|
93
|
+
}
|
|
94
|
+
/** Anti-bot vendors that BlackTip's diagnostics can recognize. */
|
|
95
|
+
export type AntiBotVendor = 'akamai' | 'datadome' | 'cloudflare' | 'perimeterx' | 'imperva' | 'kasada' | 'arkose' | 'unknown';
|
|
96
|
+
export interface AntiBotTestResult {
|
|
97
|
+
url: string;
|
|
98
|
+
/** True if the page loaded normally (no recognised challenge or block) */
|
|
99
|
+
passed: boolean;
|
|
100
|
+
finalUrl: string;
|
|
101
|
+
title: string;
|
|
102
|
+
/** Vendors that left a tell on the rendered page (block OR challenge interstitial) */
|
|
103
|
+
detectedVendors: AntiBotVendor[];
|
|
104
|
+
/** Vendor signatures observed even on a passing page (cookies, scripts, headers) */
|
|
105
|
+
vendorSignals: {
|
|
106
|
+
vendor: AntiBotVendor;
|
|
107
|
+
signal: string;
|
|
108
|
+
}[];
|
|
109
|
+
/** Akamai reference number, if a block from Akamai */
|
|
110
|
+
akamaiReference: string | null;
|
|
111
|
+
/** First 300 chars of body — useful for diagnosis */
|
|
112
|
+
bodyPreview: string;
|
|
113
|
+
/** Suggested next step based on what we observed */
|
|
114
|
+
suggestion: string;
|
|
115
|
+
durationMs: number;
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Capture the active session's TLS, HTTP/2, and HTTP header fingerprint
|
|
119
|
+
* by navigating to tls.peet.ws/api/all and httpbin.org/headers.
|
|
120
|
+
*
|
|
121
|
+
* Returns a structured snapshot you can use to verify your stealth state
|
|
122
|
+
* before hitting a real target. The most important field is
|
|
123
|
+
* `headers.uaConsistent` — if that's false, you're on a pre-v0.2.0 build
|
|
124
|
+
* with the L016 bug.
|
|
125
|
+
*/
|
|
126
|
+
export declare function captureFingerprint(bt: BlackTip): Promise<FingerprintSnapshot>;
|
|
127
|
+
/**
|
|
128
|
+
* Query the active session's egress IP and ASN, score it against known
|
|
129
|
+
* datacenter / residential patterns, and return a structured result.
|
|
130
|
+
*
|
|
131
|
+
* Uses the free ipinfo.io endpoint which doesn't require an API key for
|
|
132
|
+
* basic queries. Doesn't query commercial reputation services (those
|
|
133
|
+
* require API keys); for those, integrate at the caller's level.
|
|
134
|
+
*/
|
|
135
|
+
export declare function checkIpReputation(bt: BlackTip): Promise<IpReputationResult>;
|
|
136
|
+
/**
|
|
137
|
+
* Visit an Akamai-protected URL and report the result with diagnosis.
|
|
138
|
+
* Recognizes the Akamai Access Denied error page format and extracts
|
|
139
|
+
* the reference number for triage.
|
|
140
|
+
*/
|
|
141
|
+
export declare function testAgainstAkamai(bt: BlackTip, url: string): Promise<AkamaiTestResult>;
|
|
142
|
+
/**
|
|
143
|
+
* Visit a URL and report whether any major anti-bot vendor served a
|
|
144
|
+
* challenge or block. Recognises Akamai, DataDome, Cloudflare, PerimeterX/HUMAN,
|
|
145
|
+
* Imperva, Kasada and Arkose. Captures vendor signals (cookies, scripts) even
|
|
146
|
+
* on a passing page so you can verify a target is actually protected and
|
|
147
|
+
* BlackTip is sliding past it — not a false negative on an unprotected URL.
|
|
148
|
+
*/
|
|
149
|
+
export declare function testAgainstAntiBot(bt: BlackTip, url: string): Promise<AntiBotTestResult>;
|
|
150
|
+
//# sourceMappingURL=diagnostics.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"diagnostics.d.ts","sourceRoot":"","sources":["../src/diagnostics.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,389 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Diagnostic primitives for stealth validation.
|
|
3
|
+
*
|
|
4
|
+
* Captures the actual TLS / HTTP2 / HTTP header fingerprint that BlackTip
|
|
5
|
+
* is sending, and queries free IP reputation services to score the
|
|
6
|
+
* current network. Used to diagnose why a specific anti-bot target is
|
|
7
|
+
* blocking us — see docs/akamai-bypass.md for the full methodology.
|
|
8
|
+
*
|
|
9
|
+
* The primitives here run via the active BlackTip browser session — they
|
|
10
|
+
* don't make HTTP requests directly from Node, because the whole point
|
|
11
|
+
* is to capture what the browser actually sends, not what Node would send.
|
|
12
|
+
*/
|
|
13
|
+
// ── Datacenter ASN heuristics ──
|
|
14
|
+
//
|
|
15
|
+
// Not exhaustive — just covers the major cloud providers and known
|
|
16
|
+
// datacenter ranges. If your IP is on one of these, Akamai will almost
|
|
17
|
+
// certainly flag it. Add to this list as new providers come up.
|
|
18
|
+
const DATACENTER_ASN_PATTERNS = [
|
|
19
|
+
{ pattern: /AS16509|AS14618/, name: 'Amazon AWS' },
|
|
20
|
+
{ pattern: /AS15169/, name: 'Google Cloud' },
|
|
21
|
+
{ pattern: /AS8075/, name: 'Microsoft Azure' },
|
|
22
|
+
{ pattern: /AS16276/, name: 'OVH' },
|
|
23
|
+
{ pattern: /AS14061/, name: 'DigitalOcean' },
|
|
24
|
+
{ pattern: /AS20473/, name: 'Choopa / Vultr' },
|
|
25
|
+
{ pattern: /AS24940/, name: 'Hetzner' },
|
|
26
|
+
{ pattern: /AS63949/, name: 'Linode / Akamai (compute)' },
|
|
27
|
+
{ pattern: /AS133752/, name: 'Leaseweb' },
|
|
28
|
+
{ pattern: /AS54641|AS19551/, name: 'Cloudflare (compute)' },
|
|
29
|
+
{ pattern: /AS396982/, name: 'Google Cloud Platform (compute)' },
|
|
30
|
+
{ pattern: /AS200600/, name: 'Aeza Group (cheap VPS)' },
|
|
31
|
+
];
|
|
32
|
+
const RESIDENTIAL_ASN_PATTERNS = [
|
|
33
|
+
{ pattern: /AS5650|AS22773/, name: 'Frontier Communications / Cox (residential US)' },
|
|
34
|
+
{ pattern: /AS7922/, name: 'Comcast (residential US)' },
|
|
35
|
+
{ pattern: /AS7018/, name: 'AT&T (residential US)' },
|
|
36
|
+
{ pattern: /AS20057|AS20115/, name: 'Charter / Spectrum (residential US)' },
|
|
37
|
+
{ pattern: /AS6128/, name: 'Optimum / Cablevision (residential US)' },
|
|
38
|
+
{ pattern: /AS33363/, name: 'BHN / Spectrum (residential US)' },
|
|
39
|
+
{ pattern: /AS5089/, name: 'Virgin Media (residential UK)' },
|
|
40
|
+
{ pattern: /AS3320/, name: 'Deutsche Telekom (residential DE)' },
|
|
41
|
+
{ pattern: /AS9121/, name: 'Türk Telekom (residential TR)' },
|
|
42
|
+
];
|
|
43
|
+
// ── captureFingerprint ──
|
|
44
|
+
/**
|
|
45
|
+
* Capture the active session's TLS, HTTP/2, and HTTP header fingerprint
|
|
46
|
+
* by navigating to tls.peet.ws/api/all and httpbin.org/headers.
|
|
47
|
+
*
|
|
48
|
+
* Returns a structured snapshot you can use to verify your stealth state
|
|
49
|
+
* before hitting a real target. The most important field is
|
|
50
|
+
* `headers.uaConsistent` — if that's false, you're on a pre-v0.2.0 build
|
|
51
|
+
* with the L016 bug.
|
|
52
|
+
*/
|
|
53
|
+
export async function captureFingerprint(bt) {
|
|
54
|
+
// Step 1: TLS + HTTP/2 fingerprint via tls.peet.ws
|
|
55
|
+
await bt.navigate('https://tls.peet.ws/api/all');
|
|
56
|
+
const peetRaw = (await bt.executeJS('document.body.innerText'));
|
|
57
|
+
const peet = JSON.parse(peetRaw);
|
|
58
|
+
// Step 2: HTTP headers via httpbin.org/headers
|
|
59
|
+
await bt.navigate('https://httpbin.org/headers');
|
|
60
|
+
const hbRaw = (await bt.executeJS('document.body.innerText'));
|
|
61
|
+
const hb = JSON.parse(hbRaw);
|
|
62
|
+
const headers = hb.headers ?? {};
|
|
63
|
+
const userAgent = headers['User-Agent'] ?? null;
|
|
64
|
+
const secChUa = headers['Sec-Ch-Ua'] ?? null;
|
|
65
|
+
// Parse Chrome version from User-Agent: "...Chrome/125.0.0.0..."
|
|
66
|
+
const uaMatch = userAgent ? userAgent.match(/Chrome\/(\d+)/) : null;
|
|
67
|
+
const uaChromeVersion = uaMatch ? parseInt(uaMatch[1], 10) : null;
|
|
68
|
+
// Parse Chrome version from Sec-Ch-Ua: `"Google Chrome";v="146"`
|
|
69
|
+
const chuaMatch = secChUa ? secChUa.match(/"Google Chrome";v="(\d+)/) : null;
|
|
70
|
+
const secChUaChromeVersion = chuaMatch ? parseInt(chuaMatch[1], 10) : null;
|
|
71
|
+
const uaConsistent = uaChromeVersion != null &&
|
|
72
|
+
secChUaChromeVersion != null &&
|
|
73
|
+
uaChromeVersion === secChUaChromeVersion;
|
|
74
|
+
const tlsBlock = peet.tls ?? {};
|
|
75
|
+
const ciphers = tlsBlock.ciphers ?? [];
|
|
76
|
+
const extensions = tlsBlock.extensions ?? [];
|
|
77
|
+
const ja4 = tlsBlock.ja4 ?? null;
|
|
78
|
+
return {
|
|
79
|
+
capturedAt: new Date().toISOString(),
|
|
80
|
+
ip: peet.ip ? peet.ip.split(':')[0] ?? null : null,
|
|
81
|
+
tls: {
|
|
82
|
+
ja3: tlsBlock.ja3 ?? null,
|
|
83
|
+
ja3Hash: tlsBlock.ja3_hash ?? null,
|
|
84
|
+
ja4,
|
|
85
|
+
firstCipher: ciphers[0] ?? null,
|
|
86
|
+
cipherCount: ciphers.length || null,
|
|
87
|
+
firstExtension: extensions[0]?.name ?? null,
|
|
88
|
+
extensionCount: extensions.length || null,
|
|
89
|
+
tlsVersion: tlsBlock.tls_version_negotiated ?? null,
|
|
90
|
+
hasGreaseCipher: !!(ciphers[0] && /GREASE/i.test(ciphers[0])),
|
|
91
|
+
hasGreaseExtension: !!(extensions[0]?.name && /GREASE/i.test(extensions[0].name)),
|
|
92
|
+
isChromeLikeJa4: !!(ja4 && /^t13d/.test(ja4)),
|
|
93
|
+
},
|
|
94
|
+
http2: {
|
|
95
|
+
akamaiFingerprint: peet.http2?.akamai_fingerprint ?? null,
|
|
96
|
+
akamaiFingerprintHash: peet.http2?.akamai_fingerprint_hash ?? null,
|
|
97
|
+
sentFrames: peet.http2?.sent_frames?.map((f) => f.frame_type ?? '') ?? null,
|
|
98
|
+
},
|
|
99
|
+
headers: {
|
|
100
|
+
userAgent,
|
|
101
|
+
secChUa,
|
|
102
|
+
secChUaMobile: headers['Sec-Ch-Ua-Mobile'] ?? null,
|
|
103
|
+
secChUaPlatform: headers['Sec-Ch-Ua-Platform'] ?? null,
|
|
104
|
+
acceptLanguage: headers['Accept-Language'] ?? null,
|
|
105
|
+
acceptEncoding: headers['Accept-Encoding'] ?? null,
|
|
106
|
+
secFetchSite: headers['Sec-Fetch-Site'] ?? null,
|
|
107
|
+
secFetchMode: headers['Sec-Fetch-Mode'] ?? null,
|
|
108
|
+
secFetchDest: headers['Sec-Fetch-Dest'] ?? null,
|
|
109
|
+
secFetchUser: headers['Sec-Fetch-User'] ?? null,
|
|
110
|
+
upgradeInsecureRequests: headers['Upgrade-Insecure-Requests'] ?? null,
|
|
111
|
+
uaChromeVersion,
|
|
112
|
+
secChUaChromeVersion,
|
|
113
|
+
uaConsistent,
|
|
114
|
+
},
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
// ── checkIpReputation ──
|
|
118
|
+
/**
|
|
119
|
+
* Query the active session's egress IP and ASN, score it against known
|
|
120
|
+
* datacenter / residential patterns, and return a structured result.
|
|
121
|
+
*
|
|
122
|
+
* Uses the free ipinfo.io endpoint which doesn't require an API key for
|
|
123
|
+
* basic queries. Doesn't query commercial reputation services (those
|
|
124
|
+
* require API keys); for those, integrate at the caller's level.
|
|
125
|
+
*/
|
|
126
|
+
export async function checkIpReputation(bt) {
|
|
127
|
+
await bt.navigate('https://ipinfo.io/json');
|
|
128
|
+
const raw = (await bt.executeJS('document.body.innerText'));
|
|
129
|
+
let parsed;
|
|
130
|
+
try {
|
|
131
|
+
parsed = JSON.parse(raw);
|
|
132
|
+
}
|
|
133
|
+
catch {
|
|
134
|
+
parsed = {};
|
|
135
|
+
}
|
|
136
|
+
const org = parsed.org ?? null;
|
|
137
|
+
const notes = [];
|
|
138
|
+
let isDatacenter = false;
|
|
139
|
+
let isResidential = false;
|
|
140
|
+
if (org) {
|
|
141
|
+
for (const { pattern, name } of DATACENTER_ASN_PATTERNS) {
|
|
142
|
+
if (pattern.test(org)) {
|
|
143
|
+
isDatacenter = true;
|
|
144
|
+
notes.push(`Matches datacenter ASN: ${name}`);
|
|
145
|
+
break;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
if (!isDatacenter) {
|
|
149
|
+
for (const { pattern, name } of RESIDENTIAL_ASN_PATTERNS) {
|
|
150
|
+
if (pattern.test(org)) {
|
|
151
|
+
isResidential = true;
|
|
152
|
+
notes.push(`Matches residential ISP: ${name}`);
|
|
153
|
+
break;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
if (!isDatacenter && !isResidential) {
|
|
158
|
+
notes.push('ASN not in known datacenter or residential lists — could be either');
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
else {
|
|
162
|
+
notes.push('Could not determine ASN');
|
|
163
|
+
}
|
|
164
|
+
return {
|
|
165
|
+
ip: parsed.ip ?? null,
|
|
166
|
+
hostname: parsed.hostname ?? null,
|
|
167
|
+
asn: org ? (org.match(/AS\d+/) ?? [])[0] ?? null : null,
|
|
168
|
+
org,
|
|
169
|
+
city: parsed.city ?? null,
|
|
170
|
+
region: parsed.region ?? null,
|
|
171
|
+
country: parsed.country ?? null,
|
|
172
|
+
loc: parsed.loc ?? null,
|
|
173
|
+
timezone: parsed.timezone ?? null,
|
|
174
|
+
isDatacenter,
|
|
175
|
+
isResidential,
|
|
176
|
+
notes,
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
// ── testAgainstAkamai ──
|
|
180
|
+
/**
|
|
181
|
+
* Visit an Akamai-protected URL and report the result with diagnosis.
|
|
182
|
+
* Recognizes the Akamai Access Denied error page format and extracts
|
|
183
|
+
* the reference number for triage.
|
|
184
|
+
*/
|
|
185
|
+
export async function testAgainstAkamai(bt, url) {
|
|
186
|
+
const start = Date.now();
|
|
187
|
+
try {
|
|
188
|
+
await bt.navigate(url);
|
|
189
|
+
// Wait for the page to settle a moment so Akamai can render its block
|
|
190
|
+
// page if it's going to.
|
|
191
|
+
await new Promise((r) => setTimeout(r, 1500));
|
|
192
|
+
const info = (await bt.executeJS(`(() => ({
|
|
193
|
+
url: location.href,
|
|
194
|
+
title: document.title,
|
|
195
|
+
bodyPreview: (document.body ? document.body.innerText : '').slice(0, 600),
|
|
196
|
+
}))()`));
|
|
197
|
+
// Akamai Access Denied detection
|
|
198
|
+
const isBlocked = info.title === 'Access Denied' ||
|
|
199
|
+
/You don't have permission to access/i.test(info.bodyPreview) ||
|
|
200
|
+
/errors\.edgesuite\.net/i.test(info.bodyPreview);
|
|
201
|
+
// Extract Akamai reference number if present
|
|
202
|
+
const refMatch = info.bodyPreview.match(/Reference\s*#([0-9a-f.]+)/);
|
|
203
|
+
const akamaiReference = refMatch ? refMatch[1] ?? null : null;
|
|
204
|
+
let suggestion;
|
|
205
|
+
if (!isBlocked) {
|
|
206
|
+
suggestion = 'Page loaded successfully. No Akamai block detected.';
|
|
207
|
+
}
|
|
208
|
+
else {
|
|
209
|
+
suggestion = [
|
|
210
|
+
'Akamai blocked the request at the edge. Diagnosis steps:',
|
|
211
|
+
'1. Run `bt.captureFingerprint()` and check `headers.uaConsistent`. If false, upgrade BlackTip to v0.2.0+.',
|
|
212
|
+
'2. Run `bt.checkIpReputation()`. If `isDatacenter: true`, switch to a residential network or proxy.',
|
|
213
|
+
'3. Test the same URL in your normal Chrome from the same machine. If that ALSO blocks, your IP is flagged — switch networks.',
|
|
214
|
+
'4. Try `bt.warmSession({sites: [...]})` before the target navigation.',
|
|
215
|
+
'5. Try with `userDataDir` set in BlackTipConfig for a persistent profile.',
|
|
216
|
+
`Akamai reference: ${akamaiReference ?? 'unknown'}`,
|
|
217
|
+
].join('\n');
|
|
218
|
+
}
|
|
219
|
+
return {
|
|
220
|
+
url,
|
|
221
|
+
passed: !isBlocked,
|
|
222
|
+
finalUrl: info.url,
|
|
223
|
+
title: info.title,
|
|
224
|
+
akamaiReference,
|
|
225
|
+
bodyPreview: info.bodyPreview.slice(0, 300),
|
|
226
|
+
suggestion,
|
|
227
|
+
durationMs: Date.now() - start,
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
catch (err) {
|
|
231
|
+
return {
|
|
232
|
+
url,
|
|
233
|
+
passed: false,
|
|
234
|
+
finalUrl: url,
|
|
235
|
+
title: '',
|
|
236
|
+
akamaiReference: null,
|
|
237
|
+
bodyPreview: '',
|
|
238
|
+
suggestion: `Navigation threw: ${err instanceof Error ? err.message : String(err)}`,
|
|
239
|
+
durationMs: Date.now() - start,
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
// ── testAgainstAntiBot ──
|
|
244
|
+
//
|
|
245
|
+
// Generic anti-bot probe that recognises Akamai, DataDome, Cloudflare,
|
|
246
|
+
// PerimeterX/HUMAN, Imperva, Kasada and Arkose. Validated against the
|
|
247
|
+
// scoreboard in docs/anti-bot-validation.md.
|
|
248
|
+
const VENDOR_BLOCK_PATTERNS = [
|
|
249
|
+
{ vendor: 'akamai', titleRe: /^Access Denied$/, bodyRe: /You don't have permission to access|errors\.edgesuite\.net/i },
|
|
250
|
+
{ vendor: 'datadome', bodyRe: /geo\.captcha-delivery\.com|captcha-delivery\.com|please enable javascript and cookies to continue|datado\.me/i },
|
|
251
|
+
{ vendor: 'cloudflare', titleRe: /^Just a moment\.\.\.$|^Attention Required! \| Cloudflare$/, bodyRe: /Checking your browser before accessing|cf-browser-verification|challenges\.cloudflare\.com|Sorry, you have been blocked/i },
|
|
252
|
+
{ vendor: 'perimeterx', titleRe: /^Access to this page has been denied/, bodyRe: /Press (?:&|and) Hold to confirm you (?:are|are not) a human|px-captcha|perimeterx\.net|human\.com/i },
|
|
253
|
+
{ vendor: 'imperva', bodyRe: /Request unsuccessful\. Incapsula incident ID|_Incapsula_Resource|Incapsula incident/i },
|
|
254
|
+
{ vendor: 'kasada', bodyRe: /kpsdk|ips\.js\?|kasada/i },
|
|
255
|
+
{ vendor: 'arkose', bodyRe: /client-api\.arkoselabs\.com|funcaptcha/i },
|
|
256
|
+
];
|
|
257
|
+
const VENDOR_SCRIPT_SIGNAL_QUERY = `(() => {
|
|
258
|
+
const out = [];
|
|
259
|
+
const html = document.documentElement.outerHTML;
|
|
260
|
+
if (/datado\\.me|js\\.datadome\\.co/i.test(html)) out.push({vendor:'datadome', signal:'script'});
|
|
261
|
+
if (/perimeterx|px-cdn|px-captcha|human-security/i.test(html)) out.push({vendor:'perimeterx', signal:'script'});
|
|
262
|
+
if (/challenges\\.cloudflare\\.com|cdn-cgi\\/challenge-platform/i.test(html)) out.push({vendor:'cloudflare', signal:'script'});
|
|
263
|
+
if (/akam\\/|ak\\.bmpsdk|akamaihd\\.net\\/sensor/i.test(html)) out.push({vendor:'akamai', signal:'script'});
|
|
264
|
+
if (/x-kpsdk-/i.test(html)) out.push({vendor:'kasada', signal:'script'});
|
|
265
|
+
return JSON.stringify(out);
|
|
266
|
+
})()`;
|
|
267
|
+
// Cookie-name patterns for the vendor cookie checks. Run against the
|
|
268
|
+
// full cookie jar from BlackTip's cookies API (which includes httpOnly
|
|
269
|
+
// cookies that document.cookie can't see — cf_clearance, __cf_bm,
|
|
270
|
+
// _abck, datadome, etc. are all httpOnly).
|
|
271
|
+
const VENDOR_COOKIE_PATTERNS = [
|
|
272
|
+
{ vendor: 'datadome', nameRe: /^(datadome|dd_cookie_test_|dd_s)/i },
|
|
273
|
+
{ vendor: 'perimeterx', nameRe: /^_px[a-z0-9]*$|^_pxhd$/i },
|
|
274
|
+
{ vendor: 'cloudflare', nameRe: /^(cf_clearance|__cf_bm|__cflb|_cfuvid)$/i },
|
|
275
|
+
{ vendor: 'akamai', nameRe: /^(_abck|bm_sz|ak_bmsc|bm_sv|bm_mi|bm_so)$/i },
|
|
276
|
+
{ vendor: 'imperva', nameRe: /^(visid_incap_|incap_ses_)/i },
|
|
277
|
+
];
|
|
278
|
+
/**
|
|
279
|
+
* Visit a URL and report whether any major anti-bot vendor served a
|
|
280
|
+
* challenge or block. Recognises Akamai, DataDome, Cloudflare, PerimeterX/HUMAN,
|
|
281
|
+
* Imperva, Kasada and Arkose. Captures vendor signals (cookies, scripts) even
|
|
282
|
+
* on a passing page so you can verify a target is actually protected and
|
|
283
|
+
* BlackTip is sliding past it — not a false negative on an unprotected URL.
|
|
284
|
+
*/
|
|
285
|
+
export async function testAgainstAntiBot(bt, url) {
|
|
286
|
+
const start = Date.now();
|
|
287
|
+
try {
|
|
288
|
+
await bt.navigate(url);
|
|
289
|
+
await new Promise((r) => setTimeout(r, 2500));
|
|
290
|
+
const info = (await bt.executeJS(`(() => ({
|
|
291
|
+
url: location.href,
|
|
292
|
+
title: document.title,
|
|
293
|
+
bodyPreview: (document.body ? document.body.innerText : '').slice(0, 800),
|
|
294
|
+
}))()`));
|
|
295
|
+
const detectedVendors = [];
|
|
296
|
+
for (const { vendor, titleRe, bodyRe } of VENDOR_BLOCK_PATTERNS) {
|
|
297
|
+
if ((titleRe && titleRe.test(info.title)) || (bodyRe && bodyRe.test(info.bodyPreview))) {
|
|
298
|
+
if (!detectedVendors.includes(vendor))
|
|
299
|
+
detectedVendors.push(vendor);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
const scriptSignalsRaw = (await bt.executeJS(VENDOR_SCRIPT_SIGNAL_QUERY));
|
|
303
|
+
const vendorSignals = [];
|
|
304
|
+
try {
|
|
305
|
+
const scriptSignals = JSON.parse(scriptSignalsRaw);
|
|
306
|
+
vendorSignals.push(...scriptSignals);
|
|
307
|
+
}
|
|
308
|
+
catch { /* leave empty */ }
|
|
309
|
+
// Cookie signals — read via the cookies API so we see httpOnly cookies
|
|
310
|
+
// (cf_clearance, __cf_bm, _abck, datadome, etc. are all httpOnly and
|
|
311
|
+
// invisible to document.cookie). The current page's eTLD+1 is what we
|
|
312
|
+
// care about — we don't want to leak signals from prior navigations
|
|
313
|
+
// in the same session.
|
|
314
|
+
try {
|
|
315
|
+
const allCookies = await bt.cookies();
|
|
316
|
+
const currentHost = (() => {
|
|
317
|
+
try {
|
|
318
|
+
return new URL(info.url).hostname;
|
|
319
|
+
}
|
|
320
|
+
catch {
|
|
321
|
+
return null;
|
|
322
|
+
}
|
|
323
|
+
})();
|
|
324
|
+
const seen = new Set();
|
|
325
|
+
for (const c of allCookies) {
|
|
326
|
+
if (currentHost) {
|
|
327
|
+
// Match cookies whose domain is a parent or equal of the current host.
|
|
328
|
+
const cookieDomain = c.domain.replace(/^\./, '');
|
|
329
|
+
if (currentHost !== cookieDomain && !currentHost.endsWith('.' + cookieDomain))
|
|
330
|
+
continue;
|
|
331
|
+
}
|
|
332
|
+
for (const { vendor, nameRe } of VENDOR_COOKIE_PATTERNS) {
|
|
333
|
+
if (nameRe.test(c.name)) {
|
|
334
|
+
const key = vendor + ':cookie';
|
|
335
|
+
if (!seen.has(key)) {
|
|
336
|
+
vendorSignals.push({ vendor, signal: 'cookie' });
|
|
337
|
+
seen.add(key);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
catch { /* cookies API may fail in edge cases — leave script signals */ }
|
|
344
|
+
const refMatch = info.bodyPreview.match(/Reference\s*#([0-9a-f.]+)/);
|
|
345
|
+
const akamaiReference = detectedVendors.includes('akamai') && refMatch ? refMatch[1] ?? null : null;
|
|
346
|
+
const passed = detectedVendors.length === 0;
|
|
347
|
+
const suggestion = passed
|
|
348
|
+
? vendorSignals.length > 0
|
|
349
|
+
? `Page loaded successfully. Vendor signals present (${vendorSignals.map((s) => s.vendor).join(', ')}) — target is protected and BlackTip is passing.`
|
|
350
|
+
: 'Page loaded successfully. No anti-bot vendor signals detected — the target may not be protected.'
|
|
351
|
+
: [
|
|
352
|
+
`Blocked by: ${detectedVendors.join(', ')}.`,
|
|
353
|
+
'Diagnosis steps:',
|
|
354
|
+
'1. `bt.captureFingerprint()` — verify `headers.uaConsistent` is true.',
|
|
355
|
+
'2. `bt.checkIpReputation()` — if `isDatacenter: true`, switch to a residential proxy.',
|
|
356
|
+
'3. Test the same URL in your normal Chrome from this machine. If that also blocks, the IP is flagged.',
|
|
357
|
+
'4. `bt.warmSession({sites: [...]})` before retrying.',
|
|
358
|
+
'5. Set `userDataDir` in BlackTipConfig for a persistent profile.',
|
|
359
|
+
akamaiReference ? `Akamai reference: ${akamaiReference}` : '',
|
|
360
|
+
].filter(Boolean).join('\n');
|
|
361
|
+
return {
|
|
362
|
+
url,
|
|
363
|
+
passed,
|
|
364
|
+
finalUrl: info.url,
|
|
365
|
+
title: info.title,
|
|
366
|
+
detectedVendors,
|
|
367
|
+
vendorSignals,
|
|
368
|
+
akamaiReference,
|
|
369
|
+
bodyPreview: info.bodyPreview.slice(0, 300),
|
|
370
|
+
suggestion,
|
|
371
|
+
durationMs: Date.now() - start,
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
catch (err) {
|
|
375
|
+
return {
|
|
376
|
+
url,
|
|
377
|
+
passed: false,
|
|
378
|
+
finalUrl: url,
|
|
379
|
+
title: '',
|
|
380
|
+
detectedVendors: [],
|
|
381
|
+
vendorSignals: [],
|
|
382
|
+
akamaiReference: null,
|
|
383
|
+
bodyPreview: '',
|
|
384
|
+
suggestion: `Navigation threw: ${err instanceof Error ? err.message : String(err)}`,
|
|
385
|
+
durationMs: Date.now() - start,
|
|
386
|
+
};
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
//# sourceMappingURL=diagnostics.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"diagnostics.js","sourceRoot":"","sources":["../src/diagnostics.ts"],"names":[],"mappings":"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"}
|