webcrawl 1.2.9 → 1.3.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.
|
@@ -17,7 +17,7 @@ export interface ScrapeResult {
|
|
|
17
17
|
links?: string[];
|
|
18
18
|
statusCode: number;
|
|
19
19
|
contentHash: string;
|
|
20
|
-
tier: "cache" | "static" | "sandbox" | "cloudflare" | "browser";
|
|
20
|
+
tier: "cache" | "static" | "sandbox" | "cloudflare" | "browser" | "impersonate";
|
|
21
21
|
latencyMs: number;
|
|
22
22
|
error?: string;
|
|
23
23
|
}
|
|
@@ -37,8 +37,10 @@ export declare function setDomainUserAgent(hostname: string, userAgent: string):
|
|
|
37
37
|
*/
|
|
38
38
|
export declare function isAwsWafChallenge(html: string, status?: number): boolean;
|
|
39
39
|
export declare function isAntiBotChallenge(html: string, status: number): boolean;
|
|
40
|
+
/** Transport tier that won the last fetch ladder run (for result evidence). */
|
|
41
|
+
export declare function lastFetchTier(): "impit-tls" | "impersonate" | "fetch";
|
|
40
42
|
/**
|
|
41
|
-
* Fetches a URL using `impit` (BoringSSL Chrome 136 TLS JA3/JA4 + HTTP/2 fingerprint impersonation)
|
|
43
|
+
* Fetches a URL using `impit` (BoringSSL Chrome 136 TLS JA3/JA4 + HTTP/2 fingerprint impersonation)sonation)
|
|
42
44
|
* so Cloudflare Bot Management and Vercel Security Checkpoint treat requests as real Chrome.
|
|
43
45
|
* Falls back to standard `fetch` if `impit` is unavailable on the current platform.
|
|
44
46
|
* Every redirect hop is checked against the SSRF guard.
|
|
@@ -104,11 +104,27 @@ export function isAntiBotChallenge(html, status) {
|
|
|
104
104
|
lower.includes("_cf_chl_opt")) {
|
|
105
105
|
return true;
|
|
106
106
|
}
|
|
107
|
+
// IP-reputation interstitials (stubhub, expedia) that pass the TLS gate
|
|
108
|
+
// but gate datacenter IPs behind a "enable JS / are you human" shell.
|
|
109
|
+
// These can arrive cached or served with status 200, so text markers are
|
|
110
|
+
// required on top of the 403/429 status check above.
|
|
111
|
+
if (lower.includes("enable javascript and disable any ad blocker") ||
|
|
112
|
+
lower.includes("show us your human side") ||
|
|
113
|
+
lower.includes("we can't tell if you're a human or a bot") ||
|
|
114
|
+
lower.includes("we can’t tell if you’re a human or a bot")) {
|
|
115
|
+
return true;
|
|
116
|
+
}
|
|
107
117
|
// AWS WAF serves its invisible JS challenge as HTTP 202 with none of the
|
|
108
118
|
// Cloudflare/Vercel markers, so those pages used to pass as real content.
|
|
109
119
|
return isAwsWafChallenge(html, status);
|
|
110
120
|
}
|
|
111
121
|
let sharedImpit = null;
|
|
122
|
+
/** Tier that won the most recent rawGet ladder ("impit-tls" → "impersonate" → "fetch"). */
|
|
123
|
+
let lastTier = "impit-tls";
|
|
124
|
+
/** Transport tier that won the last fetch ladder run (for result evidence). */
|
|
125
|
+
export function lastFetchTier() {
|
|
126
|
+
return lastTier;
|
|
127
|
+
}
|
|
112
128
|
/** One request, redirects NOT followed (callers validate each hop via followSafely). */
|
|
113
129
|
async function rawGet(url, reqHeaders, timeoutMs, maxBytes) {
|
|
114
130
|
const read = async (status, headers, getBuf) => {
|
|
@@ -132,21 +148,92 @@ async function rawGet(url, reqHeaders, timeoutMs, maxBytes) {
|
|
|
132
148
|
timeout: timeoutMs,
|
|
133
149
|
redirect: "manual",
|
|
134
150
|
});
|
|
135
|
-
|
|
151
|
+
const hop = await read(res.status, headersToObject(res.headers), () => res.arrayBuffer());
|
|
152
|
+
// AWS-WAF-style blocks (403/429/challenge shells) on the impit tier
|
|
153
|
+
// (Chrome 136) retry once on the curl-impersonate tier (Chrome 150),
|
|
154
|
+
// which passes WAFs that block the older fingerprint.
|
|
155
|
+
if (isAntiBotChallenge(new TextDecoder().decode(hop.body).slice(0, 8000), hop.status)) {
|
|
156
|
+
const impersonateHop = await rawGetImpersonate(url, reqHeaders, timeoutMs, maxBytes);
|
|
157
|
+
if (impersonateHop) {
|
|
158
|
+
lastTier = "impersonate";
|
|
159
|
+
return impersonateHop;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
lastTier = "impit-tls";
|
|
163
|
+
return hop;
|
|
136
164
|
}
|
|
137
165
|
catch (err) {
|
|
138
166
|
if (err instanceof ResourceTooLargeError || err instanceof UnsafeUrlError)
|
|
139
167
|
throw err;
|
|
168
|
+
// impit unavailable/failed: curl-impersonate tier, then plain fetch.
|
|
169
|
+
const impersonateHop = await rawGetImpersonate(url, reqHeaders, timeoutMs, maxBytes);
|
|
170
|
+
if (impersonateHop) {
|
|
171
|
+
lastTier = "impersonate";
|
|
172
|
+
return impersonateHop;
|
|
173
|
+
}
|
|
140
174
|
const res = await fetch(url, {
|
|
141
175
|
headers: reqHeaders,
|
|
142
176
|
redirect: "manual",
|
|
143
177
|
signal: AbortSignal.timeout(timeoutMs),
|
|
144
178
|
});
|
|
179
|
+
lastTier = "fetch";
|
|
145
180
|
return await read(res.status, headersToObject(res.headers), () => res.arrayBuffer());
|
|
146
181
|
}
|
|
147
182
|
}
|
|
148
183
|
/**
|
|
149
|
-
*
|
|
184
|
+
* One GET hop with the curl-impersonate tier (Chrome 150 TLS/HTTP2 fingerprint).
|
|
185
|
+
* Used by `rawGet` when the impit hop is blocked (403/429/challenge) — newer
|
|
186
|
+
* fingerprints pass AWS WAF domains (stubhub, expedia) that block Chrome 136.
|
|
187
|
+
* Falls back to plain `fetch` when the binary is unavailable.
|
|
188
|
+
*/
|
|
189
|
+
async function rawGetImpersonate(url, reqHeaders, timeoutMs, maxBytes) {
|
|
190
|
+
let impersonate;
|
|
191
|
+
try {
|
|
192
|
+
impersonate = await import("@webcrawl/curl-impersonate");
|
|
193
|
+
}
|
|
194
|
+
catch {
|
|
195
|
+
return null;
|
|
196
|
+
}
|
|
197
|
+
// The chrome150 profile's own identity headers MUST survive (profileHeaders
|
|
198
|
+
// merges extras last, so a stale UA would override the profile UA and the
|
|
199
|
+
// UA/TLS mismatch is exactly what DataDome/AWS WAF block). Keep cookies and
|
|
200
|
+
// referer, drop UA + sec-ch/client-hint identity headers.
|
|
201
|
+
const extra = {};
|
|
202
|
+
for (const [key, value] of Object.entries(reqHeaders)) {
|
|
203
|
+
const lower = key.toLowerCase();
|
|
204
|
+
if (lower === "user-agent" ||
|
|
205
|
+
lower.startsWith("sec-ch-ua") ||
|
|
206
|
+
lower.startsWith("sec-fetch-") ||
|
|
207
|
+
lower === "upgrade-insecure-requests" ||
|
|
208
|
+
lower === "priority") {
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
211
|
+
extra[key] = value;
|
|
212
|
+
}
|
|
213
|
+
try {
|
|
214
|
+
const res = await impersonate.impersonateFetch(url, {
|
|
215
|
+
headers: extra,
|
|
216
|
+
timeoutMs,
|
|
217
|
+
maxBytes,
|
|
218
|
+
});
|
|
219
|
+
const headers = res.headers;
|
|
220
|
+
const isRedirect = res.status >= 300 && res.status < 400 && !!headers["location"];
|
|
221
|
+
if (!isRedirect && maxBytes !== undefined)
|
|
222
|
+
checkDeclaredLength(headers, maxBytes);
|
|
223
|
+
return {
|
|
224
|
+
status: res.status,
|
|
225
|
+
headers,
|
|
226
|
+
body: isRedirect ? new ArrayBuffer(0) : res.body.buffer,
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
catch (err) {
|
|
230
|
+
if (err instanceof UnsafeUrlError)
|
|
231
|
+
throw err;
|
|
232
|
+
return null; // tier unavailable or fetch failed — caller falls back
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* Fetches a URL using `impit` (BoringSSL Chrome 136 TLS JA3/JA4 + HTTP/2 fingerprint impersonation)sonation)
|
|
150
237
|
* so Cloudflare Bot Management and Vercel Security Checkpoint treat requests as real Chrome.
|
|
151
238
|
* Falls back to standard `fetch` if `impit` is unavailable on the current platform.
|
|
152
239
|
* Every redirect hop is checked against the SSRF guard.
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* powers both this local engine (`npx webcrawl`) and the hosted `webcrawl.app` API.
|
|
11
11
|
*/
|
|
12
12
|
import { extractWebsiteData, fastHtmlToMarkdown, fetchWithTlsImpersonation, getCachedPageAsync, mapWebsiteUrls, setCachedPage, sha256, } from "./engine.js";
|
|
13
|
-
import { fetchResource } from "./engine/fetcher.js";
|
|
13
|
+
import { fetchResource, lastFetchTier } from "./engine/fetcher.js";
|
|
14
14
|
import { parseFile, publicParseMeta } from "./parse/index.js";
|
|
15
15
|
import { isBinaryContentType, looksLikeBinaryUrl } from "./parse/fetch.js";
|
|
16
16
|
import { maxDownloadBytes } from "./parse/limits.js";
|
|
@@ -174,7 +174,7 @@ export function createLocalEngineClient() {
|
|
|
174
174
|
},
|
|
175
175
|
evidence: {
|
|
176
176
|
...targetInspection.evidence,
|
|
177
|
-
tier: "impit-tls",
|
|
177
|
+
tier: lastFetchTier() === "impersonate" ? "impersonate" : "impit-tls",
|
|
178
178
|
contentHash,
|
|
179
179
|
latencyMs: Date.now() - start,
|
|
180
180
|
},
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* and returns structured results paired with verifiable Evidence & Provenance.
|
|
7
7
|
*/
|
|
8
8
|
export type TargetCategory = "web" | "documents" | "media" | "commerce" | "travel" | "real-estate" | "local" | "security" | "developer" | "academic";
|
|
9
|
-
export type TargetTier = "cache" | "static" | "impit-tls" | "cloudflare" | "stealth-browser" | "sandbox" | "native-parser" | "ocr" | "api";
|
|
9
|
+
export type TargetTier = "cache" | "static" | "impit-tls" | "impersonate" | "cloudflare" | "stealth-browser" | "sandbox" | "native-parser" | "ocr" | "api";
|
|
10
10
|
export interface TargetEvidence {
|
|
11
11
|
/** Which target plugin handled the inspection */
|
|
12
12
|
targetId: string;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "webcrawl",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.1",
|
|
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,8 @@
|
|
|
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.
|
|
130
|
+
"@webcrawl/client": "^1.3.1",
|
|
131
|
+
"@webcrawl/curl-impersonate": "^1.3.1",
|
|
131
132
|
"ai": "^7.0.150",
|
|
132
133
|
"impit": "^0.14.7",
|
|
133
134
|
"mammoth": "^1.13.0",
|