webcrawl 1.2.7 → 1.2.8
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.
|
@@ -1,5 +1,27 @@
|
|
|
1
|
-
/**
|
|
2
|
-
|
|
1
|
+
/**
|
|
2
|
+
* AWS WAF challenge handling (local prototype).
|
|
3
|
+
*
|
|
4
|
+
* `webcrawl`'s bot-check detector (`isAntiBotChallenge`) only matches
|
|
5
|
+
* Cloudflare and Vercel Security Checkpoint signatures. AWS WAF serves
|
|
6
|
+
* its invisible JS challenge as **HTTP 202** with an empty `<title>` and
|
|
7
|
+
* none of those markers, so such pages come back as
|
|
8
|
+
* `Empty page (HTTP 202)` with `blocked: false` and never reach the
|
|
9
|
+
* stealth browser. This module adds the missing detection plus a local
|
|
10
|
+
* stealth-routing path so those pages can be solved and read anyway.
|
|
11
|
+
*
|
|
12
|
+
* Detection markers are layered (no single one is proof):
|
|
13
|
+
* - status 202 (documented Challenge interstitial status)
|
|
14
|
+
* - `awsWafCookieDomainList` / `AwsWafIntegration` (documented JS API)
|
|
15
|
+
* - `aws-waf-token` cookie reference
|
|
16
|
+
* - `<div id="challenge-container">` + `/__challenge_<id>/.../challenge.js`
|
|
17
|
+
*
|
|
18
|
+
* The detection itself now lives in the shared detector
|
|
19
|
+
* (`isAwsWafChallenge` in `./fetcher.js`, also folded into
|
|
20
|
+
* `isAntiBotChallenge`); this module re-exports it and adds the local
|
|
21
|
+
* stealth-routing path so challenge pages can be solved and read anyway.
|
|
22
|
+
*/
|
|
23
|
+
import { isAwsWafChallenge } from "./fetcher.js";
|
|
24
|
+
export { isAwsWafChallenge };
|
|
3
25
|
/** True when a fetch response is any known bot check (upstream detector + AWS WAF). */
|
|
4
26
|
export declare function isBlockedLike(html: string, status?: number): boolean;
|
|
5
27
|
export interface AwsWafPage {
|
|
@@ -14,40 +14,15 @@
|
|
|
14
14
|
* - `awsWafCookieDomainList` / `AwsWafIntegration` (documented JS API)
|
|
15
15
|
* - `aws-waf-token` cookie reference
|
|
16
16
|
* - `<div id="challenge-container">` + `/__challenge_<id>/.../challenge.js`
|
|
17
|
+
*
|
|
18
|
+
* The detection itself now lives in the shared detector
|
|
19
|
+
* (`isAwsWafChallenge` in `./fetcher.js`, also folded into
|
|
20
|
+
* `isAntiBotChallenge`); this module re-exports it and adds the local
|
|
21
|
+
* stealth-routing path so challenge pages can be solved and read anyway.
|
|
17
22
|
*/
|
|
18
|
-
import { fetchWithTlsImpersonation,
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
"awswafcookiedomainlist",
|
|
22
|
-
"awswafintegration",
|
|
23
|
-
"aws-waf-token",
|
|
24
|
-
"id=\"challenge-container\"",
|
|
25
|
-
"/__challenge_",
|
|
26
|
-
];
|
|
27
|
-
/** True when an HTML response (with optional status) is an AWS WAF challenge interstitial. */
|
|
28
|
-
export function isAwsWafChallenge(html, status) {
|
|
29
|
-
const lower = String(html ?? "").slice(0, 8000).toLowerCase();
|
|
30
|
-
const hasMarker = AWS_WAF_MARKERS.some((m) => lower.includes(m));
|
|
31
|
-
if (hasMarker)
|
|
32
|
-
return true;
|
|
33
|
-
// 202 alone is the documented Challenge status, but APIs can legitimately
|
|
34
|
-
// return 202 - only treat it as a challenge when the body is empty/challenge-ish.
|
|
35
|
-
if (status === 202) {
|
|
36
|
-
// 202 is the documented Challenge interstitial status. The interstitial's
|
|
37
|
-
// entire payload is <script>/<noscript>, so visible text is empty.
|
|
38
|
-
if (!lower.includes("<html"))
|
|
39
|
-
return false;
|
|
40
|
-
const visibleText = lower
|
|
41
|
-
.slice(0, 8000)
|
|
42
|
-
.replace(/<script[\s\S]*?<\/script>/g, "")
|
|
43
|
-
.replace(/<noscript[\s\S]*?<\/noscript>/g, "")
|
|
44
|
-
.replace(/<style[\s\S]*?<\/style>/g, "")
|
|
45
|
-
.replace(/<[^>]+>/g, " ")
|
|
46
|
-
.trim();
|
|
47
|
-
return visibleText.length === 0;
|
|
48
|
-
}
|
|
49
|
-
return false;
|
|
50
|
-
}
|
|
23
|
+
import { fetchWithTlsImpersonation, isAntiBotChallenge, isAwsWafChallenge } from "./fetcher.js";
|
|
24
|
+
import { fastHtmlToMarkdown } from "./markdown.js";
|
|
25
|
+
export { isAwsWafChallenge };
|
|
51
26
|
/** True when a fetch response is any known bot check (upstream detector + AWS WAF). */
|
|
52
27
|
export function isBlockedLike(html, status) {
|
|
53
28
|
return isAntiBotChallenge(html ?? "", status ?? 200) || isAwsWafChallenge(html ?? "", status);
|
|
@@ -23,6 +23,13 @@ export interface ScrapeResult {
|
|
|
23
23
|
}
|
|
24
24
|
export declare function getBrowserHeaders(targetUrl: string, extraHeaders?: Record<string, string>): Record<string, string>;
|
|
25
25
|
export declare function setDomainCookie(hostname: string, cookieHeader: string): void;
|
|
26
|
+
/**
|
|
27
|
+
* True when an HTML response (with optional status) is an AWS WAF challenge
|
|
28
|
+
* interstitial: a marker match proves it regardless of status; otherwise a
|
|
29
|
+
* documented HTTP 202 whose payload is a `<script>`-only HTML shell with zero
|
|
30
|
+
* visible text (APIs legitimately return 202 too, so status alone is not proof).
|
|
31
|
+
*/
|
|
32
|
+
export declare function isAwsWafChallenge(html: string, status?: number): boolean;
|
|
26
33
|
export declare function isAntiBotChallenge(html: string, status: number): boolean;
|
|
27
34
|
/**
|
|
28
35
|
* Fetches a URL using `impit` (BoringSSL Chrome 136 TLS JA3/JA4 + HTTP/2 fingerprint impersonation)
|
|
@@ -47,18 +47,55 @@ function hashCode(str) {
|
|
|
47
47
|
}
|
|
48
48
|
return hash;
|
|
49
49
|
}
|
|
50
|
+
/** Markers that only appear on an AWS WAF challenge interstitial. */
|
|
51
|
+
const AWS_WAF_CHALLENGE_MARKERS = [
|
|
52
|
+
"awswafcookiedomainlist",
|
|
53
|
+
"awswafintegration",
|
|
54
|
+
"aws-waf-token",
|
|
55
|
+
'id="challenge-container"',
|
|
56
|
+
"/__challenge_",
|
|
57
|
+
];
|
|
58
|
+
/**
|
|
59
|
+
* True when an HTML response (with optional status) is an AWS WAF challenge
|
|
60
|
+
* interstitial: a marker match proves it regardless of status; otherwise a
|
|
61
|
+
* documented HTTP 202 whose payload is a `<script>`-only HTML shell with zero
|
|
62
|
+
* visible text (APIs legitimately return 202 too, so status alone is not proof).
|
|
63
|
+
*/
|
|
64
|
+
export function isAwsWafChallenge(html, status) {
|
|
65
|
+
const lower = String(html ?? "").slice(0, 8000).toLowerCase();
|
|
66
|
+
if (AWS_WAF_CHALLENGE_MARKERS.some((m) => lower.includes(m)))
|
|
67
|
+
return true;
|
|
68
|
+
if (status === 202) {
|
|
69
|
+
if (!lower.includes("<html"))
|
|
70
|
+
return false;
|
|
71
|
+
const visibleText = lower
|
|
72
|
+
.slice(0, 8000)
|
|
73
|
+
.replace(/<script[\s\S]*?<\/script>/g, "")
|
|
74
|
+
.replace(/<noscript[\s\S]*?<\/noscript>/g, "")
|
|
75
|
+
.replace(/<style[\s\S]*?<\/style>/g, "")
|
|
76
|
+
.replace(/<[^>]+>/g, " ")
|
|
77
|
+
.trim();
|
|
78
|
+
return visibleText.length === 0;
|
|
79
|
+
}
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
50
82
|
export function isAntiBotChallenge(html, status) {
|
|
51
83
|
if (status === 403 || status === 429 || status === 503) {
|
|
52
84
|
return true;
|
|
53
85
|
}
|
|
54
86
|
const lower = html.slice(0, 6000).toLowerCase();
|
|
55
|
-
|
|
87
|
+
if (lower.includes("cf-browser-verification") ||
|
|
56
88
|
lower.includes("challenge-platform") ||
|
|
57
89
|
lower.includes("just a moment...") ||
|
|
58
90
|
lower.includes("vercel security checkpoint") ||
|
|
59
91
|
lower.includes("_vercel/security") ||
|
|
60
92
|
lower.includes("enable javascript and cookies to continue") ||
|
|
61
|
-
lower.includes("_cf_chl_opt"))
|
|
93
|
+
lower.includes("_cf_chl_opt")) {
|
|
94
|
+
return true;
|
|
95
|
+
}
|
|
96
|
+
// AWS WAF serves its invisible JS challenge as HTTP 202 with none of the
|
|
97
|
+
// Cloudflare/Vercel markers, so those pages used to pass as real content.
|
|
98
|
+
return isAwsWafChallenge(html, status);
|
|
62
99
|
}
|
|
63
100
|
let sharedImpit = null;
|
|
64
101
|
/** One request, redirects NOT followed (callers validate each hop via followSafely). */
|
package/dist/core/interact/vm.js
CHANGED
|
@@ -155,7 +155,7 @@ try {
|
|
|
155
155
|
const PER_DOC = 1500000;
|
|
156
156
|
const TOTAL = 10000000;
|
|
157
157
|
const started = Date.now();
|
|
158
|
-
const markers = ["vercel security checkpoint", "just a moment...", "cf-browser-verification", "challenge-platform", "_cf_chl_opt", "enable javascript and cookies to continue", "_vercel/security", "attention required! | cloudflare"];
|
|
158
|
+
const markers = ["vercel security checkpoint", "just a moment...", "cf-browser-verification", "challenge-platform", "_cf_chl_opt", "enable javascript and cookies to continue", "_vercel/security", "attention required! | cloudflare", "awswafcookiedomainlist", "awswafintegration", "aws-waf-token", 'id="challenge-container"', "/__challenge_"];
|
|
159
159
|
const looksBlocked = (s) => { const l = String(s || "").slice(0, 8000).toLowerCase(); return markers.some((m) => l.includes(m)); };
|
|
160
160
|
let ua = "";
|
|
161
161
|
try {
|
package/dist/core/sandbox.js
CHANGED
|
@@ -122,18 +122,22 @@ export async function escalateToSandboxOrBrowser(url, options = {}) {
|
|
|
122
122
|
// ignore cookie extraction failure
|
|
123
123
|
}
|
|
124
124
|
// A challenge page is not content: never return it as a successful escalation.
|
|
125
|
+
// An empty shell (e.g. an AWS WAF 202 interstitial) is not content either:
|
|
126
|
+
// parse first and fall through to the browser tier instead.
|
|
125
127
|
if (html && html.length > 100 && !isAntiBotChallenge(html, 200)) {
|
|
126
128
|
const parsed = fastHtmlToMarkdown(html, url, {
|
|
127
129
|
retainImages: options.retainImages,
|
|
128
130
|
});
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
131
|
+
if (parsed.markdown.trim().length > 0) {
|
|
132
|
+
return {
|
|
133
|
+
html,
|
|
134
|
+
markdown: parsed.markdown,
|
|
135
|
+
title: parsed.title,
|
|
136
|
+
links: parsed.links,
|
|
137
|
+
statusCode: 200,
|
|
138
|
+
tier: "sandbox",
|
|
139
|
+
};
|
|
140
|
+
}
|
|
137
141
|
}
|
|
138
142
|
}
|
|
139
143
|
finally {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "webcrawl",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.8",
|
|
4
4
|
"description": "Webcrawl Anything CLI & Core Engine — crawl, search, and extract structured evidence from any website, SPA, PDF, Office file, YouTube video, GitHub repo, ArXiv paper, or CVE",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -127,7 +127,7 @@
|
|
|
127
127
|
"@ai-sdk/gateway": "^4.0.123",
|
|
128
128
|
"@firecrawl/pdf-inspector": "^1.25.2",
|
|
129
129
|
"@vercel/sandbox": "^1.0.0",
|
|
130
|
-
"@webcrawl/client": "^1.2.
|
|
130
|
+
"@webcrawl/client": "^1.2.8",
|
|
131
131
|
"ai": "^7.0.150",
|
|
132
132
|
"impit": "^0.14.7",
|
|
133
133
|
"mammoth": "^1.13.0",
|