capskip 1.3.0 → 1.5.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 +77 -0
- package/README.md +118 -7
- package/docs/API_REFERENCE.md +267 -1
- package/examples/amazon_waf.js +97 -0
- package/examples/datadome.js +142 -0
- package/package.json +6 -2
- package/src/apiParams.js +314 -0
- package/src/index.js +1 -1
- package/src/solver.js +160 -4
- package/types/index.d.ts +122 -6
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Pass a DataDome challenge and fetch the page with the cookie.
|
|
5
|
+
*
|
|
6
|
+
* DataDome answers a request it doubts with a block page (status 403, sometimes
|
|
7
|
+
* 401) instead of the site. That page carries a `var dd={...}` object and loads
|
|
8
|
+
* the challenge in an iframe from geo.captcha-delivery.com: a slider under
|
|
9
|
+
* /captcha/, or a few-second device check under /interstitial/. CapSkip passes
|
|
10
|
+
* either and returns the `datadome` cookie.
|
|
11
|
+
*
|
|
12
|
+
* The cookie belongs to one IP address and one User-Agent, so:
|
|
13
|
+
*
|
|
14
|
+
* * solve through the proxy your requests use (or none, if they leave from
|
|
15
|
+
* the machine running CapSkip), and
|
|
16
|
+
* * send the User-Agent CapSkip reports back with every request.
|
|
17
|
+
*
|
|
18
|
+
* When you send no proxy and the CapSkip app has its own DataDome proxy list,
|
|
19
|
+
* it solves through one of those and the result names it as `proxy`
|
|
20
|
+
* (host:port): the cookie then only works through that proxy.
|
|
21
|
+
*
|
|
22
|
+
* Requests go out with Node's built-in fetch. Sending them through a proxy
|
|
23
|
+
* needs undici's ProxyAgent (`npm install undici`); without it, the example
|
|
24
|
+
* prints what to do instead of re-fetching through the proxy.
|
|
25
|
+
*
|
|
26
|
+
* Environment:
|
|
27
|
+
* TARGET_URL a page behind DataDome (default: https://www.example.com/)
|
|
28
|
+
* DATADOME_PROXY optional, e.g. http://login:password@203.0.113.10:8080
|
|
29
|
+
* CAPSKIP_API_KEY / CAPSKIP_HOST / CAPSKIP_PORT
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
const { CapSkip } = require('capskip');
|
|
33
|
+
|
|
34
|
+
const solver = new CapSkip({
|
|
35
|
+
apiKey: process.env.CAPSKIP_API_KEY || 'capskip',
|
|
36
|
+
host: process.env.CAPSKIP_HOST || '127.0.0.1',
|
|
37
|
+
port: Number(process.env.CAPSKIP_PORT || 8080),
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
const PAGE_URL = process.env.TARGET_URL || 'https://www.example.com/';
|
|
41
|
+
const PROXY = process.env.DATADOME_PROXY; // scheme://[login:password@]host:port, or unset
|
|
42
|
+
|
|
43
|
+
let ProxyAgent;
|
|
44
|
+
try {
|
|
45
|
+
({ ProxyAgent } = require('undici'));
|
|
46
|
+
} catch (error) {
|
|
47
|
+
ProxyAgent = undefined;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** The challenge iframe address, as the block page's own script builds it. */
|
|
51
|
+
function captchaUrlFromBlockPage(html, pageUrl) {
|
|
52
|
+
const iframe = html.match(/<iframe[^>]+src="(https:\/\/[^"]*captcha-delivery\.com\/[^"]+)"/);
|
|
53
|
+
if (iframe) {
|
|
54
|
+
return iframe[1]; // HTML-escaped & is fine, CapSkip decodes it
|
|
55
|
+
}
|
|
56
|
+
const match = html.match(/var dd=(\{[^}]*\})/);
|
|
57
|
+
if (!match) {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
const dd = JSON.parse(match[1].replace(/'/g, '"'));
|
|
61
|
+
const path = dd.rt === 'i' ? 'interstitial' : 'captcha';
|
|
62
|
+
const query = new URLSearchParams({ initialCid: dd.cid, hash: dd.hsh, cid: dd.cookie });
|
|
63
|
+
if (dd.t) {
|
|
64
|
+
query.set('t', dd.t);
|
|
65
|
+
}
|
|
66
|
+
query.set('referer', pageUrl);
|
|
67
|
+
query.set('s', String(dd.s));
|
|
68
|
+
for (const key of ['e', 'b']) {
|
|
69
|
+
if (dd[key]) {
|
|
70
|
+
query.set(key, String(dd[key]));
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
query.set('dm', 'cd');
|
|
74
|
+
return `https://${dd.host || 'geo.captcha-delivery.com'}/${path}/?${query}`;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** fetch, through `proxyUrl` when one is given and undici is installed. */
|
|
78
|
+
function get(url, { proxyUrl, headers } = {}) {
|
|
79
|
+
const options = { headers, redirect: 'follow' };
|
|
80
|
+
if (proxyUrl) {
|
|
81
|
+
options.dispatcher = new ProxyAgent(proxyUrl);
|
|
82
|
+
}
|
|
83
|
+
return fetch(url, options);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async function main() {
|
|
87
|
+
if (PROXY && !ProxyAgent) {
|
|
88
|
+
console.log('DATADOME_PROXY is set: run `npm install undici` so the requests use it.');
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// 1. Request the page. A 403/401 with a dd object is DataDome's block page.
|
|
93
|
+
const response = await get(PAGE_URL, { proxyUrl: PROXY });
|
|
94
|
+
const html = await response.text();
|
|
95
|
+
const captchaUrl = captchaUrlFromBlockPage(html, PAGE_URL);
|
|
96
|
+
if (![401, 403].includes(response.status) || !captchaUrl) {
|
|
97
|
+
console.log(`No DataDome block page at ${PAGE_URL} (HTTP ${response.status}).`);
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
console.log('Block page:', `${captchaUrl.slice(0, 90)}...`);
|
|
101
|
+
|
|
102
|
+
// 2. Solve it. 2Captcha's argument order: datadome(captcha_url, pageurl,
|
|
103
|
+
// userAgent, proxy), with userAgent and proxy as options. No userAgent:
|
|
104
|
+
// CapSkip uses its own browser's and reports it. With one, it must be
|
|
105
|
+
// Chrome for Windows with the same major version as that browser.
|
|
106
|
+
const options = {};
|
|
107
|
+
if (PROXY) {
|
|
108
|
+
const scheme = PROXY.includes('://') ? PROXY.split('://', 1)[0] : 'http';
|
|
109
|
+
options.proxy = { type: scheme.toUpperCase(), uri: PROXY.split('://').pop() };
|
|
110
|
+
}
|
|
111
|
+
const result = await solver.dataDome(captchaUrl, PAGE_URL, options);
|
|
112
|
+
|
|
113
|
+
console.log('Captcha ID:', result.captchaId);
|
|
114
|
+
console.log('Cookie: ', `${result.cookie.slice(0, 60)}...`);
|
|
115
|
+
console.log('User-Agent:', result.userAgent);
|
|
116
|
+
if (result.proxy) {
|
|
117
|
+
console.log('Proxy: ', result.proxy, '(from the CapSkip app DataDome list; the cookie only works through it)');
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// 3. Repeat the request with the cookie, the same User-Agent and the same IP.
|
|
121
|
+
let proxyUrl = PROXY;
|
|
122
|
+
if (result.proxy && !PROXY) {
|
|
123
|
+
// Named without its login: add yours if that list entry has one.
|
|
124
|
+
if (!ProxyAgent) {
|
|
125
|
+
console.log(`Send the cookie through http://${result.proxy}: run \`npm install undici\` to do it here.`);
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
proxyUrl = `http://${result.proxy}`;
|
|
129
|
+
}
|
|
130
|
+
// `cookie` is the datadome=... pair, ready for the Cookie header; `code`
|
|
131
|
+
// keeps the attributes (Domain, Max-Age...) for a cookie jar.
|
|
132
|
+
const again = await get(PAGE_URL, {
|
|
133
|
+
proxyUrl,
|
|
134
|
+
headers: { 'User-Agent': result.userAgent, Cookie: result.cookie },
|
|
135
|
+
});
|
|
136
|
+
console.log('With the cookie: HTTP', again.status);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
main().catch((error) => {
|
|
140
|
+
console.error(`${error.name}: ${error.message}`);
|
|
141
|
+
process.exitCode = 1;
|
|
142
|
+
});
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "capskip",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "Captcha solver for Node.js: solve reCAPTCHA v2/v3, Cloudflare Turnstile, GeeTest and image captchas with CapSkip, a local unlimited captcha solver with no per-solve fees.",
|
|
3
|
+
"version": "1.5.0",
|
|
4
|
+
"description": "Captcha solver for Node.js: solve reCAPTCHA v2/v3, Cloudflare Turnstile, GeeTest, AWS WAF, DataDome and image captchas with CapSkip, a local unlimited captcha solver with no per-solve fees.",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"types": "types/index.d.ts",
|
|
7
7
|
"exports": {
|
|
@@ -39,6 +39,10 @@
|
|
|
39
39
|
"cloudflare-turnstile",
|
|
40
40
|
"geetest",
|
|
41
41
|
"geetest-solver",
|
|
42
|
+
"aws-waf",
|
|
43
|
+
"aws-waf-captcha",
|
|
44
|
+
"datadome",
|
|
45
|
+
"datadome-solver",
|
|
42
46
|
"image-captcha",
|
|
43
47
|
"ocr",
|
|
44
48
|
"2captcha-alternative",
|
package/src/apiParams.js
CHANGED
|
@@ -46,6 +46,42 @@ const FRIENDLY_CAPTCHA_SUBMIT = new Set([
|
|
|
46
46
|
'api_server', 'useragent', 'json', 'proxy', 'proxytype',
|
|
47
47
|
]);
|
|
48
48
|
|
|
49
|
+
const AMAZON_WAF_SUBMIT = new Set([
|
|
50
|
+
'method', 'sitekey', 'pageurl', 'iv', 'context', 'challenge_script',
|
|
51
|
+
'captcha_script', 'jsapiScript', 'json', 'proxy', 'proxytype',
|
|
52
|
+
]);
|
|
53
|
+
|
|
54
|
+
// The script parameters of an AWS WAF task. CapSkip POSTs the solution to the
|
|
55
|
+
// host these name, so it refuses any that is not a genuine AWS WAF one.
|
|
56
|
+
const AMAZON_WAF_SCRIPTS = ['challenge_script', 'captcha_script', 'jsapiScript'];
|
|
57
|
+
|
|
58
|
+
// The server's own test: one or more [A-Za-z0-9_-] path segments, then one of
|
|
59
|
+
// the three AWS WAF script files.
|
|
60
|
+
const AMAZON_WAF_SCRIPT_PATH = /^(\/[A-Za-z0-9_-]+)+\/(challenge|captcha|jsapi)\.js$/;
|
|
61
|
+
|
|
62
|
+
const DATADOME_SUBMIT = new Set([
|
|
63
|
+
'method', 'captcha_url', 'pageurl', 'useragent', 'json', 'proxy', 'proxytype',
|
|
64
|
+
]);
|
|
65
|
+
|
|
66
|
+
// Every DataDome challenge is served from this domain or a subdomain of it
|
|
67
|
+
// (geo.captcha-delivery.com in practice). CapSkip refuses any other host.
|
|
68
|
+
const DATADOME_CHALLENGE_DOMAIN = 'captcha-delivery.com';
|
|
69
|
+
|
|
70
|
+
// The two challenge pages: /captcha/ draws the slider, /interstitial/ runs the
|
|
71
|
+
// device check. Any other path renders nothing to solve.
|
|
72
|
+
const DATADOME_CHALLENGE_PATHS = ['/captcha', '/interstitial'];
|
|
73
|
+
|
|
74
|
+
// A Chrome version as Chrome writes it in its User-Agent: four dotted numbers.
|
|
75
|
+
const CHROME_VERSION = /\bChrome\/(\d+)\.\d+\.\d+\.\d+\b/;
|
|
76
|
+
|
|
77
|
+
// Chrome-derived browsers and non-desktop builds. A User-Agent carrying any of
|
|
78
|
+
// these is not stock Windows Chrome, so a cookie earned under it would be
|
|
79
|
+
// refused on replay; the server answers ERROR_WRONG_USERAGENT for each.
|
|
80
|
+
const NOT_STOCK_WINDOWS_CHROME = [
|
|
81
|
+
'Edg/', 'EdgA/', 'OPR/', 'YaBrowser/', 'SamsungBrowser/', 'Vivaldi/',
|
|
82
|
+
'HeadlessChrome/', 'CriOS/', 'Mobile', 'Android',
|
|
83
|
+
];
|
|
84
|
+
|
|
49
85
|
// CapSkip solves the puzzle family only. `avatar` is a different challenge
|
|
50
86
|
// behind a different endpoint; the server refuses it at submit time rather than
|
|
51
87
|
// answering it with a puzzle answer, which would bill for a solve the target
|
|
@@ -81,6 +117,16 @@ const PARAM_ALIASES = {
|
|
|
81
117
|
moduleScript: 'module_script',
|
|
82
118
|
nomoduleScript: 'nomodule_script',
|
|
83
119
|
noModuleScript: 'nomodule_script',
|
|
120
|
+
// AWS WAF: the API documents challenge_script / captcha_script in snake case
|
|
121
|
+
// and jsapiScript in camel case, as 2Captcha does. Accept the other spelling
|
|
122
|
+
// of each, which is what createTask-style callers arrive with.
|
|
123
|
+
challengeScript: 'challenge_script',
|
|
124
|
+
captchaScript: 'captcha_script',
|
|
125
|
+
jsapi_script: 'jsapiScript',
|
|
126
|
+
// DataDome: 2Captcha and the API document captcha_url; createTask-style
|
|
127
|
+
// callers arrive with captchaUrl.
|
|
128
|
+
captchaUrl: 'captcha_url',
|
|
129
|
+
captchaURL: 'captcha_url',
|
|
84
130
|
};
|
|
85
131
|
|
|
86
132
|
function has(obj, key) {
|
|
@@ -353,6 +399,255 @@ function validateFriendlyCaptchaSubmit(params) {
|
|
|
353
399
|
}
|
|
354
400
|
}
|
|
355
401
|
|
|
402
|
+
/**
|
|
403
|
+
* True for a script URL CapSkip will accept on an AWS WAF task.
|
|
404
|
+
*
|
|
405
|
+
* Mirrors the server: https, a host under .awswaf.com, no credentials or
|
|
406
|
+
* explicit port, and a path ending in challenge.js, captcha.js or jsapi.js.
|
|
407
|
+
* The server only applies the check once the task is queued, so failing here
|
|
408
|
+
* saves a submit that could never solve.
|
|
409
|
+
*/
|
|
410
|
+
function isAmazonWafScriptUrl(url) {
|
|
411
|
+
let parsed;
|
|
412
|
+
try {
|
|
413
|
+
parsed = new URL(String(url).trim());
|
|
414
|
+
} catch (error) {
|
|
415
|
+
return false;
|
|
416
|
+
}
|
|
417
|
+
// WHATWG URL already lowercases the scheme and host, and drops a :443 on an
|
|
418
|
+
// https URL, so `port` is empty for both the default and an explicit 443.
|
|
419
|
+
return parsed.protocol === 'https:'
|
|
420
|
+
&& parsed.hostname.toLowerCase().endsWith('.awswaf.com')
|
|
421
|
+
&& parsed.username === ''
|
|
422
|
+
&& parsed.password === ''
|
|
423
|
+
&& (parsed.port === '' || parsed.port === '443')
|
|
424
|
+
&& AMAZON_WAF_SCRIPT_PATH.test(parsed.pathname);
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
/**
|
|
428
|
+
* Drop unset optionals, and the empty ones too: an AWS WAF optional left as ''
|
|
429
|
+
* means "not sent" to the server, exactly like an absent one, so it is dropped
|
|
430
|
+
* rather than tripping the iv/context pairing.
|
|
431
|
+
*/
|
|
432
|
+
function normalizeAmazonWafSubmit(params) {
|
|
433
|
+
const out = {};
|
|
434
|
+
for (const [key, value] of Object.entries(dropUnset(params))) {
|
|
435
|
+
if (value !== '') {
|
|
436
|
+
out[key] = value;
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
return out;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
function validateAmazonWafSubmit(params) {
|
|
443
|
+
// Without sitekey the server answers ERROR_BAD_PARAMETERS and without
|
|
444
|
+
// pageurl ERROR_PAGEURL. Fail locally so a missing value costs no round-trip.
|
|
445
|
+
for (const key of ['sitekey', 'pageurl']) {
|
|
446
|
+
if (!params[key]) {
|
|
447
|
+
throw new ValidationException(`'${key}' is required for Amazon WAF.`);
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
const pageurl = String(params.pageurl).trim();
|
|
452
|
+
if (!/^https?:\/\//i.test(pageurl)) {
|
|
453
|
+
throw new ValidationException(
|
|
454
|
+
`'pageurl' must be a full http or https URL for Amazon WAF, got '${pageurl}'.`,
|
|
455
|
+
);
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
// iv and context come off the same challenge page and only work as a pair;
|
|
459
|
+
// the server refuses one without the other with ERROR_BAD_PARAMETERS.
|
|
460
|
+
if (Boolean(params.iv) !== Boolean(params.context)) {
|
|
461
|
+
throw new ValidationException(
|
|
462
|
+
"Amazon WAF 'iv' and 'context' go together: send both (a challenge "
|
|
463
|
+
+ 'page) or neither (a widget page, or let CapSkip read them from pageurl).',
|
|
464
|
+
);
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
for (const key of AMAZON_WAF_SCRIPTS) {
|
|
468
|
+
const value = params[key];
|
|
469
|
+
if (value && !isAmazonWafScriptUrl(value)) {
|
|
470
|
+
throw new ValidationException(
|
|
471
|
+
`'${key}' must be the https URL of an AWS WAF script on an awswaf.com `
|
|
472
|
+
+ `host (challenge.js, captcha.js or jsapi.js), got '${value}'. CapSkip `
|
|
473
|
+
+ 'refuses any other script, because it is not an AWS WAF integration.',
|
|
474
|
+
);
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
const unknown = unknownKeys(params, AMAZON_WAF_SUBMIT);
|
|
479
|
+
if (unknown.length > 0) {
|
|
480
|
+
throw new ValidationException(
|
|
481
|
+
`Unsupported parameters for Amazon WAF: ${reprList(unknown)}.`,
|
|
482
|
+
);
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
const HTML_NAMED_ENTITIES = {
|
|
487
|
+
amp: '&', lt: '<', gt: '>', quot: '"', apos: "'", nbsp: ' ',
|
|
488
|
+
};
|
|
489
|
+
|
|
490
|
+
/**
|
|
491
|
+
* Decode HTML character references, as the server does before it parses a
|
|
492
|
+
* DataDome captcha_url: an iframe src copied out of the page source carries
|
|
493
|
+
* & between its query parameters.
|
|
494
|
+
*/
|
|
495
|
+
function htmlUnescape(text) {
|
|
496
|
+
return text.replace(/&(#[0-9]+|#[xX][0-9a-fA-F]+|[A-Za-z]+);/g, (match, ref) => {
|
|
497
|
+
if (ref[0] === '#') {
|
|
498
|
+
const code = ref[1] === 'x' || ref[1] === 'X'
|
|
499
|
+
? parseInt(ref.slice(2), 16)
|
|
500
|
+
: parseInt(ref.slice(1), 10);
|
|
501
|
+
try {
|
|
502
|
+
return String.fromCodePoint(code);
|
|
503
|
+
} catch (error) {
|
|
504
|
+
return match;
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
const named = HTML_NAMED_ENTITIES[ref.toLowerCase()];
|
|
508
|
+
return named === undefined ? match : named;
|
|
509
|
+
});
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
function isDataDomeHost(host) {
|
|
513
|
+
const lowered = String(host || '').toLowerCase();
|
|
514
|
+
return lowered === DATADOME_CHALLENGE_DOMAIN
|
|
515
|
+
|| lowered.endsWith(`.${DATADOME_CHALLENGE_DOMAIN}`);
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
/**
|
|
519
|
+
* The query of a DataDome challenge URL as a URLSearchParams, or null if it is
|
|
520
|
+
* not one.
|
|
521
|
+
*
|
|
522
|
+
* Mirrors the server: an https URL on captcha-delivery.com (or a subdomain),
|
|
523
|
+
* path /captcha/ or /interstitial/, carrying initialCid, hash and cid. An
|
|
524
|
+
* iframe src copied HTML-escaped (&) and a scheme-relative //geo... URL
|
|
525
|
+
* are both read the way the server reads them.
|
|
526
|
+
*/
|
|
527
|
+
function parseDataDomeCaptchaUrl(url) {
|
|
528
|
+
let text = htmlUnescape(String(url === undefined || url === null ? '' : url).trim());
|
|
529
|
+
if (text.startsWith('//')) {
|
|
530
|
+
text = `https:${text}`;
|
|
531
|
+
}
|
|
532
|
+
let parsed;
|
|
533
|
+
try {
|
|
534
|
+
parsed = new URL(text);
|
|
535
|
+
} catch (error) {
|
|
536
|
+
return null;
|
|
537
|
+
}
|
|
538
|
+
if (parsed.protocol !== 'https:' || !isDataDomeHost(parsed.hostname)) {
|
|
539
|
+
return null;
|
|
540
|
+
}
|
|
541
|
+
const pathname = parsed.pathname.replace(/\/+$/, '').toLowerCase();
|
|
542
|
+
if (!DATADOME_CHALLENGE_PATHS.includes(pathname)) {
|
|
543
|
+
return null;
|
|
544
|
+
}
|
|
545
|
+
const query = parsed.searchParams;
|
|
546
|
+
if (!['initialCid', 'hash', 'cid'].every((key) => query.get(key))) {
|
|
547
|
+
return null;
|
|
548
|
+
}
|
|
549
|
+
return query;
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
/**
|
|
553
|
+
* True for a stock Chrome-for-Windows User-Agent, the only kind CapSkip takes.
|
|
554
|
+
*
|
|
555
|
+
* DataDome ties the cookie to the browser that earned it, and CapSkip solves
|
|
556
|
+
* in Chrome on Windows, so a cookie for any other User-Agent would be refused
|
|
557
|
+
* on replay. The server also requires the same Chrome major version as its
|
|
558
|
+
* own browser; that part can only be checked there.
|
|
559
|
+
*/
|
|
560
|
+
function isDataDomeUserAgent(userAgent) {
|
|
561
|
+
const ua = String(userAgent === undefined || userAgent === null ? '' : userAgent);
|
|
562
|
+
return ua.includes('Windows NT ')
|
|
563
|
+
&& CHROME_VERSION.test(ua)
|
|
564
|
+
&& !NOT_STOCK_WINDOWS_CHROME.some((token) => ua.includes(token));
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
/**
|
|
568
|
+
* Drop unset optionals, and the empty ones too: a DataDome optional left as ''
|
|
569
|
+
* means "not sent" to the server, exactly like an absent one.
|
|
570
|
+
*/
|
|
571
|
+
function normalizeDataDomeSubmit(params) {
|
|
572
|
+
const out = {};
|
|
573
|
+
for (const [key, value] of Object.entries(dropUnset(params))) {
|
|
574
|
+
if (value !== '') {
|
|
575
|
+
out[key] = value;
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
return out;
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
/** An absolute http(s) URL with a host that is not DataDome's own. */
|
|
582
|
+
function isDataDomePageUrl(pageurl) {
|
|
583
|
+
let parsed;
|
|
584
|
+
try {
|
|
585
|
+
parsed = new URL(pageurl);
|
|
586
|
+
} catch (error) {
|
|
587
|
+
return false;
|
|
588
|
+
}
|
|
589
|
+
return (parsed.protocol === 'http:' || parsed.protocol === 'https:')
|
|
590
|
+
&& parsed.hostname !== ''
|
|
591
|
+
&& !isDataDomeHost(parsed.hostname);
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
function validateDataDomeSubmit(params) {
|
|
595
|
+
// Without captcha_url the server answers ERROR_BAD_PARAMETERS and without
|
|
596
|
+
// pageurl ERROR_PAGEURL. Fail locally so a missing value costs no round-trip.
|
|
597
|
+
for (const key of ['captcha_url', 'pageurl']) {
|
|
598
|
+
if (!params[key]) {
|
|
599
|
+
throw new ValidationException(`'${key}' is required for DataDome.`);
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
const query = parseDataDomeCaptchaUrl(params.captcha_url);
|
|
604
|
+
if (query === null) {
|
|
605
|
+
throw new ValidationException(
|
|
606
|
+
"'captcha_url' must be the https address of a DataDome challenge on "
|
|
607
|
+
+ 'captcha-delivery.com, under /captcha/ or /interstitial/ and carrying '
|
|
608
|
+
+ `initialCid, hash and cid, got '${params.captcha_url}'. Take it from `
|
|
609
|
+
+ "the block page's iframe src, or build it from the page's `var dd` object.",
|
|
610
|
+
);
|
|
611
|
+
}
|
|
612
|
+
// t=bv is DataDome saying the address that fetched the block page is
|
|
613
|
+
// banned. Nothing sent from it can pass, and the server refuses the task
|
|
614
|
+
// with ERROR_PROXY_BANNED, so say so before the round-trip.
|
|
615
|
+
if (query.get('t') === 'bv') {
|
|
616
|
+
throw new ValidationException(
|
|
617
|
+
'This DataDome block page bans the IP address that requested it '
|
|
618
|
+
+ '(t=bv), so no solve from that address can pass. Change the address '
|
|
619
|
+
+ '(proxy), request the page again and submit the new captcha_url.',
|
|
620
|
+
);
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
const pageurl = String(params.pageurl).trim();
|
|
624
|
+
if (!isDataDomePageUrl(pageurl)) {
|
|
625
|
+
throw new ValidationException(
|
|
626
|
+
"'pageurl' must be the full http or https URL of the page that "
|
|
627
|
+
+ `returned the DataDome block page, got '${pageurl}'.`,
|
|
628
|
+
);
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
const userAgent = params.useragent;
|
|
632
|
+
if (userAgent !== undefined && userAgent !== null && userAgent !== ''
|
|
633
|
+
&& !isDataDomeUserAgent(userAgent)) {
|
|
634
|
+
throw new ValidationException(
|
|
635
|
+
"DataDome 'userAgent' must be a stock Chrome for Windows User-Agent "
|
|
636
|
+
+ 'with the same major version as the browser CapSkip solves with, got '
|
|
637
|
+
+ `'${userAgent}'. The cookie only works with the User-Agent that `
|
|
638
|
+
+ "earned it. Leave it out and CapSkip uses its own browser's, which "
|
|
639
|
+
+ 'the result reports as `userAgent`.',
|
|
640
|
+
);
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
const unknown = unknownKeys(params, DATADOME_SUBMIT);
|
|
644
|
+
if (unknown.length > 0) {
|
|
645
|
+
throw new ValidationException(
|
|
646
|
+
`Unsupported parameters for DataDome: ${reprList(unknown)}.`,
|
|
647
|
+
);
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
|
|
356
651
|
function validateProxyType(params) {
|
|
357
652
|
const proxytype = params.proxytype;
|
|
358
653
|
if (proxytype === undefined || proxytype === null || proxytype === '') {
|
|
@@ -390,6 +685,12 @@ function prepareSubmitParams(params, captchaType, version = 'v2') {
|
|
|
390
685
|
} else if (captchaType === 'friendly_captcha') {
|
|
391
686
|
prepared = dropUnset(prepared);
|
|
392
687
|
validateFriendlyCaptchaSubmit(prepared);
|
|
688
|
+
} else if (captchaType === 'amazon_waf') {
|
|
689
|
+
prepared = normalizeAmazonWafSubmit(prepared);
|
|
690
|
+
validateAmazonWafSubmit(prepared);
|
|
691
|
+
} else if (captchaType === 'datadome') {
|
|
692
|
+
prepared = normalizeDataDomeSubmit(prepared);
|
|
693
|
+
validateDataDomeSubmit(prepared);
|
|
393
694
|
}
|
|
394
695
|
|
|
395
696
|
// Skipped for 'normal', which rejects proxy outright with a clearer message.
|
|
@@ -410,6 +711,11 @@ module.exports = {
|
|
|
410
711
|
CAPY_SUBMIT,
|
|
411
712
|
CAPTCHAFOX_SUBMIT,
|
|
412
713
|
FRIENDLY_CAPTCHA_SUBMIT,
|
|
714
|
+
AMAZON_WAF_SUBMIT,
|
|
715
|
+
AMAZON_WAF_SCRIPTS,
|
|
716
|
+
DATADOME_SUBMIT,
|
|
717
|
+
DATADOME_CHALLENGE_DOMAIN,
|
|
718
|
+
DATADOME_CHALLENGE_PATHS,
|
|
413
719
|
CAPY_VERSIONS,
|
|
414
720
|
FRIENDLY_CAPTCHA_VERSIONS,
|
|
415
721
|
PROXY_TYPES,
|
|
@@ -426,6 +732,14 @@ module.exports = {
|
|
|
426
732
|
validateCapySubmit,
|
|
427
733
|
validateCaptchaFoxSubmit,
|
|
428
734
|
validateFriendlyCaptchaSubmit,
|
|
735
|
+
isAmazonWafScriptUrl,
|
|
736
|
+
normalizeAmazonWafSubmit,
|
|
737
|
+
validateAmazonWafSubmit,
|
|
738
|
+
htmlUnescape,
|
|
739
|
+
parseDataDomeCaptchaUrl,
|
|
740
|
+
isDataDomeUserAgent,
|
|
741
|
+
normalizeDataDomeSubmit,
|
|
742
|
+
validateDataDomeSubmit,
|
|
429
743
|
validateProxyType,
|
|
430
744
|
prepareSubmitParams,
|
|
431
745
|
};
|
package/src/index.js
CHANGED