webcrawl 1.2.9 → 1.3.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.
|
@@ -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.
|
|
@@ -109,6 +109,12 @@ export function isAntiBotChallenge(html, status) {
|
|
|
109
109
|
return isAwsWafChallenge(html, status);
|
|
110
110
|
}
|
|
111
111
|
let sharedImpit = null;
|
|
112
|
+
/** Tier that won the most recent rawGet ladder ("impit-tls" → "impersonate" → "fetch"). */
|
|
113
|
+
let lastTier = "impit-tls";
|
|
114
|
+
/** Transport tier that won the last fetch ladder run (for result evidence). */
|
|
115
|
+
export function lastFetchTier() {
|
|
116
|
+
return lastTier;
|
|
117
|
+
}
|
|
112
118
|
/** One request, redirects NOT followed (callers validate each hop via followSafely). */
|
|
113
119
|
async function rawGet(url, reqHeaders, timeoutMs, maxBytes) {
|
|
114
120
|
const read = async (status, headers, getBuf) => {
|
|
@@ -132,21 +138,92 @@ async function rawGet(url, reqHeaders, timeoutMs, maxBytes) {
|
|
|
132
138
|
timeout: timeoutMs,
|
|
133
139
|
redirect: "manual",
|
|
134
140
|
});
|
|
135
|
-
|
|
141
|
+
const hop = await read(res.status, headersToObject(res.headers), () => res.arrayBuffer());
|
|
142
|
+
// AWS-WAF-style blocks (403/429/challenge shells) on the impit tier
|
|
143
|
+
// (Chrome 136) retry once on the curl-impersonate tier (Chrome 150),
|
|
144
|
+
// which passes WAFs that block the older fingerprint.
|
|
145
|
+
if (isAntiBotChallenge(new TextDecoder().decode(hop.body).slice(0, 8000), hop.status)) {
|
|
146
|
+
const impersonateHop = await rawGetImpersonate(url, reqHeaders, timeoutMs, maxBytes);
|
|
147
|
+
if (impersonateHop) {
|
|
148
|
+
lastTier = "impersonate";
|
|
149
|
+
return impersonateHop;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
lastTier = "impit-tls";
|
|
153
|
+
return hop;
|
|
136
154
|
}
|
|
137
155
|
catch (err) {
|
|
138
156
|
if (err instanceof ResourceTooLargeError || err instanceof UnsafeUrlError)
|
|
139
157
|
throw err;
|
|
158
|
+
// impit unavailable/failed: curl-impersonate tier, then plain fetch.
|
|
159
|
+
const impersonateHop = await rawGetImpersonate(url, reqHeaders, timeoutMs, maxBytes);
|
|
160
|
+
if (impersonateHop) {
|
|
161
|
+
lastTier = "impersonate";
|
|
162
|
+
return impersonateHop;
|
|
163
|
+
}
|
|
140
164
|
const res = await fetch(url, {
|
|
141
165
|
headers: reqHeaders,
|
|
142
166
|
redirect: "manual",
|
|
143
167
|
signal: AbortSignal.timeout(timeoutMs),
|
|
144
168
|
});
|
|
169
|
+
lastTier = "fetch";
|
|
145
170
|
return await read(res.status, headersToObject(res.headers), () => res.arrayBuffer());
|
|
146
171
|
}
|
|
147
172
|
}
|
|
148
173
|
/**
|
|
149
|
-
*
|
|
174
|
+
* One GET hop with the curl-impersonate tier (Chrome 150 TLS/HTTP2 fingerprint).
|
|
175
|
+
* Used by `rawGet` when the impit hop is blocked (403/429/challenge) — newer
|
|
176
|
+
* fingerprints pass AWS WAF domains (stubhub, expedia) that block Chrome 136.
|
|
177
|
+
* Falls back to plain `fetch` when the binary is unavailable.
|
|
178
|
+
*/
|
|
179
|
+
async function rawGetImpersonate(url, reqHeaders, timeoutMs, maxBytes) {
|
|
180
|
+
let impersonate;
|
|
181
|
+
try {
|
|
182
|
+
impersonate = await import("@webcrawl/curl-impersonate");
|
|
183
|
+
}
|
|
184
|
+
catch {
|
|
185
|
+
return null;
|
|
186
|
+
}
|
|
187
|
+
// The chrome150 profile's own identity headers MUST survive (profileHeaders
|
|
188
|
+
// merges extras last, so a stale UA would override the profile UA and the
|
|
189
|
+
// UA/TLS mismatch is exactly what DataDome/AWS WAF block). Keep cookies and
|
|
190
|
+
// referer, drop UA + sec-ch/client-hint identity headers.
|
|
191
|
+
const extra = {};
|
|
192
|
+
for (const [key, value] of Object.entries(reqHeaders)) {
|
|
193
|
+
const lower = key.toLowerCase();
|
|
194
|
+
if (lower === "user-agent" ||
|
|
195
|
+
lower.startsWith("sec-ch-ua") ||
|
|
196
|
+
lower.startsWith("sec-fetch-") ||
|
|
197
|
+
lower === "upgrade-insecure-requests" ||
|
|
198
|
+
lower === "priority") {
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
201
|
+
extra[key] = value;
|
|
202
|
+
}
|
|
203
|
+
try {
|
|
204
|
+
const res = await impersonate.impersonateFetch(url, {
|
|
205
|
+
headers: extra,
|
|
206
|
+
timeoutMs,
|
|
207
|
+
maxBytes,
|
|
208
|
+
});
|
|
209
|
+
const headers = res.headers;
|
|
210
|
+
const isRedirect = res.status >= 300 && res.status < 400 && !!headers["location"];
|
|
211
|
+
if (!isRedirect && maxBytes !== undefined)
|
|
212
|
+
checkDeclaredLength(headers, maxBytes);
|
|
213
|
+
return {
|
|
214
|
+
status: res.status,
|
|
215
|
+
headers,
|
|
216
|
+
body: isRedirect ? new ArrayBuffer(0) : res.body.buffer,
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
catch (err) {
|
|
220
|
+
if (err instanceof UnsafeUrlError)
|
|
221
|
+
throw err;
|
|
222
|
+
return null; // tier unavailable or fetch failed — caller falls back
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
226
|
+
* Fetches a URL using `impit` (BoringSSL Chrome 136 TLS JA3/JA4 + HTTP/2 fingerprint impersonation)sonation)
|
|
150
227
|
* so Cloudflare Bot Management and Vercel Security Checkpoint treat requests as real Chrome.
|
|
151
228
|
* Falls back to standard `fetch` if `impit` is unavailable on the current platform.
|
|
152
229
|
* 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.0",
|
|
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.0",
|
|
131
|
+
"@webcrawl/curl-impersonate": "^1.3.0",
|
|
131
132
|
"ai": "^7.0.150",
|
|
132
133
|
"impit": "^0.14.7",
|
|
133
134
|
"mammoth": "^1.13.0",
|