capskip 1.0.2 → 1.2.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 +45 -0
- package/README.md +180 -22
- package/docs/API_REFERENCE.md +201 -1
- package/docs/GETTING_STARTED.md +1 -0
- package/docs/TROUBLESHOOTING.md +42 -0
- package/docs/TUTORIAL.md +97 -21
- package/examples/altcha.js +103 -0
- package/examples/geetest.js +68 -0
- package/package.json +24 -2
- package/src/apiParams.js +118 -0
- package/src/index.js +1 -1
- package/src/solver.js +201 -1
- package/types/index.d.ts +80 -3
package/docs/TUTORIAL.md
CHANGED
|
@@ -12,14 +12,15 @@ Work through it top to bottom, or jump to the section you need.
|
|
|
12
12
|
5. [reCAPTCHA v2](#5-recaptcha-v2)
|
|
13
13
|
6. [reCAPTCHA v3](#6-recaptcha-v3)
|
|
14
14
|
7. [Cloudflare Turnstile](#7-cloudflare-turnstile)
|
|
15
|
-
8. [
|
|
16
|
-
9. [
|
|
17
|
-
10. [
|
|
18
|
-
11. [
|
|
19
|
-
12. [
|
|
20
|
-
13. [
|
|
21
|
-
14. [
|
|
22
|
-
15. [
|
|
15
|
+
8. [GeeTest v3](#8-geetest-v3)
|
|
16
|
+
9. [Using a proxy](#9-using-a-proxy)
|
|
17
|
+
10. [Concurrency](#10-concurrency)
|
|
18
|
+
11. [The manual workflow](#11-the-manual-workflow)
|
|
19
|
+
12. [Return values](#12-return-values)
|
|
20
|
+
13. [Error handling](#13-error-handling)
|
|
21
|
+
14. [End-to-end: solve and submit](#14-end-to-end-solve-and-submit)
|
|
22
|
+
15. [Parameter reference](#15-parameter-reference)
|
|
23
|
+
16. [Best practices](#16-best-practices)
|
|
23
24
|
|
|
24
25
|
---
|
|
25
26
|
|
|
@@ -77,7 +78,7 @@ const solver = new CapSkip({
|
|
|
77
78
|
host: '127.0.0.1', // where CapSkip is listening
|
|
78
79
|
port: 8080, // API port from CapSkip settings
|
|
79
80
|
defaultTimeout: 120, // seconds to wait for an image captcha
|
|
80
|
-
recaptchaTimeout: 300, // seconds to wait for reCAPTCHA / Turnstile
|
|
81
|
+
recaptchaTimeout: 300, // seconds to wait for reCAPTCHA / Turnstile / GeeTest
|
|
81
82
|
pollingInterval: 5, // max seconds between result polls (starts at 0.25s, backs off to this)
|
|
82
83
|
});
|
|
83
84
|
```
|
|
@@ -149,7 +150,7 @@ await solver.normal('captcha.png', { json: 1 });
|
|
|
149
150
|
```
|
|
150
151
|
|
|
151
152
|
> **Note:** Proxies are **not** supported for image captcha — passing one throws
|
|
152
|
-
> `ValidationException`. Proxies apply only to reCAPTCHA and
|
|
153
|
+
> `ValidationException`. Proxies apply only to reCAPTCHA, Turnstile, and GeeTest.
|
|
153
154
|
|
|
154
155
|
---
|
|
155
156
|
|
|
@@ -231,24 +232,94 @@ await solver.turnstile('0x4AAAAAAA...', 'https://example.com', {
|
|
|
231
232
|
|
|
232
233
|
---
|
|
233
234
|
|
|
234
|
-
## 8.
|
|
235
|
+
## 8. GeeTest v3
|
|
236
|
+
|
|
237
|
+
`solver.geetest(gt, challenge, url)` solves the GeeTest v3 slide puzzle.
|
|
238
|
+
|
|
239
|
+
### Finding `gt` and `challenge`
|
|
240
|
+
|
|
241
|
+
Unlike a sitekey, GeeTest needs **two** values, and one of them is short-lived:
|
|
242
|
+
|
|
243
|
+
| Value | Lifetime | Where it comes from |
|
|
244
|
+
|---|---|---|
|
|
245
|
+
| `gt` | Static per site | The same place as `challenge` |
|
|
246
|
+
| `challenge` | **Single-use, expires in ~1 minute** | An endpoint the site calls that returns `{"gt": "...", "challenge": "..."}` |
|
|
247
|
+
|
|
248
|
+
Open DevTools → Network on the target page and look for a request to something like
|
|
249
|
+
`.../register.php`, `gettype`, or `get.php`. You can also read the values out of the
|
|
250
|
+
`initGeetest({ gt, challenge })` call in the page scripts.
|
|
251
|
+
|
|
252
|
+
```js
|
|
253
|
+
// Ask the target site for a fresh pair immediately before solving.
|
|
254
|
+
const resp = await fetch('https://example.com/captcha/register.php');
|
|
255
|
+
const { gt, challenge } = await resp.json();
|
|
256
|
+
|
|
257
|
+
const result = await solver.geetest(gt, challenge, 'https://example.com/login');
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
### Using the answer
|
|
261
|
+
|
|
262
|
+
The result carries the three values the site's own front-end would submit:
|
|
263
|
+
|
|
264
|
+
```js
|
|
265
|
+
result.challenge; // geetest_challenge
|
|
266
|
+
result.validate; // geetest_validate
|
|
267
|
+
result.seccode; // geetest_seccode
|
|
268
|
+
|
|
269
|
+
result.code; // the same answer as a raw JSON string
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
Post them back exactly as the site expects:
|
|
273
|
+
|
|
274
|
+
```js
|
|
275
|
+
await fetch('https://example.com/login', {
|
|
276
|
+
method: 'POST',
|
|
277
|
+
body: new URLSearchParams({
|
|
278
|
+
username: '...',
|
|
279
|
+
password: '...',
|
|
280
|
+
geetest_challenge: result.challenge,
|
|
281
|
+
geetest_validate: result.validate,
|
|
282
|
+
geetest_seccode: result.seccode,
|
|
283
|
+
}),
|
|
284
|
+
});
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
> **`challenge` is one-shot.** Never cache or reuse a pair. If a solve fails with a
|
|
288
|
+
> bad-challenge error, request a *new* pair and retry — retrying with the same
|
|
289
|
+
> `challenge` can never succeed.
|
|
290
|
+
|
|
291
|
+
If the site uses a non-default GeeTest API server domain, pass it through:
|
|
292
|
+
|
|
293
|
+
```js
|
|
294
|
+
await solver.geetest(gt, challenge, url, { api_server: 'api-na.geetest.com' });
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
Because GeeTest is a real browser solve (load, slide, verify), it uses the longer
|
|
298
|
+
`recaptchaTimeout` budget rather than `defaultTimeout`.
|
|
299
|
+
|
|
300
|
+
---
|
|
301
|
+
|
|
302
|
+
## 9. Using a proxy
|
|
235
303
|
|
|
236
304
|
Solving through the same IP you will submit from greatly improves acceptance rates
|
|
237
|
-
for reCAPTCHA and
|
|
305
|
+
for reCAPTCHA, Turnstile, and GeeTest. Pass the proxy as an object with `type` and `uri`:
|
|
238
306
|
|
|
239
307
|
```js
|
|
240
308
|
const proxy = { type: 'HTTPS', uri: 'user:pass@1.2.3.4:3128' };
|
|
241
309
|
|
|
242
310
|
await solver.recaptcha('...', 'https://example.com', { proxy });
|
|
243
311
|
await solver.turnstile('...', 'https://example.com', { proxy });
|
|
312
|
+
await solver.geetest('gt', 'challenge', 'https://example.com', { proxy });
|
|
244
313
|
```
|
|
245
314
|
|
|
246
|
-
Supported proxy types: `HTTP`, `HTTPS`, `SOCKS5`, `SOCKS5H
|
|
315
|
+
Supported proxy types: `HTTP`, `HTTPS`, `SOCKS5`, `SOCKS5H` — matched
|
|
316
|
+
case-insensitively. Anything else (including `SOCKS4`) raises
|
|
317
|
+
`ValidationException` before the request is sent. The `uri` may include
|
|
247
318
|
credentials (`login:password@host:port`) or be a bare `host:port`.
|
|
248
319
|
|
|
249
320
|
---
|
|
250
321
|
|
|
251
|
-
##
|
|
322
|
+
## 10. Concurrency
|
|
252
323
|
|
|
253
324
|
Every solve method returns a Promise, so you can solve many captchas concurrently
|
|
254
325
|
with `Promise.all`:
|
|
@@ -279,7 +350,7 @@ const results = await Promise.allSettled([task1, task2]);
|
|
|
279
350
|
|
|
280
351
|
---
|
|
281
352
|
|
|
282
|
-
##
|
|
353
|
+
## 11. The manual workflow
|
|
283
354
|
|
|
284
355
|
If you want to submit now and collect the answer later, use the two low-level
|
|
285
356
|
steps directly.
|
|
@@ -316,7 +387,7 @@ Pass `1` as the second argument to `getResult` to get the full object (including
|
|
|
316
387
|
|
|
317
388
|
---
|
|
318
389
|
|
|
319
|
-
##
|
|
390
|
+
## 12. Return values
|
|
320
391
|
|
|
321
392
|
Every high-level solve method (`normal`, `recaptcha`, `turnstile`, `solve`)
|
|
322
393
|
resolves to an object:
|
|
@@ -334,7 +405,7 @@ the solution string (or an object when called with `json = 1`).
|
|
|
334
405
|
|
|
335
406
|
---
|
|
336
407
|
|
|
337
|
-
##
|
|
408
|
+
## 13. Error handling
|
|
338
409
|
|
|
339
410
|
The SDK throws four error types, all subclasses of `CapSkipError`:
|
|
340
411
|
|
|
@@ -388,7 +459,7 @@ try {
|
|
|
388
459
|
|
|
389
460
|
---
|
|
390
461
|
|
|
391
|
-
##
|
|
462
|
+
## 14. End-to-end: solve and submit
|
|
392
463
|
|
|
393
464
|
A realistic flow — solve a reCAPTCHA, then submit the token to the target site
|
|
394
465
|
through the **same** proxy:
|
|
@@ -453,7 +524,7 @@ await fetch(CHALLENGE_URL, {
|
|
|
453
524
|
|
|
454
525
|
---
|
|
455
526
|
|
|
456
|
-
##
|
|
527
|
+
## 15. Parameter reference
|
|
457
528
|
|
|
458
529
|
### Solve methods
|
|
459
530
|
|
|
@@ -462,6 +533,7 @@ await fetch(CHALLENGE_URL, {
|
|
|
462
533
|
| Image | `normal(file, { json })` |
|
|
463
534
|
| reCAPTCHA | `recaptcha(sitekey, url, { version, enterprise, ... })` |
|
|
464
535
|
| Turnstile | `turnstile(sitekey, url, { ... })` |
|
|
536
|
+
| GeeTest v3 | `geetest(gt, challenge, url, { ... })` |
|
|
465
537
|
| Manual submit | `send(params) -> Promise<id>` |
|
|
466
538
|
| Manual poll | `getResult(id, json)` |
|
|
467
539
|
|
|
@@ -474,6 +546,7 @@ The SDK accepts friendly names and converts them to the raw API parameters:
|
|
|
474
546
|
| `url` | `pageurl` |
|
|
475
547
|
| `score`, `minScore` | `min_score` |
|
|
476
548
|
| `datas`, `data_s` | `data-s` |
|
|
549
|
+
| `apiServer`, `api_subdomain` | `api_server` |
|
|
477
550
|
| `proxy` (object) | `proxy` + `proxytype` strings |
|
|
478
551
|
|
|
479
552
|
Anything CapSkip does not document for a given captcha type is rejected with
|
|
@@ -481,14 +554,17 @@ Anything CapSkip does not document for a given captcha type is rejected with
|
|
|
481
554
|
|
|
482
555
|
---
|
|
483
556
|
|
|
484
|
-
##
|
|
557
|
+
## 16. Best practices
|
|
485
558
|
|
|
486
559
|
- **Keep CapSkip running.** The SDK talks to a local app; if it is not running you
|
|
487
560
|
get `NetworkException`.
|
|
488
561
|
- **Use the token immediately.** reCAPTCHA and Turnstile tokens expire within a
|
|
489
562
|
couple of minutes.
|
|
490
563
|
- **Match sitekey and pageurl exactly** to the page the widget loads on.
|
|
491
|
-
- **
|
|
564
|
+
- **Fetch a fresh GeeTest `challenge` per solve.** It is single-use and expires in
|
|
565
|
+
about a minute; a cached pair always fails.
|
|
566
|
+
- **Solve and submit from the same IP** (same proxy) for reCAPTCHA, Turnstile, and
|
|
567
|
+
GeeTest.
|
|
492
568
|
- **Never commit secrets.** Read `CAPSKIP_API_KEY` and proxy credentials from the
|
|
493
569
|
environment, not source code.
|
|
494
570
|
- **Tune timeouts** for slow captcha types with `recaptchaTimeout` and
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Solve an ALTCHA proof-of-work challenge.
|
|
5
|
+
*
|
|
6
|
+
* ALTCHA is not a recognition captcha -- there is nothing to read. The site
|
|
7
|
+
* issues a challenge and the browser must brute-force a number that satisfies
|
|
8
|
+
* it. CapSkip does that work for you, in milliseconds.
|
|
9
|
+
*
|
|
10
|
+
* You need the challenge, in one of two forms:
|
|
11
|
+
*
|
|
12
|
+
* * `challengeUrl` - the endpoint that serves it; CapSkip fetches it for you
|
|
13
|
+
* * `challengeJson` - the challenge document itself, if you already have it
|
|
14
|
+
*
|
|
15
|
+
* To find them, open DevTools -> Network on the target page and look for the
|
|
16
|
+
* request the `<altcha-widget>` makes for its challenge (often something like
|
|
17
|
+
* `/altcha/challenge`). The request URL is your `challengeUrl`; its JSON
|
|
18
|
+
* response is your `challengeJson`.
|
|
19
|
+
*
|
|
20
|
+
* Note the widget attribute that names the endpoint changed between versions:
|
|
21
|
+
* v1/v2 use `challengeurl="..."`, while v3+ uses `challenge="..."` for both a
|
|
22
|
+
* URL and inline data. Read the page source rather than assuming.
|
|
23
|
+
*
|
|
24
|
+
* Challenges expire fast -- some sites inside two minutes -- so fetch one
|
|
25
|
+
* immediately before solving and post the token promptly. An expired challenge
|
|
26
|
+
* is rejected with a bare "verification failed" that looks exactly like a wrong
|
|
27
|
+
* answer.
|
|
28
|
+
*
|
|
29
|
+
* This example issues its own challenge the way a site's server would, so it
|
|
30
|
+
* runs as-is with no third-party dependency. Swap in your target's endpoint to
|
|
31
|
+
* use it for real.
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
const crypto = require('crypto');
|
|
35
|
+
|
|
36
|
+
const { CapSkip } = require('../src');
|
|
37
|
+
|
|
38
|
+
const solver = new CapSkip({
|
|
39
|
+
apiKey: process.env.CAPSKIP_API_KEY || 'capskip',
|
|
40
|
+
host: process.env.CAPSKIP_HOST || '127.0.0.1',
|
|
41
|
+
port: Number(process.env.CAPSKIP_PORT || 8080),
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
const PAGE_URL = 'https://example.com/signup';
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Mint an ALTCHA challenge, exactly as a site's own server would.
|
|
48
|
+
*
|
|
49
|
+
* Replace this with a fetch of your target's challenge endpoint -- or skip it
|
|
50
|
+
* entirely and pass `challengeUrl` so CapSkip does the fetching.
|
|
51
|
+
*/
|
|
52
|
+
function issueChallenge(number = 54321) {
|
|
53
|
+
const salt = `${crypto.randomBytes(12).toString('hex')}?expires=${
|
|
54
|
+
Math.floor(Date.now() / 1000) + 600}`;
|
|
55
|
+
return {
|
|
56
|
+
algorithm: 'SHA-256',
|
|
57
|
+
challenge: crypto.createHash('sha256').update(`${salt}${number}`).digest('hex'),
|
|
58
|
+
salt,
|
|
59
|
+
signature: '0'.repeat(64),
|
|
60
|
+
maxnumber: 100000,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
(async () => {
|
|
65
|
+
// --- Option A: you already have the challenge document ---------------------
|
|
66
|
+
// No network request at all: CapSkip solves it locally.
|
|
67
|
+
const result = await solver.altcha(PAGE_URL, { challengeJson: issueChallenge() });
|
|
68
|
+
|
|
69
|
+
console.log('Captcha ID:', result.captchaId);
|
|
70
|
+
console.log('Number: ', result.number);
|
|
71
|
+
console.log('Token: ', `${result.token.slice(0, 60)}...`);
|
|
72
|
+
|
|
73
|
+
// --- Option B: let CapSkip fetch the challenge ------------------------------
|
|
74
|
+
// Point it at the endpoint the widget calls. Add `proxy` if the endpoint
|
|
75
|
+
// should be fetched from a particular IP -- the proxy is used only for that
|
|
76
|
+
// fetch, never for the solve itself.
|
|
77
|
+
//
|
|
78
|
+
// const result = await solver.altcha(PAGE_URL, {
|
|
79
|
+
// challengeUrl: 'https://example.com/captcha/api/altcha/challenge',
|
|
80
|
+
// proxy: { type: 'HTTP', uri: 'login:password@1.2.3.4:8080' },
|
|
81
|
+
// });
|
|
82
|
+
|
|
83
|
+
// Post the token back in the form field the widget uses, named `altcha`:
|
|
84
|
+
//
|
|
85
|
+
// await fetch(SIGNUP_URL, {
|
|
86
|
+
// method: 'POST',
|
|
87
|
+
// body: new URLSearchParams({
|
|
88
|
+
// email: 'someone@example.com',
|
|
89
|
+
// altcha: result.token,
|
|
90
|
+
// }),
|
|
91
|
+
// });
|
|
92
|
+
//
|
|
93
|
+
// Do not re-encode, trim or re-order it. The token is base64 of a JSON
|
|
94
|
+
// document whose fields are covered by the server's HMAC signature, so any
|
|
95
|
+
// modification invalidates it.
|
|
96
|
+
|
|
97
|
+
// `code` holds the same string as `token`, which is what you forward if you
|
|
98
|
+
// are porting code written against another solver's API.
|
|
99
|
+
console.assert(result.code === result.token);
|
|
100
|
+
})().catch((err) => {
|
|
101
|
+
console.error(err);
|
|
102
|
+
process.exit(1);
|
|
103
|
+
});
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Solve a GeeTest v3 slider.
|
|
5
|
+
*
|
|
6
|
+
* GeeTest v3 needs two values from the target site:
|
|
7
|
+
*
|
|
8
|
+
* - `gt` static per site
|
|
9
|
+
* - `challenge` single-use, expires in about a minute
|
|
10
|
+
*
|
|
11
|
+
* The site fetches them itself from an endpoint that returns
|
|
12
|
+
* `{"gt": "...", "challenge": "..."}` (often `.../register.php` or a
|
|
13
|
+
* `gettype`/`get.php` request). Open DevTools -> Network to find that request,
|
|
14
|
+
* then request a *fresh* pair right before solving, as this example does.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
const { CapSkip } = require('capskip');
|
|
18
|
+
|
|
19
|
+
const solver = new CapSkip({
|
|
20
|
+
apiKey: process.env.CAPSKIP_API_KEY || 'capskip',
|
|
21
|
+
host: process.env.CAPSKIP_HOST || '127.0.0.1',
|
|
22
|
+
port: Number(process.env.CAPSKIP_PORT || 8080),
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
// A public GeeTest v3 demo page, and the endpoint that page calls to issue a
|
|
26
|
+
// fresh gt/challenge pair. Safe to run as-is.
|
|
27
|
+
const PAGE_URL = 'https://2captcha.com/demo/geetest';
|
|
28
|
+
const REGISTER_URL = 'https://2captcha.com/api/v1/captcha-demo/gee-test/init-params';
|
|
29
|
+
|
|
30
|
+
/** Get a fresh gt/challenge pair. Replace with the endpoint your target uses. */
|
|
31
|
+
async function fetchChallenge() {
|
|
32
|
+
const resp = await fetch(REGISTER_URL);
|
|
33
|
+
if (!resp.ok) {
|
|
34
|
+
throw new Error(`Could not fetch a gt/challenge pair: HTTP ${resp.status}`);
|
|
35
|
+
}
|
|
36
|
+
return resp.json();
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
(async () => {
|
|
40
|
+
const { gt, challenge } = await fetchChallenge();
|
|
41
|
+
|
|
42
|
+
const result = await solver.geetest(gt, challenge, PAGE_URL);
|
|
43
|
+
|
|
44
|
+
console.log('Captcha ID:', result.captchaId);
|
|
45
|
+
console.log('Challenge: ', result.challenge);
|
|
46
|
+
console.log('Validate: ', result.validate);
|
|
47
|
+
console.log('Seccode: ', result.seccode);
|
|
48
|
+
|
|
49
|
+
// `code` holds the same answer as a raw JSON string, which is what you forward
|
|
50
|
+
// if you are porting code written against another solver's API.
|
|
51
|
+
console.log('Raw code: ', result.code);
|
|
52
|
+
|
|
53
|
+
// Post these back exactly as the site's own front-end would, e.g.:
|
|
54
|
+
//
|
|
55
|
+
// await fetch(LOGIN_URL, {
|
|
56
|
+
// method: 'POST',
|
|
57
|
+
// body: new URLSearchParams({
|
|
58
|
+
// geetest_challenge: result.challenge,
|
|
59
|
+
// geetest_validate: result.validate,
|
|
60
|
+
// geetest_seccode: result.seccode,
|
|
61
|
+
// }),
|
|
62
|
+
// });
|
|
63
|
+
console.log('Form fields:', {
|
|
64
|
+
geetest_challenge: result.challenge,
|
|
65
|
+
geetest_validate: result.validate,
|
|
66
|
+
geetest_seccode: result.seccode,
|
|
67
|
+
});
|
|
68
|
+
})();
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "capskip",
|
|
3
|
-
"version": "1.0
|
|
4
|
-
"description": "Node.js
|
|
3
|
+
"version": "1.2.0",
|
|
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.",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"types": "types/index.d.ts",
|
|
7
7
|
"exports": {
|
|
@@ -23,9 +23,31 @@
|
|
|
23
23
|
"keywords": [
|
|
24
24
|
"captcha",
|
|
25
25
|
"captcha-solver",
|
|
26
|
+
"captcha-solving",
|
|
27
|
+
"captcha-bypass",
|
|
28
|
+
"unlimited-captcha-solver",
|
|
29
|
+
"local-captcha-solver",
|
|
30
|
+
"free-captcha-solver",
|
|
26
31
|
"recaptcha",
|
|
32
|
+
"recaptcha-solver",
|
|
33
|
+
"recaptcha-v2",
|
|
34
|
+
"recaptcha-v3",
|
|
35
|
+
"recaptcha-enterprise",
|
|
27
36
|
"cloudflare",
|
|
28
37
|
"turnstile",
|
|
38
|
+
"turnstile-solver",
|
|
39
|
+
"cloudflare-turnstile",
|
|
40
|
+
"geetest",
|
|
41
|
+
"geetest-solver",
|
|
42
|
+
"image-captcha",
|
|
43
|
+
"ocr",
|
|
44
|
+
"2captcha-alternative",
|
|
45
|
+
"anti-captcha-alternative",
|
|
46
|
+
"puppeteer",
|
|
47
|
+
"playwright",
|
|
48
|
+
"selenium",
|
|
49
|
+
"browser-automation",
|
|
50
|
+
"web-scraping",
|
|
29
51
|
"capskip",
|
|
30
52
|
"automation"
|
|
31
53
|
],
|
package/src/apiParams.js
CHANGED
|
@@ -21,12 +21,33 @@ const TURNSTILE_SUBMIT = new Set([
|
|
|
21
21
|
'proxy', 'proxytype',
|
|
22
22
|
]);
|
|
23
23
|
|
|
24
|
+
const GEETEST_SUBMIT = new Set([
|
|
25
|
+
'method', 'gt', 'challenge', 'pageurl', 'api_server', 'json',
|
|
26
|
+
'proxy', 'proxytype',
|
|
27
|
+
]);
|
|
28
|
+
|
|
29
|
+
const ALTCHA_SUBMIT = new Set([
|
|
30
|
+
'method', 'pageurl', 'challenge_url', 'challenge_json', 'json',
|
|
31
|
+
'proxy', 'proxytype',
|
|
32
|
+
]);
|
|
33
|
+
|
|
34
|
+
// The only values CapSkip maps to a proxy scheme; it answers
|
|
35
|
+
// ERROR_BAD_PARAMETERS for anything else, SOCKS4 included. Matched
|
|
36
|
+
// case-insensitively, as the server does.
|
|
37
|
+
const PROXY_TYPES = ['HTTP', 'HTTPS', 'SOCKS5', 'SOCKS5H'];
|
|
38
|
+
|
|
24
39
|
const PARAM_ALIASES = {
|
|
25
40
|
url: 'pageurl',
|
|
26
41
|
score: 'min_score',
|
|
27
42
|
minScore: 'min_score',
|
|
28
43
|
datas: 'data-s',
|
|
29
44
|
data_s: 'data-s',
|
|
45
|
+
apiServer: 'api_server',
|
|
46
|
+
api_subdomain: 'api_server',
|
|
47
|
+
challengeUrl: 'challenge_url',
|
|
48
|
+
challengeURL: 'challenge_url',
|
|
49
|
+
challengeJson: 'challenge_json',
|
|
50
|
+
challengeJSON: 'challenge_json',
|
|
30
51
|
};
|
|
31
52
|
|
|
32
53
|
function has(obj, key) {
|
|
@@ -145,6 +166,86 @@ function validateTurnstileSubmit(params) {
|
|
|
145
166
|
}
|
|
146
167
|
}
|
|
147
168
|
|
|
169
|
+
function validateGeetestSubmit(params) {
|
|
170
|
+
// All three are documented as required. gt is static per site, challenge is
|
|
171
|
+
// single-use and expires in about a minute; without them CapSkip answers
|
|
172
|
+
// ERROR_BAD_PARAMETERS, and without pageurl ERROR_PAGEURL. Fail locally so a
|
|
173
|
+
// missing value does not cost a round-trip.
|
|
174
|
+
for (const key of ['gt', 'challenge', 'pageurl']) {
|
|
175
|
+
if (!params[key]) {
|
|
176
|
+
throw new ValidationException(`'${key}' is required for GeeTest v3.`);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const unknown = unknownKeys(params, GEETEST_SUBMIT);
|
|
181
|
+
if (unknown.length > 0) {
|
|
182
|
+
throw new ValidationException(
|
|
183
|
+
`Unsupported parameters for GeeTest: ${reprList(unknown)}.`,
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Drop unset challenge params and serialize an inline challenge document.
|
|
190
|
+
*
|
|
191
|
+
* `altcha(url, { challengeUrl, challengeJson })` is normally called with one of
|
|
192
|
+
* the two left out, and the form body can only carry a string -- so a document
|
|
193
|
+
* passed as an object is serialized rather than stringified into
|
|
194
|
+
* "[object Object]". Mirrors the server, which reads a JSON-body `null` as
|
|
195
|
+
* "not sent".
|
|
196
|
+
*/
|
|
197
|
+
function normalizeAltchaSubmit(params) {
|
|
198
|
+
const out = {};
|
|
199
|
+
for (const [key, value] of Object.entries(params)) {
|
|
200
|
+
if (value !== undefined && value !== null) {
|
|
201
|
+
out[key] = value;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const challenge = out.challenge_json;
|
|
206
|
+
if (typeof challenge === 'object') {
|
|
207
|
+
out.challenge_json = JSON.stringify(challenge);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
return out;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function validateAltchaSubmit(params) {
|
|
214
|
+
if (!params.pageurl) {
|
|
215
|
+
throw new ValidationException("'pageurl' is required for ALTCHA.");
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// CapSkip answers ERROR_BAD_PARAMETERS when neither is sent. Sending both is
|
|
219
|
+
// deliberately allowed -- the inline document simply wins, because fetching
|
|
220
|
+
// would only re-obtain what the caller already supplied.
|
|
221
|
+
if (!params.challenge_url && !params.challenge_json) {
|
|
222
|
+
throw new ValidationException(
|
|
223
|
+
"ALTCHA needs a challenge: pass 'challenge_url' for CapSkip to fetch it, "
|
|
224
|
+
+ "or 'challenge_json' with the challenge document itself.",
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const unknown = unknownKeys(params, ALTCHA_SUBMIT);
|
|
229
|
+
if (unknown.length > 0) {
|
|
230
|
+
throw new ValidationException(
|
|
231
|
+
`Unsupported parameters for ALTCHA: ${reprList(unknown)}.`,
|
|
232
|
+
);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function validateProxyType(params) {
|
|
237
|
+
const proxytype = params.proxytype;
|
|
238
|
+
if (proxytype === undefined || proxytype === null || proxytype === '') {
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
if (!PROXY_TYPES.includes(String(proxytype).toUpperCase())) {
|
|
242
|
+
throw new ValidationException(
|
|
243
|
+
`Unsupported proxytype '${proxytype}'. `
|
|
244
|
+
+ `CapSkip accepts: ${PROXY_TYPES.join(', ')}.`,
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
148
249
|
function prepareSubmitParams(params, captchaType, version = 'v2') {
|
|
149
250
|
let prepared = applyParamAliases(params);
|
|
150
251
|
prepared = applyProxy(prepared);
|
|
@@ -155,6 +256,16 @@ function prepareSubmitParams(params, captchaType, version = 'v2') {
|
|
|
155
256
|
validateRecaptchaSubmit(prepared, version);
|
|
156
257
|
} else if (captchaType === 'turnstile') {
|
|
157
258
|
validateTurnstileSubmit(prepared);
|
|
259
|
+
} else if (captchaType === 'geetest') {
|
|
260
|
+
validateGeetestSubmit(prepared);
|
|
261
|
+
} else if (captchaType === 'altcha') {
|
|
262
|
+
prepared = normalizeAltchaSubmit(prepared);
|
|
263
|
+
validateAltchaSubmit(prepared);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// Skipped for 'normal', which rejects proxy outright with a clearer message.
|
|
267
|
+
if (captchaType !== 'normal') {
|
|
268
|
+
validateProxyType(prepared);
|
|
158
269
|
}
|
|
159
270
|
|
|
160
271
|
return prepared;
|
|
@@ -165,11 +276,18 @@ module.exports = {
|
|
|
165
276
|
RECAPTCHA_V2_SUBMIT,
|
|
166
277
|
RECAPTCHA_V3_SUBMIT,
|
|
167
278
|
TURNSTILE_SUBMIT,
|
|
279
|
+
GEETEST_SUBMIT,
|
|
280
|
+
ALTCHA_SUBMIT,
|
|
281
|
+
PROXY_TYPES,
|
|
168
282
|
PARAM_ALIASES,
|
|
169
283
|
applyParamAliases,
|
|
170
284
|
applyProxy,
|
|
171
285
|
validateNormalSubmit,
|
|
172
286
|
validateRecaptchaSubmit,
|
|
173
287
|
validateTurnstileSubmit,
|
|
288
|
+
validateGeetestSubmit,
|
|
289
|
+
normalizeAltchaSubmit,
|
|
290
|
+
validateAltchaSubmit,
|
|
291
|
+
validateProxyType,
|
|
174
292
|
prepareSubmitParams,
|
|
175
293
|
};
|
package/src/index.js
CHANGED