capskip 1.4.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 +36 -0
- package/README.md +72 -6
- package/docs/API_REFERENCE.md +149 -0
- package/examples/datadome.js +142 -0
- package/package.json +4 -2
- package/src/apiParams.js +203 -0
- package/src/index.js +1 -1
- package/src/solver.js +85 -1
- package/types/index.d.ts +65 -5
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,42 @@ All notable changes to this project are documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [1.5.0] - 2026-11-02
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- **DataDome support** via `dataDome(captchaUrl, url, options)`, sent as
|
|
13
|
+
`method=datadome`, covering both DataDome challenges: the slider
|
|
14
|
+
(`/captcha/`) and the device check (`/interstitial/`). The arguments follow
|
|
15
|
+
2Captcha's `datadome(captcha_url, pageurl, userAgent, proxy)`, with
|
|
16
|
+
`userAgent` and `proxy` as optional options, as on every other method.
|
|
17
|
+
- The result carries the cookie twice: `code` exactly as DataDome set it,
|
|
18
|
+
attributes included, and `cookie`, the `datadome=...` pair alone for a Cookie
|
|
19
|
+
header. `userAgent` is the User-Agent the cookie was earned with, the one sent
|
|
20
|
+
or CapSkip's own browser's.
|
|
21
|
+
- `proxy` in the result: when a task is sent without a proxy and the CapSkip app
|
|
22
|
+
solves it through its own DataDome proxy list, the result names that proxy as
|
|
23
|
+
`host:port`. The cookie only works from that address, so it is reported
|
|
24
|
+
rather than dropped. Polled results now carry a `proxy` field whenever the
|
|
25
|
+
server sends one.
|
|
26
|
+
- Parameter aliases `captchaUrl` / `captchaURL` for `captcha_url`.
|
|
27
|
+
- TypeScript definitions for `DataDomeOptions`, plus the new `SolveResult`
|
|
28
|
+
fields `cookie` and `proxy`.
|
|
29
|
+
- `examples/datadome.js`.
|
|
30
|
+
|
|
31
|
+
### Notes
|
|
32
|
+
|
|
33
|
+
- Refused locally, before a round-trip: a `captcha_url` that is not an https
|
|
34
|
+
challenge address on captcha-delivery.com carrying `initialCid`, `hash` and
|
|
35
|
+
`cid`; a block page that bans the requesting address (`t=bv`); a `pageurl`
|
|
36
|
+
that is not http(s), or is on captcha-delivery.com; and a `userAgent` that is
|
|
37
|
+
not stock Chrome for Windows. CapSkip itself checks that the Chrome major
|
|
38
|
+
version matches its browser and answers `ERROR_WRONG_USERAGENT` when it does
|
|
39
|
+
not. An optional passed as `''` is treated as not sent.
|
|
40
|
+
- An iframe `src` copied HTML-escaped (`&`) is accepted, as the server
|
|
41
|
+
decodes it, and is sent unchanged.
|
|
42
|
+
- DataDome uses `recaptchaTimeout`: the challenge is passed in a real browser.
|
|
43
|
+
|
|
8
44
|
## [1.4.0] - 2026-11-01
|
|
9
45
|
|
|
10
46
|
### Added
|
package/README.md
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
[](LICENSE)
|
|
6
6
|
[](https://github.com/capskip/capskip-node/actions/workflows/ci.yml)
|
|
7
7
|
|
|
8
|
-
**Solve reCAPTCHA v2, reCAPTCHA v3, Cloudflare Turnstile, GeeTest, ALTCHA, Capy Puzzle, CaptchaFox, Friendly Captcha, AWS WAF and image captchas from Node.js.**
|
|
8
|
+
**Solve reCAPTCHA v2, reCAPTCHA v3, Cloudflare Turnstile, GeeTest, ALTCHA, Capy Puzzle, CaptchaFox, Friendly Captcha, AWS WAF, DataDome and image captchas from Node.js.**
|
|
9
9
|
|
|
10
10
|
Official Node.js client for [CapSkip](https://capskip.com), a **local captcha solver** that runs on your own machine. Licensed once, not billed per solve. TypeScript definitions included.
|
|
11
11
|
|
|
@@ -19,7 +19,7 @@ npm install capskip
|
|
|
19
19
|
|
|
20
20
|
CapSkip is a desktop app. It does the solving on your machine and exposes the standard captcha-solver HTTP API — the same `in.php` / `res.php` endpoints every 2captcha-compatible client already speaks — on `127.0.0.1:8080`.
|
|
21
21
|
|
|
22
|
-
This SDK is a thin wrapper over that API, with the method names you would expect: `normal()`, `recaptcha()`, `turnstile()`, `geetest()`, `altcha()`, `capy()`, `captchafox()`, `friendlyCaptcha()`, `amazonWaf()`. Nothing leaves your network, and there is no credit balance to keep an eye on.
|
|
22
|
+
This SDK is a thin wrapper over that API, with the method names you would expect: `normal()`, `recaptcha()`, `turnstile()`, `geetest()`, `altcha()`, `capy()`, `captchafox()`, `friendlyCaptcha()`, `amazonWaf()`, `dataDome()`. Nothing leaves your network, and there is no credit balance to keep an eye on.
|
|
23
23
|
|
|
24
24
|
## Supported captcha types
|
|
25
25
|
|
|
@@ -40,6 +40,7 @@ This SDK is a thin wrapper over that API, with the method names you would expect
|
|
|
40
40
|
| **Friendly Captcha solver** (proof-of-work) | `solver.friendlyCaptcha(sitekey, url, { version })` |
|
|
41
41
|
| **AWS WAF solver** (challenge page) | `solver.amazonWaf(sitekey, url, { iv, context })` |
|
|
42
42
|
| AWS WAF (widget) | `solver.amazonWaf(sitekey, url, { jsapiScript })` |
|
|
43
|
+
| **DataDome solver** (slider and device check) | `solver.dataDome(captchaUrl, url)` |
|
|
43
44
|
|
|
44
45
|
**hCaptcha and FunCaptcha/Arkose are not supported.** hCaptcha is the one people misidentify most often, since it also puts a `data-sitekey` on the widget — check for `class="h-captcha"` or a `js.hcaptcha.com` script before reaching for `recaptcha()`.
|
|
45
46
|
|
|
@@ -137,7 +138,7 @@ const solver = new CapSkip({
|
|
|
137
138
|
port: 8080, // CapSkip port from app settings
|
|
138
139
|
defaultTimeout: 120, // seconds — image captcha polling timeout
|
|
139
140
|
recaptchaTimeout: 300, // seconds — reCAPTCHA / Turnstile / GeeTest / CaptchaFox /
|
|
140
|
-
// Friendly Captcha / AWS WAF polling timeout
|
|
141
|
+
// Friendly Captcha / AWS WAF / DataDome polling timeout
|
|
141
142
|
// (ALTCHA and Capy use defaultTimeout — neither is a browser solve)
|
|
142
143
|
pollingInterval: 5, // max seconds between res.php polls (starts at 0.25s, backs off to this)
|
|
143
144
|
});
|
|
@@ -357,6 +358,68 @@ https URL on an `awswaf.com` host. CapSkip posts the solution to the host the
|
|
|
357
358
|
script names, so anything else is refused before the request is made. See the
|
|
358
359
|
[AWS WAF API docs](https://capskip.com/api-docs/#aws-waf).
|
|
359
360
|
|
|
361
|
+
### DataDome
|
|
362
|
+
|
|
363
|
+
DataDome answers a request it doubts with a block page (status 403, sometimes
|
|
364
|
+
401) instead of the site. CapSkip passes the challenge on that page, a slider
|
|
365
|
+
or a device check, and returns the `datadome` cookie that lets your requests
|
|
366
|
+
through. The arguments follow 2Captcha's `datadome(captcha_url, pageurl,
|
|
367
|
+
userAgent, proxy)`, with `userAgent` and `proxy` as optional options.
|
|
368
|
+
|
|
369
|
+
```js
|
|
370
|
+
const result = await solver.dataDome(
|
|
371
|
+
'https://geo.captcha-delivery.com/captcha/?initialCid=...&hash=...&cid=...&t=fe&referer=...&s=45225&e=...&dm=cd',
|
|
372
|
+
'https://www.example.com/', // the page that returned the block page
|
|
373
|
+
{
|
|
374
|
+
userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 '
|
|
375
|
+
+ '(KHTML, like Gecko) Chrome/156.0.0.0 Safari/537.36',
|
|
376
|
+
proxy: { type: 'HTTP', uri: 'login:password@203.0.113.10:8080' },
|
|
377
|
+
},
|
|
378
|
+
);
|
|
379
|
+
|
|
380
|
+
result.cookie; // "datadome=..." -- send it as the Cookie header
|
|
381
|
+
result.code; // the cookie exactly as DataDome set it: Max-Age, Domain, Path...
|
|
382
|
+
result.userAgent; // the User-Agent the cookie was earned with
|
|
383
|
+
result.proxy; // set only when CapSkip picked the proxy, see below
|
|
384
|
+
```
|
|
385
|
+
|
|
386
|
+
`captchaUrl` is the address of the challenge iframe on
|
|
387
|
+
`geo.captcha-delivery.com`: `/captcha/` for a slider, `/interstitial/` for a
|
|
388
|
+
device check. Take the iframe's `src` when the block page has one; most build it
|
|
389
|
+
with a script instead, so assemble it from the page's `var dd={...}` object as
|
|
390
|
+
the [API docs](https://capskip.com/api-docs/#datadome) show. Each block page is
|
|
391
|
+
a fresh challenge, so submit it soon and fetch the page again for the next task.
|
|
392
|
+
|
|
393
|
+
**The cookie belongs to one IP address and one browser.** DataDome refuses it
|
|
394
|
+
from any other address, or with any other User-Agent, so:
|
|
395
|
+
|
|
396
|
+
- **Send the User-Agent you will use**, or none. It must be Chrome for Windows,
|
|
397
|
+
with the same major version as the browser CapSkip solves with; anything else
|
|
398
|
+
(Firefox, Safari, Edge, mobile, macOS) would earn a cookie the site rejects,
|
|
399
|
+
so it is refused before the request is sent, and a different Chrome major is
|
|
400
|
+
refused by CapSkip with `ERROR_WRONG_USERAGENT`. Send none and CapSkip uses
|
|
401
|
+
its own browser's: copy `result.userAgent` into your requests as it is.
|
|
402
|
+
- **Solve from the address you will use the cookie from.** If your requests go
|
|
403
|
+
out through a proxy, pass that proxy with the task. If they come from the
|
|
404
|
+
machine running CapSkip, pass none.
|
|
405
|
+
- **When CapSkip picks the proxy, it tells you which.** The CapSkip app has a
|
|
406
|
+
DataDome proxy list of its own (Settings, Captcha types, DataDome, Configure
|
|
407
|
+
Proxies). A task sent without a proxy is solved through that list, and the
|
|
408
|
+
result then carries `proxy` as `host:port`: send the cookie through that
|
|
409
|
+
proxy, or it will be refused. A proxy you send always takes priority over
|
|
410
|
+
the list, and then no `proxy` comes back.
|
|
411
|
+
|
|
412
|
+
```js
|
|
413
|
+
const headers = { 'User-Agent': result.userAgent, Cookie: result.cookie };
|
|
414
|
+
// When result.proxy is set, route this request through it (it is named without
|
|
415
|
+
// its login: add your credentials if it has one), e.g. with undici's ProxyAgent.
|
|
416
|
+
const page = await fetch('https://www.example.com/', { headers });
|
|
417
|
+
```
|
|
418
|
+
|
|
419
|
+
A block page whose `dd` object carries `'t':'bv'` means DataDome has banned the
|
|
420
|
+
address that fetched it; no solve from there can pass, so it is refused before
|
|
421
|
+
the request is sent. Change the address and request the page again.
|
|
422
|
+
|
|
360
423
|
### With a proxy (every method except image captcha)
|
|
361
424
|
|
|
362
425
|
```js
|
|
@@ -457,7 +520,10 @@ Captcha add `token`, the same string as `code`. Capy expands its answer into
|
|
|
457
520
|
`captchakey`, `challengekey` and `answer` — it returns an object rather than a
|
|
458
521
|
token, and `code` keeps the raw answer. AWS WAF likewise returns an object,
|
|
459
522
|
expanded into `captcha_voucher` and `existing_token`. ALTCHA adds `token` (the same
|
|
460
|
-
string as `code`) and `number`, the counter that solved it.
|
|
523
|
+
string as `code`) and `number`, the counter that solved it. DataDome adds
|
|
524
|
+
`cookie`, the `datadome=...` pair alone, while `code` keeps the cookie with its
|
|
525
|
+
attributes; it also carries `userAgent`, and `proxy` when CapSkip chose the
|
|
526
|
+
proxy.
|
|
461
527
|
|
|
462
528
|
---
|
|
463
529
|
|
|
@@ -502,7 +568,7 @@ const result: SolveResult = await solver.recaptcha('...', 'https://example.com')
|
|
|
502
568
|
|
|
503
569
|
### How do I solve a captcha in Node.js?
|
|
504
570
|
|
|
505
|
-
Install the CapSkip desktop app, `npm install capskip`, then call the method that matches the widget — `recaptcha()`, `turnstile()`, `geetest()`, `altcha()`, `capy()`, `captchafox()`, `friendlyCaptcha()`, `amazonWaf()` or `normal()`. Each returns a Promise that resolves once CapSkip has an answer, giving you a token, or the recognized text in the case of an image captcha.
|
|
571
|
+
Install the CapSkip desktop app, `npm install capskip`, then call the method that matches the widget — `recaptcha()`, `turnstile()`, `geetest()`, `altcha()`, `capy()`, `captchafox()`, `friendlyCaptcha()`, `amazonWaf()`, `dataDome()` or `normal()`. Each returns a Promise that resolves once CapSkip has an answer, giving you a token (a cookie for DataDome), or the recognized text in the case of an image captcha.
|
|
506
572
|
|
|
507
573
|
### Is this a free captcha solver?
|
|
508
574
|
|
|
@@ -510,7 +576,7 @@ The SDK itself is MIT-licensed and free. Solving needs the CapSkip app, which is
|
|
|
510
576
|
|
|
511
577
|
### Which captchas can it solve?
|
|
512
578
|
|
|
513
|
-
reCAPTCHA v2 (checkbox and invisible), reCAPTCHA v3, reCAPTCHA Enterprise, Cloudflare Turnstile, GeeTest v3, ALTCHA, Capy Puzzle, CaptchaFox, Friendly Captcha, AWS WAF, and image/text captchas. Not hCaptcha, and not FunCaptcha/Arkose.
|
|
579
|
+
reCAPTCHA v2 (checkbox and invisible), reCAPTCHA v3, reCAPTCHA Enterprise, Cloudflare Turnstile, GeeTest v3, ALTCHA, Capy Puzzle, CaptchaFox, Friendly Captcha, AWS WAF, DataDome, and image/text captchas. Not hCaptcha, and not FunCaptcha/Arkose.
|
|
514
580
|
|
|
515
581
|
### Does it work with Puppeteer and Playwright?
|
|
516
582
|
|
package/docs/API_REFERENCE.md
CHANGED
|
@@ -28,6 +28,7 @@ method returns a `Promise`.
|
|
|
28
28
|
| CaptchaFox (widget) | `captchafox()` | `captchafox` |
|
|
29
29
|
| Friendly Captcha (proof-of-work) | `friendlyCaptcha()` | `friendly_captcha` |
|
|
30
30
|
| AWS WAF (challenge page or widget) | `amazonWaf()` | `amazon_waf` |
|
|
31
|
+
| DataDome (slider and device check) | `dataDome()` | `datadome` |
|
|
31
32
|
|
|
32
33
|
**Proxy** is supported for every method except image captcha. For ALTCHA the
|
|
33
34
|
proxy is used only for the `challengeUrl` fetch; for Capy, only for the
|
|
@@ -713,6 +714,138 @@ for, and the puzzle rounds go through AWS on top of that.
|
|
|
713
714
|
|
|
714
715
|
---
|
|
715
716
|
|
|
717
|
+
## 11. DataDome — `dataDome(captchaUrl, url, { ... })`
|
|
718
|
+
|
|
719
|
+
DataDome is the bot protection behind many large shops, news sites and
|
|
720
|
+
marketplaces. When it doubts a visitor, the site answers with a block page
|
|
721
|
+
(status 403, sometimes 401) holding a slider or a short device check. CapSkip
|
|
722
|
+
passes that challenge and returns the `datadome` cookie; requests that carry it
|
|
723
|
+
are let through. Public docs: <https://capskip.com/api-docs/#datadome>.
|
|
724
|
+
|
|
725
|
+
The arguments follow 2Captcha's `datadome(captcha_url, pageurl, userAgent,
|
|
726
|
+
proxy)`, so code written against it ports directly; `userAgent` and `proxy` go
|
|
727
|
+
in the options object and are optional here.
|
|
728
|
+
|
|
729
|
+
### POST `/in.php`
|
|
730
|
+
|
|
731
|
+
| Parameter | Type | Required | Description |
|
|
732
|
+
|---|---|---|---|
|
|
733
|
+
| `key` | string | Yes | CapSkip API key |
|
|
734
|
+
| `method` | string | Yes | `datadome` |
|
|
735
|
+
| `captcha_url` | string | Yes | Address of the challenge iframe on `geo.captcha-delivery.com` |
|
|
736
|
+
| `pageurl` | string | Yes | Full http or https URL of the page that returned the block page |
|
|
737
|
+
| `userAgent` | string | No | The User-Agent you will send with the cookie: Chrome for Windows, same major version as CapSkip's browser. Sent as `useragent` |
|
|
738
|
+
| `json` | int | No | `0` plain text, `1` JSON |
|
|
739
|
+
| `proxy` | string | No | The proxy your later requests go through |
|
|
740
|
+
| `proxytype` | string | No | `HTTP`, `HTTPS`, `SOCKS5` or `SOCKS5H` |
|
|
741
|
+
|
|
742
|
+
The SDK also accepts `captchaUrl` / `captchaURL` for `captcha_url`. An optional
|
|
743
|
+
left `undefined`, `null` or `''` is not sent — the server reads an empty value
|
|
744
|
+
as absent. The `captcha_url` is sent exactly as given.
|
|
745
|
+
|
|
746
|
+
Refused by the SDK before the request is made, as the server would refuse them
|
|
747
|
+
(`ValidationException`):
|
|
748
|
+
|
|
749
|
+
- a missing `captcha_url` or `pageurl`;
|
|
750
|
+
- a `captcha_url` that is not https on captcha-delivery.com (or a subdomain),
|
|
751
|
+
under `/captcha/` or `/interstitial/`, carrying `initialCid`, `hash` and
|
|
752
|
+
`cid` (server: `ERROR_BAD_PARAMETERS`);
|
|
753
|
+
- a `captcha_url` with `t=bv`: the block page bans the address that fetched it,
|
|
754
|
+
so nothing sent from there can pass (server: `ERROR_PROXY_BANNED`);
|
|
755
|
+
- a `pageurl` that is not http(s), or that points at captcha-delivery.com
|
|
756
|
+
(server: `ERROR_PAGEURL`);
|
|
757
|
+
- a `userAgent` that is not stock Chrome for Windows: Firefox, Safari, Edge,
|
|
758
|
+
Opera, macOS, Linux, Android and iOS all fail (server:
|
|
759
|
+
`ERROR_WRONG_USERAGENT`).
|
|
760
|
+
|
|
761
|
+
The Chrome **major version** is checked by CapSkip itself, against the browser
|
|
762
|
+
it solves with, and a mismatch comes back as `ApiException('ERROR_WRONG_USERAGENT')`.
|
|
763
|
+
|
|
764
|
+
### Building the `captchaUrl`
|
|
765
|
+
|
|
766
|
+
If the block page already contains the challenge iframe, take its `src` as it
|
|
767
|
+
is (an HTML-escaped `&` is fine). Most block pages build it with a script
|
|
768
|
+
from a `var dd={...}` object instead:
|
|
769
|
+
|
|
770
|
+
```js
|
|
771
|
+
const dd = JSON.parse(html.match(/var dd=(\{[^}]*\})/)[1].replace(/'/g, '"'));
|
|
772
|
+
const path = dd.rt === 'i' ? 'interstitial' : 'captcha';
|
|
773
|
+
const q = new URLSearchParams({ initialCid: dd.cid, hash: dd.hsh, cid: dd.cookie });
|
|
774
|
+
if (dd.t) q.set('t', dd.t);
|
|
775
|
+
q.set('referer', pageUrl);
|
|
776
|
+
q.set('s', String(dd.s));
|
|
777
|
+
for (const k of ['e', 'b']) {
|
|
778
|
+
if (dd[k]) q.set(k, String(dd[k]));
|
|
779
|
+
}
|
|
780
|
+
q.set('dm', 'cd');
|
|
781
|
+
const captchaUrl = `https://${dd.host || 'geo.captcha-delivery.com'}/${path}/?${q}`;
|
|
782
|
+
```
|
|
783
|
+
|
|
784
|
+
Each block page is a fresh challenge: submit it soon, and request the page again
|
|
785
|
+
for the next task rather than reusing an old `captchaUrl`.
|
|
786
|
+
|
|
787
|
+
### The cookie belongs to one IP and one User-Agent
|
|
788
|
+
|
|
789
|
+
DataDome refuses the cookie from any other address, or with any other
|
|
790
|
+
User-Agent:
|
|
791
|
+
|
|
792
|
+
- **Solve from the address you will use the cookie from.** Pass the proxy your
|
|
793
|
+
requests go through; pass none if they come from the machine running CapSkip.
|
|
794
|
+
- **Send the User-Agent you will use, or none.** With none, CapSkip uses its
|
|
795
|
+
browser's own, and the result names it.
|
|
796
|
+
- **CapSkip's own DataDome proxy list.** The app can hold a list of proxies for
|
|
797
|
+
DataDome (Settings, Captcha types, DataDome, Configure Proxies). A task sent
|
|
798
|
+
without a proxy is solved through that list, and the result then carries
|
|
799
|
+
`proxy`: that proxy as `host:port`, without its login. The cookie only works
|
|
800
|
+
through it. A proxy sent with the task always takes priority, and then no
|
|
801
|
+
`proxy` comes back.
|
|
802
|
+
|
|
803
|
+
### GET `/res.php`
|
|
804
|
+
|
|
805
|
+
The SDK polls with `json=1`, where CapSkip returns the cookie in `request` with
|
|
806
|
+
`userAgent` and, when its list supplied the proxy, `proxy` next to it:
|
|
807
|
+
|
|
808
|
+
```json
|
|
809
|
+
{
|
|
810
|
+
"status": 1,
|
|
811
|
+
"request": "datadome=FAPPpli4...; Max-Age=31536000; Domain=.example.com; Path=/; Secure; SameSite=Lax",
|
|
812
|
+
"userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.0.0 Safari/537.36",
|
|
813
|
+
"proxy": "203.0.113.10:8080"
|
|
814
|
+
}
|
|
815
|
+
```
|
|
816
|
+
|
|
817
|
+
(In plain-text mode the same two values arrive in the `X-DataDome-User-Agent`
|
|
818
|
+
and `X-DataDome-Proxy` response headers.) See [Return value](#return-value).
|
|
819
|
+
|
|
820
|
+
### SDK usage
|
|
821
|
+
|
|
822
|
+
```js
|
|
823
|
+
// 2Captcha's argument order, with userAgent and proxy as options
|
|
824
|
+
const result = await solver.dataDome(captchaUrl, 'https://www.example.com/', {
|
|
825
|
+
userAgent,
|
|
826
|
+
proxy: { type: 'HTTP', uri: 'login:password@203.0.113.10:8080' },
|
|
827
|
+
});
|
|
828
|
+
|
|
829
|
+
// No proxy, no User-Agent: CapSkip's browser, and its proxy list if it has one
|
|
830
|
+
const result = await solver.dataDome(captchaUrl, 'https://www.example.com/');
|
|
831
|
+
|
|
832
|
+
const headers = {
|
|
833
|
+
'User-Agent': result.userAgent,
|
|
834
|
+
Cookie: result.cookie, // "datadome=..."
|
|
835
|
+
};
|
|
836
|
+
if (result.proxy) {
|
|
837
|
+
// send this request through result.proxy
|
|
838
|
+
}
|
|
839
|
+
```
|
|
840
|
+
|
|
841
|
+
DataDome may send a replacement `datadome` cookie on later responses; keep the
|
|
842
|
+
newest one, as a browser would. When the block page comes back anyway, build a
|
|
843
|
+
new `captchaUrl` from it and submit a new task.
|
|
844
|
+
|
|
845
|
+
Uses `recaptchaTimeout`: the challenge is passed in a real browser.
|
|
846
|
+
|
|
847
|
+
---
|
|
848
|
+
|
|
716
849
|
## Return value
|
|
717
850
|
|
|
718
851
|
Every solve method resolves to:
|
|
@@ -777,6 +910,21 @@ answer):
|
|
|
777
910
|
}
|
|
778
911
|
```
|
|
779
912
|
|
|
913
|
+
DataDome returns a cookie. `code` keeps it exactly as DataDome set it, for a
|
|
914
|
+
cookie jar; `cookie` is the `datadome=...` pair alone, for a Cookie header.
|
|
915
|
+
`userAgent` is the User-Agent the cookie was earned with, and `proxy` appears
|
|
916
|
+
only when the task sent no proxy and CapSkip used its own DataDome proxy list:
|
|
917
|
+
|
|
918
|
+
```js
|
|
919
|
+
{
|
|
920
|
+
captchaId: '12345',
|
|
921
|
+
code: 'datadome=FAPPpli4...; Max-Age=31536000; Domain=.example.com; Path=/; Secure; SameSite=Lax',
|
|
922
|
+
cookie: 'datadome=FAPPpli4...',
|
|
923
|
+
userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.0.0 Safari/537.36',
|
|
924
|
+
proxy: '203.0.113.10:8080',
|
|
925
|
+
}
|
|
926
|
+
```
|
|
927
|
+
|
|
780
928
|
GeeTest additionally expands its answer into `challenge`, `validate`, and
|
|
781
929
|
`seccode` (`code` keeps the raw JSON string):
|
|
782
930
|
|
|
@@ -814,6 +962,7 @@ Convenience aliases mapped before sending to CapSkip:
|
|
|
814
962
|
| `challengeScript` | `challenge_script` |
|
|
815
963
|
| `captchaScript` | `captcha_script` |
|
|
816
964
|
| `jsapi_script` | `jsapiScript` |
|
|
965
|
+
| `captchaUrl` / `captchaURL` | `captcha_url` |
|
|
817
966
|
| `proxy` object | `proxy` + `proxytype` strings |
|
|
818
967
|
|
|
819
968
|
```js
|
|
@@ -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, AWS WAF 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": {
|
|
@@ -41,6 +41,8 @@
|
|
|
41
41
|
"geetest-solver",
|
|
42
42
|
"aws-waf",
|
|
43
43
|
"aws-waf-captcha",
|
|
44
|
+
"datadome",
|
|
45
|
+
"datadome-solver",
|
|
44
46
|
"image-captcha",
|
|
45
47
|
"ocr",
|
|
46
48
|
"2captcha-alternative",
|
package/src/apiParams.js
CHANGED
|
@@ -59,6 +59,29 @@ const AMAZON_WAF_SCRIPTS = ['challenge_script', 'captcha_script', 'jsapiScript']
|
|
|
59
59
|
// the three AWS WAF script files.
|
|
60
60
|
const AMAZON_WAF_SCRIPT_PATH = /^(\/[A-Za-z0-9_-]+)+\/(challenge|captcha|jsapi)\.js$/;
|
|
61
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
|
+
|
|
62
85
|
// CapSkip solves the puzzle family only. `avatar` is a different challenge
|
|
63
86
|
// behind a different endpoint; the server refuses it at submit time rather than
|
|
64
87
|
// answering it with a puzzle answer, which would bill for a solve the target
|
|
@@ -100,6 +123,10 @@ const PARAM_ALIASES = {
|
|
|
100
123
|
challengeScript: 'challenge_script',
|
|
101
124
|
captchaScript: 'captcha_script',
|
|
102
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',
|
|
103
130
|
};
|
|
104
131
|
|
|
105
132
|
function has(obj, key) {
|
|
@@ -456,6 +483,171 @@ function validateAmazonWafSubmit(params) {
|
|
|
456
483
|
}
|
|
457
484
|
}
|
|
458
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
|
+
|
|
459
651
|
function validateProxyType(params) {
|
|
460
652
|
const proxytype = params.proxytype;
|
|
461
653
|
if (proxytype === undefined || proxytype === null || proxytype === '') {
|
|
@@ -496,6 +688,9 @@ function prepareSubmitParams(params, captchaType, version = 'v2') {
|
|
|
496
688
|
} else if (captchaType === 'amazon_waf') {
|
|
497
689
|
prepared = normalizeAmazonWafSubmit(prepared);
|
|
498
690
|
validateAmazonWafSubmit(prepared);
|
|
691
|
+
} else if (captchaType === 'datadome') {
|
|
692
|
+
prepared = normalizeDataDomeSubmit(prepared);
|
|
693
|
+
validateDataDomeSubmit(prepared);
|
|
499
694
|
}
|
|
500
695
|
|
|
501
696
|
// Skipped for 'normal', which rejects proxy outright with a clearer message.
|
|
@@ -518,6 +713,9 @@ module.exports = {
|
|
|
518
713
|
FRIENDLY_CAPTCHA_SUBMIT,
|
|
519
714
|
AMAZON_WAF_SUBMIT,
|
|
520
715
|
AMAZON_WAF_SCRIPTS,
|
|
716
|
+
DATADOME_SUBMIT,
|
|
717
|
+
DATADOME_CHALLENGE_DOMAIN,
|
|
718
|
+
DATADOME_CHALLENGE_PATHS,
|
|
521
719
|
CAPY_VERSIONS,
|
|
522
720
|
FRIENDLY_CAPTCHA_VERSIONS,
|
|
523
721
|
PROXY_TYPES,
|
|
@@ -537,6 +735,11 @@ module.exports = {
|
|
|
537
735
|
isAmazonWafScriptUrl,
|
|
538
736
|
normalizeAmazonWafSubmit,
|
|
539
737
|
validateAmazonWafSubmit,
|
|
738
|
+
htmlUnescape,
|
|
739
|
+
parseDataDomeCaptchaUrl,
|
|
740
|
+
isDataDomeUserAgent,
|
|
741
|
+
normalizeDataDomeSubmit,
|
|
742
|
+
validateDataDomeSubmit,
|
|
540
743
|
validateProxyType,
|
|
541
744
|
prepareSubmitParams,
|
|
542
745
|
};
|
package/src/index.js
CHANGED
package/src/solver.js
CHANGED
|
@@ -83,6 +83,12 @@ function applyPollResult(result, polled) {
|
|
|
83
83
|
if (userAgent) {
|
|
84
84
|
result.userAgent = userAgent;
|
|
85
85
|
}
|
|
86
|
+
// DataDome names the proxy a task was solved through when the task sent
|
|
87
|
+
// none and CapSkip took one from its own DataDome proxy list. The cookie
|
|
88
|
+
// only works from that address, so it is reported rather than dropped.
|
|
89
|
+
if (polled.proxy) {
|
|
90
|
+
result.proxy = polled.proxy;
|
|
91
|
+
}
|
|
86
92
|
// ALTCHA's createTask-shaped `solution` object. Carried through so
|
|
87
93
|
// applyAltchaSolution can read the counter the server already worked out,
|
|
88
94
|
// which is the only reliable source for a proof-of-work v2 answer; that
|
|
@@ -277,6 +283,24 @@ function applyAmazonWafSolution(result) {
|
|
|
277
283
|
return expandObjectAnswer(result, AMAZON_WAF_FIELDS);
|
|
278
284
|
}
|
|
279
285
|
|
|
286
|
+
/**
|
|
287
|
+
* Expose the DataDome cookie as `cookie`, ready for a Cookie header.
|
|
288
|
+
*
|
|
289
|
+
* `code` keeps the cookie exactly as DataDome set it, attributes included
|
|
290
|
+
* (`datadome=...; Max-Age=...; Domain=...; Path=/; Secure; SameSite=Lax`),
|
|
291
|
+
* for a cookie jar. `cookie` is the part before the first semicolon, the
|
|
292
|
+
* `datadome=...` pair a request sends back. `userAgent` and, when CapSkip
|
|
293
|
+
* picked the proxy, `proxy` are carried through from the poll as they are.
|
|
294
|
+
* An answer that is not a string is left untouched.
|
|
295
|
+
*/
|
|
296
|
+
function applyDataDomeSolution(result) {
|
|
297
|
+
const { code } = result;
|
|
298
|
+
if (typeof code === 'string' && code) {
|
|
299
|
+
result.cookie = code.split(';', 1)[0].trim();
|
|
300
|
+
}
|
|
301
|
+
return result;
|
|
302
|
+
}
|
|
303
|
+
|
|
280
304
|
/**
|
|
281
305
|
* Expose a single-token answer as `token`, named for the form field it fills.
|
|
282
306
|
*
|
|
@@ -326,7 +350,8 @@ function parseSubmitResponse(response) {
|
|
|
326
350
|
|
|
327
351
|
/**
|
|
328
352
|
* Client for the CapSkip local captcha solver (image, reCAPTCHA, Turnstile,
|
|
329
|
-
* GeeTest v3, ALTCHA, Capy Puzzle, CaptchaFox, Friendly Captcha, AWS WAF
|
|
353
|
+
* GeeTest v3, ALTCHA, Capy Puzzle, CaptchaFox, Friendly Captcha, AWS WAF,
|
|
354
|
+
* DataDome).
|
|
330
355
|
*/
|
|
331
356
|
class CapSkip {
|
|
332
357
|
constructor({
|
|
@@ -620,6 +645,61 @@ class CapSkip {
|
|
|
620
645
|
return applyAmazonWafSolution(result);
|
|
621
646
|
}
|
|
622
647
|
|
|
648
|
+
/**
|
|
649
|
+
* Solve a DataDome challenge and return the `datadome` cookie.
|
|
650
|
+
*
|
|
651
|
+
* `captchaUrl` and `url` are required; everything else goes in `options`:
|
|
652
|
+
*
|
|
653
|
+
* - `captchaUrl` is the address of the challenge iframe on
|
|
654
|
+
* geo.captcha-delivery.com, under /captcha/ (slider) or /interstitial/
|
|
655
|
+
* (device check). Take the iframe's src from the block page, or build it
|
|
656
|
+
* from the page's `var dd` object as the API docs show.
|
|
657
|
+
* - `url` is the page that returned the block page.
|
|
658
|
+
* - `options.userAgent` is the one you will send with the cookie. It must be
|
|
659
|
+
* Chrome for Windows with the same major version as the browser CapSkip
|
|
660
|
+
* solves with; leave it out and CapSkip uses that browser's own.
|
|
661
|
+
* - `options.proxy` is the proxy your later requests go through, as a URI
|
|
662
|
+
* string or `{ type: 'HTTP', uri: 'login:password@IP:PORT' }`.
|
|
663
|
+
*
|
|
664
|
+
* DataDome ties the cookie to the IP address and User-Agent that earned it.
|
|
665
|
+
* The result carries:
|
|
666
|
+
*
|
|
667
|
+
* - `code` -- the cookie exactly as DataDome set it, attributes included;
|
|
668
|
+
* - `cookie` -- just the `datadome=...` pair, for a Cookie header;
|
|
669
|
+
* - `userAgent` -- the User-Agent the cookie was earned with. Send it with
|
|
670
|
+
* every request that carries the cookie;
|
|
671
|
+
* - `proxy` -- present only when you sent no proxy and CapSkip solved
|
|
672
|
+
* through its own DataDome proxy list: that proxy as `host:port`. The
|
|
673
|
+
* cookie works only through it.
|
|
674
|
+
*/
|
|
675
|
+
async dataDome(captchaUrl, url, options = {}) {
|
|
676
|
+
// An unset value is dropped rather than sent as undefined, so
|
|
677
|
+
// `dataDome(undefined, url, { captchaUrl })` reaches the alias without
|
|
678
|
+
// tripping the conflict check; an empty one is dropped during validation,
|
|
679
|
+
// since the server reads '' as "not sent".
|
|
680
|
+
const given = {};
|
|
681
|
+
const add = (key, value) => {
|
|
682
|
+
if (value !== undefined && value !== null) {
|
|
683
|
+
given[key] = value;
|
|
684
|
+
}
|
|
685
|
+
};
|
|
686
|
+
add('captcha_url', captchaUrl);
|
|
687
|
+
add('url', url);
|
|
688
|
+
for (const [key, value] of Object.entries(options)) {
|
|
689
|
+
add(key, value);
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
// A real browser session, slider or device check, so it gets the longer
|
|
693
|
+
// of the two timeouts unless the caller asked for a specific one.
|
|
694
|
+
const result = await this.solve({
|
|
695
|
+
timeout: this.recaptchaTimeout,
|
|
696
|
+
...given,
|
|
697
|
+
method: 'datadome',
|
|
698
|
+
poll_json: 1,
|
|
699
|
+
});
|
|
700
|
+
return applyDataDomeSolution(result);
|
|
701
|
+
}
|
|
702
|
+
|
|
623
703
|
async solve(options = {}) {
|
|
624
704
|
const {
|
|
625
705
|
timeout = 0,
|
|
@@ -723,6 +803,9 @@ class CapSkip {
|
|
|
723
803
|
if (method === 'amazon_waf') {
|
|
724
804
|
return prepareSubmitParams(params, 'amazon_waf');
|
|
725
805
|
}
|
|
806
|
+
if (method === 'datadome') {
|
|
807
|
+
return prepareSubmitParams(params, 'datadome');
|
|
808
|
+
}
|
|
726
809
|
return applyProxy(applyParamAliases(params));
|
|
727
810
|
}
|
|
728
811
|
}
|
|
@@ -738,6 +821,7 @@ module.exports = {
|
|
|
738
821
|
applyAltchaSolution,
|
|
739
822
|
applyCapySolution,
|
|
740
823
|
applyAmazonWafSolution,
|
|
824
|
+
applyDataDomeSolution,
|
|
741
825
|
expandObjectAnswer,
|
|
742
826
|
applyTokenSolution,
|
|
743
827
|
};
|
package/types/index.d.ts
CHANGED
|
@@ -20,7 +20,7 @@ export interface CapSkipOptions {
|
|
|
20
20
|
defaultTimeout?: number;
|
|
21
21
|
/**
|
|
22
22
|
* Seconds to poll reCAPTCHA / Turnstile / GeeTest / CaptchaFox / Friendly
|
|
23
|
-
* Captcha / AWS WAF before timing out. Default `300`. ALTCHA and Capy use
|
|
23
|
+
* Captcha / AWS WAF / DataDome before timing out. Default `300`. ALTCHA and Capy use
|
|
24
24
|
* `defaultTimeout` instead — neither is a browser solve.
|
|
25
25
|
*/
|
|
26
26
|
recaptchaTimeout?: number;
|
|
@@ -37,13 +37,16 @@ export interface SolveResult {
|
|
|
37
37
|
* this is the raw JSON string CapSkip returns — prefer the parsed
|
|
38
38
|
* `challenge` / `validate` / `seccode` fields below. For ALTCHA it is the
|
|
39
39
|
* base64 token, also exposed as `token`. For Capy and AWS WAF it is the raw
|
|
40
|
-
* answer object, expanded into the fields below.
|
|
40
|
+
* answer object, expanded into the fields below. For DataDome it is the
|
|
41
|
+
* `datadome` cookie exactly as DataDome set it, attributes included.
|
|
41
42
|
*/
|
|
42
43
|
code: string | Record<string, unknown>;
|
|
43
44
|
/**
|
|
44
|
-
* Turnstile and
|
|
45
|
-
* For CaptchaFox this is the browser's own, not one you sent, and
|
|
46
|
-
* when the solve captured none rather than guessed.
|
|
45
|
+
* Turnstile, CaptchaFox and DataDome — the User-Agent to use when submitting
|
|
46
|
+
* the token. For CaptchaFox this is the browser's own, not one you sent, and
|
|
47
|
+
* it is absent when the solve captured none rather than guessed. For DataDome
|
|
48
|
+
* it is the User-Agent the cookie was earned with — the one you sent, or
|
|
49
|
+
* CapSkip's own browser's — and every request carrying the cookie must send it.
|
|
47
50
|
*/
|
|
48
51
|
userAgent?: string;
|
|
49
52
|
/** GeeTest only — the `geetest_challenge` value to post back. */
|
|
@@ -82,6 +85,17 @@ export interface SolveResult {
|
|
|
82
85
|
captcha_voucher?: string;
|
|
83
86
|
/** AWS WAF only — the token the voucher was issued for. Pair with `captcha_voucher`. */
|
|
84
87
|
existing_token?: string;
|
|
88
|
+
/**
|
|
89
|
+
* DataDome only — the `datadome=...` pair alone (`code` up to its first
|
|
90
|
+
* `;`), ready for a `Cookie` header.
|
|
91
|
+
*/
|
|
92
|
+
cookie?: string;
|
|
93
|
+
/**
|
|
94
|
+
* DataDome only — present only when the task sent no proxy and CapSkip solved
|
|
95
|
+
* through its own DataDome proxy list: that proxy as `host:port`. The cookie
|
|
96
|
+
* works only through it.
|
|
97
|
+
*/
|
|
98
|
+
proxy?: string;
|
|
85
99
|
}
|
|
86
100
|
|
|
87
101
|
/** Extra options for {@link CapSkip.normal}. */
|
|
@@ -280,6 +294,36 @@ export interface AmazonWafOptions {
|
|
|
280
294
|
[key: string]: unknown;
|
|
281
295
|
}
|
|
282
296
|
|
|
297
|
+
/** Extra options for {@link CapSkip.dataDome}. */
|
|
298
|
+
export interface DataDomeOptions {
|
|
299
|
+
/**
|
|
300
|
+
* The User-Agent you will send with the cookie. Must be stock Chrome for
|
|
301
|
+
* Windows with the same major version as the browser CapSkip solves with.
|
|
302
|
+
* Leave it out and CapSkip uses its own browser's, reported as `userAgent`.
|
|
303
|
+
*/
|
|
304
|
+
userAgent?: string;
|
|
305
|
+
/** Alias for `userAgent`. */
|
|
306
|
+
useragent?: string;
|
|
307
|
+
/** The challenge iframe URL (alias for the `captchaUrl` argument). */
|
|
308
|
+
captcha_url?: string;
|
|
309
|
+
/** The challenge iframe URL (alias for the `captchaUrl` argument). */
|
|
310
|
+
captchaUrl?: string;
|
|
311
|
+
/**
|
|
312
|
+
* The proxy your later requests go through. Left out, CapSkip solves through
|
|
313
|
+
* its own DataDome proxy list and the result names that proxy as `proxy`.
|
|
314
|
+
*/
|
|
315
|
+
proxy?: Proxy | string;
|
|
316
|
+
/** Proxy type when `proxy` is a bare string. Default `HTTP`. */
|
|
317
|
+
proxytype?: string;
|
|
318
|
+
/** Poll timeout in seconds. Default: the client's `recaptchaTimeout`. */
|
|
319
|
+
timeout?: number;
|
|
320
|
+
/** Max seconds between polls for this solve. */
|
|
321
|
+
polling_interval?: number;
|
|
322
|
+
/** `1` to request the raw JSON response from CapSkip. */
|
|
323
|
+
json?: number;
|
|
324
|
+
[key: string]: unknown;
|
|
325
|
+
}
|
|
326
|
+
|
|
283
327
|
/** Options for the {@link CapSkip.solve} manual workflow. */
|
|
284
328
|
export interface SolveOptions {
|
|
285
329
|
/** Poll timeout in seconds (falls back to the configured default). */
|
|
@@ -376,6 +420,22 @@ export class CapSkip {
|
|
|
376
420
|
url: string,
|
|
377
421
|
options?: AmazonWafOptions,
|
|
378
422
|
): Promise<SolveResult>;
|
|
423
|
+
/**
|
|
424
|
+
* Solve a DataDome challenge (slider or device check) and return the
|
|
425
|
+
* `datadome` cookie.
|
|
426
|
+
*
|
|
427
|
+
* `captchaUrl` is the challenge iframe's address on captcha-delivery.com
|
|
428
|
+
* (`/captcha/` or `/interstitial/`), `url` the page that returned the block
|
|
429
|
+
* page. The result carries `code` (the cookie as set), `cookie` (the
|
|
430
|
+
* `datadome=...` pair), `userAgent` (send it with the cookie) and, only when
|
|
431
|
+
* no proxy was passed and CapSkip used its own DataDome proxy list, `proxy`
|
|
432
|
+
* — the cookie works only through that address.
|
|
433
|
+
*/
|
|
434
|
+
dataDome(
|
|
435
|
+
captchaUrl: string | undefined,
|
|
436
|
+
url: string,
|
|
437
|
+
options?: DataDomeOptions,
|
|
438
|
+
): Promise<SolveResult>;
|
|
379
439
|
/** Submit then poll to completion. Used by the higher-level solve methods. */
|
|
380
440
|
solve(options?: SolveOptions): Promise<SolveResult>;
|
|
381
441
|
/** Submit a captcha without polling; resolves to the captcha id. */
|