capskip 1.2.0 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +43 -0
- package/README.md +94 -8
- package/docs/API_REFERENCE.md +268 -2
- package/examples/captchafox.js +102 -0
- package/examples/capy.js +101 -0
- package/examples/friendly_captcha.js +115 -0
- package/package.json +1 -1
- package/src/apiParams.js +138 -0
- package/src/solver.js +205 -0
- package/types/index.d.ts +137 -5
package/examples/capy.js
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Solve a Capy Puzzle captcha.
|
|
5
|
+
*
|
|
6
|
+
* Capy is a slide puzzle: a piece has to be dragged into the hole cut out of a
|
|
7
|
+
* photograph. CapSkip locates the hole and produces the drag path, which takes
|
|
8
|
+
* one HTTP fetch and about a tenth of a second of pixel math -- no browser.
|
|
9
|
+
*
|
|
10
|
+
* You need two values, both read off the target page:
|
|
11
|
+
*
|
|
12
|
+
* * captchakey - the site's public Capy key, prefixed `PUZZLE_`. Look for
|
|
13
|
+
* `capy_captchakey` in the page source, or read it out of the widget script
|
|
14
|
+
* URL: <script src="https://jp.api.capy.me/puzzle/get_js/?k=PUZZLE_XXXX">
|
|
15
|
+
* * pageurl - the full URL of the page the captcha is on
|
|
16
|
+
*
|
|
17
|
+
* Optionally `api_server`, the root of the Capy API the key lives behind, taken
|
|
18
|
+
* from that same script URL. CapSkip defaults to https://jp.api.capy.me.
|
|
19
|
+
* Note that api.capy.me no longer resolves, although several solver services
|
|
20
|
+
* still document it -- if a site's widget points elsewhere, pass that.
|
|
21
|
+
*
|
|
22
|
+
* Unlike reCAPTCHA or Turnstile, the answer is NOT a single token. It is three
|
|
23
|
+
* values that together go into the target form, and all three must be submitted.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
const { CapSkip } = require('capskip');
|
|
27
|
+
|
|
28
|
+
const solver = new CapSkip({
|
|
29
|
+
apiKey: process.env.CAPSKIP_API_KEY || 'capskip',
|
|
30
|
+
host: process.env.CAPSKIP_HOST || '127.0.0.1',
|
|
31
|
+
port: Number(process.env.CAPSKIP_PORT || 8080),
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
const CAPTCHA_KEY = 'PUZZLE_Abc1dEFghIJKLM2no34P56q7rStu8v';
|
|
35
|
+
const PAGE_URL = 'https://example.com/login';
|
|
36
|
+
|
|
37
|
+
async function main() {
|
|
38
|
+
const result = await solver.capy(CAPTCHA_KEY, PAGE_URL);
|
|
39
|
+
|
|
40
|
+
console.log('Captcha ID: ', result.captchaId);
|
|
41
|
+
console.log('Captcha key: ', result.captchakey);
|
|
42
|
+
console.log('Challenge key:', result.challengekey);
|
|
43
|
+
console.log('Answer: ', `${result.answer.slice(0, 48)}...`);
|
|
44
|
+
|
|
45
|
+
// --- With a non-default API server ---------------------------------------
|
|
46
|
+
//
|
|
47
|
+
// await solver.capy(CAPTCHA_KEY, PAGE_URL, {
|
|
48
|
+
// api_server: 'https://jp.api.capy.me/',
|
|
49
|
+
// });
|
|
50
|
+
//
|
|
51
|
+
// --- With a per-request proxy --------------------------------------------
|
|
52
|
+
// The proxy is used for the puzzle-image fetch, which is the only request a
|
|
53
|
+
// Capy solve makes. Worth configuring once you solve at any volume: a steady
|
|
54
|
+
// stream of puzzle draws from one address is the pattern rate limiting exists
|
|
55
|
+
// to catch.
|
|
56
|
+
//
|
|
57
|
+
// await solver.capy(CAPTCHA_KEY, PAGE_URL, {
|
|
58
|
+
// proxy: { type: 'HTTP', uri: 'login:password@1.2.3.4:8080' },
|
|
59
|
+
// });
|
|
60
|
+
|
|
61
|
+
// Post all three values back in the fields the Capy widget would have filled
|
|
62
|
+
// in. Submit them verbatim -- `answer` is the drag path the widget would have
|
|
63
|
+
// recorded, and the site's backend verifies it against the challenge it
|
|
64
|
+
// issued, so trimming or re-encoding it invalidates the solve.
|
|
65
|
+
//
|
|
66
|
+
// await fetch(LOGIN_URL, {
|
|
67
|
+
// method: 'POST',
|
|
68
|
+
// body: new URLSearchParams({
|
|
69
|
+
// email: 'someone@example.com',
|
|
70
|
+
// capy_captchakey: result.captchakey,
|
|
71
|
+
// capy_challengekey: result.challengekey,
|
|
72
|
+
// capy_answer: result.answer,
|
|
73
|
+
// }),
|
|
74
|
+
// });
|
|
75
|
+
console.log('Form fields:', {
|
|
76
|
+
capy_captchakey: result.captchakey,
|
|
77
|
+
capy_challengekey: result.challengekey,
|
|
78
|
+
capy_answer: `${result.answer.slice(0, 32)}...`,
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
// The challenge key is single-use and short-lived: CapSkip generates it at
|
|
82
|
+
// solve time and the puzzle is bound to it. Submit promptly rather than
|
|
83
|
+
// caching these values or reusing a challengekey for a second submission.
|
|
84
|
+
//
|
|
85
|
+
// One more thing worth knowing if you ever build against Capy yourself: it
|
|
86
|
+
// grades the wall-clock gap between issuing the puzzle and verifying the
|
|
87
|
+
// answer, and refuses anything superhuman with "CAPTCHA verification failed"
|
|
88
|
+
// -- the same message a wrong answer gets. CapSkip therefore holds every
|
|
89
|
+
// result until two seconds have elapsed, so a solve takes about that long
|
|
90
|
+
// rather than a fifth of a second. Nothing to configure; just do not be
|
|
91
|
+
// surprised by the latency.
|
|
92
|
+
|
|
93
|
+
// `respKey` comes back empty for a puzzle solve. It exists for shape
|
|
94
|
+
// compatibility with 2Captcha's documented response and carries nothing.
|
|
95
|
+
console.log('respKey is empty:', result.respKey === '');
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
main().catch((error) => {
|
|
99
|
+
console.error(`${error.name}: ${error.message}`);
|
|
100
|
+
process.exitCode = 1;
|
|
101
|
+
});
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Solve a Friendly Captcha proof-of-work challenge.
|
|
5
|
+
*
|
|
6
|
+
* Friendly Captcha asks the visitor's browser to do a piece of arithmetic
|
|
7
|
+
* instead of asking the visitor to do anything: there is no image, no slider and
|
|
8
|
+
* no audio fallback. CapSkip does that work and returns the token the widget
|
|
9
|
+
* would have written into your form.
|
|
10
|
+
*
|
|
11
|
+
* You need two values, and a third is worth sending whenever you can get it:
|
|
12
|
+
*
|
|
13
|
+
* * sitekey - the `data-sitekey` attribute of the widget element, which
|
|
14
|
+
* is the one carrying class="frc-captcha"
|
|
15
|
+
* * pageurl - the full URL of the page the widget appears on
|
|
16
|
+
* * moduleScript - the `src` of the widget script tag carrying type="module".
|
|
17
|
+
* Not required, but it is what tells CapSkip which protocol
|
|
18
|
+
* version the site uses.
|
|
19
|
+
*
|
|
20
|
+
* READ THE VERSION SECTION BELOW. It is the part that will cost you an afternoon
|
|
21
|
+
* if you skip it.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
const { CapSkip } = require('capskip');
|
|
25
|
+
|
|
26
|
+
const solver = new CapSkip({
|
|
27
|
+
apiKey: process.env.CAPSKIP_API_KEY || 'capskip',
|
|
28
|
+
host: process.env.CAPSKIP_HOST || '127.0.0.1',
|
|
29
|
+
port: Number(process.env.CAPSKIP_PORT || 8080),
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
const SITEKEY = 'FCMGEMUD2M567T8G';
|
|
33
|
+
const PAGE_URL = 'https://example.com/signup';
|
|
34
|
+
|
|
35
|
+
async function main() {
|
|
36
|
+
// --- Version 1 and version 2 ---------------------------------------------
|
|
37
|
+
// Two entirely different protocols ship under this one name, and a sitekey
|
|
38
|
+
// does not tell you which one a site uses -- they share a brand and a sitekey
|
|
39
|
+
// namespace and nothing else. Both are live and both are in use. Solve the
|
|
40
|
+
// wrong one and you get a well-formed token that the target site rejects, with
|
|
41
|
+
// no indication anywhere that the version was the problem.
|
|
42
|
+
//
|
|
43
|
+
// v1 friendly-challenge widget.module.min.js / widget.min.js (default)
|
|
44
|
+
// v2 @friendlycaptcha/sdk site.min.js
|
|
45
|
+
//
|
|
46
|
+
// CapSkip decides in this order: an explicit `version`, then the script URL,
|
|
47
|
+
// then v1. So either say which version, or send the script URL and let it read
|
|
48
|
+
// the version off the build the site actually loads.
|
|
49
|
+
|
|
50
|
+
// Option A: say which version.
|
|
51
|
+
const result = await solver.friendlyCaptcha(SITEKEY, PAGE_URL, { version: 'v2' });
|
|
52
|
+
|
|
53
|
+
// Option B: let the script URL decide -- the most reliable signal there is.
|
|
54
|
+
//
|
|
55
|
+
// await solver.friendlyCaptcha(SITEKEY, PAGE_URL, {
|
|
56
|
+
// moduleScript: 'https://cdn.example.com/@friendlycaptcha/sdk@0.1.6/site.min.js',
|
|
57
|
+
// nomoduleScript: 'https://cdn.example.com/@friendlycaptcha/sdk@0.1.6/site.compat.js',
|
|
58
|
+
// });
|
|
59
|
+
|
|
60
|
+
console.log('Captcha ID:', result.captchaId);
|
|
61
|
+
console.log('Token: ', `${result.token.slice(0, 60)}...`);
|
|
62
|
+
console.log('Length: ', result.token.length);
|
|
63
|
+
|
|
64
|
+
// --- Data residency -------------------------------------------------------
|
|
65
|
+
// Pass api_server for a sitekey on the EU tenant. Both tenants mint a token
|
|
66
|
+
// for the same sitekey, so the wrong one is only caught by the site's own
|
|
67
|
+
// verification -- which is the worst kind of silent.
|
|
68
|
+
//
|
|
69
|
+
// await solver.friendlyCaptcha(SITEKEY, PAGE_URL, { version: 'v2', api_server: 'eu' });
|
|
70
|
+
//
|
|
71
|
+
// --- With a per-request proxy --------------------------------------------
|
|
72
|
+
// You will want one sooner here than almost anywhere else. The service decides
|
|
73
|
+
// how much work each request is worth and raises that figure for addresses it
|
|
74
|
+
// has already seen a lot of -- the published range between a fresh address and
|
|
75
|
+
// a heavily used one is close to thirty times the work for the same token.
|
|
76
|
+
//
|
|
77
|
+
// await solver.friendlyCaptcha(SITEKEY, PAGE_URL, {
|
|
78
|
+
// version: 'v2',
|
|
79
|
+
// proxy: { type: 'HTTP', uri: 'login:password@1.2.3.4:8080' },
|
|
80
|
+
// });
|
|
81
|
+
|
|
82
|
+
// Post the token back in the hidden field the widget would have filled in.
|
|
83
|
+
// THE TWO VERSIONS DO NOT USE THE SAME FIELD NAME, which is what catches
|
|
84
|
+
// people who move a working v1 integration onto a v2 site:
|
|
85
|
+
//
|
|
86
|
+
// v1 -> frc-captcha-solution
|
|
87
|
+
// v2 -> frc-captcha-response
|
|
88
|
+
//
|
|
89
|
+
// await fetch(SIGNUP_URL, {
|
|
90
|
+
// method: 'POST',
|
|
91
|
+
// body: new URLSearchParams({
|
|
92
|
+
// email: 'someone@example.com',
|
|
93
|
+
// 'frc-captcha-response': result.token, // v2
|
|
94
|
+
// }),
|
|
95
|
+
// });
|
|
96
|
+
console.log('Form field:', { 'frc-captcha-response': `${result.token.slice(0, 40)}...` });
|
|
97
|
+
|
|
98
|
+
// Submit the token verbatim. Do not trim it or re-encode it.
|
|
99
|
+
//
|
|
100
|
+
// The two versions produce tokens of very different shapes and sizes: a v1
|
|
101
|
+
// token is four dot-separated parts and runs to a few hundred characters,
|
|
102
|
+
// while a v2 token is a single opaque string beginning `AQQA.` and is roughly
|
|
103
|
+
// six kilobytes long. Make sure whatever carries it -- a hidden field, a
|
|
104
|
+
// database column, a proxied request -- is sized for that.
|
|
105
|
+
|
|
106
|
+
// Solve time is not a constant for this method: the service decides how much
|
|
107
|
+
// work a request is worth at the moment it is made, so the same sitekey can
|
|
108
|
+
// cost noticeably more at one time than another. This is why the method uses
|
|
109
|
+
// recaptchaTimeout rather than the shorter default.
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
main().catch((error) => {
|
|
113
|
+
console.error(`${error.name}: ${error.message}`);
|
|
114
|
+
process.exitCode = 1;
|
|
115
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "capskip",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
4
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",
|
package/src/apiParams.js
CHANGED
|
@@ -31,6 +31,30 @@ const ALTCHA_SUBMIT = new Set([
|
|
|
31
31
|
'proxy', 'proxytype',
|
|
32
32
|
]);
|
|
33
33
|
|
|
34
|
+
const CAPY_SUBMIT = new Set([
|
|
35
|
+
'method', 'captchakey', 'pageurl', 'api_server', 'version', 'useragent', 'json',
|
|
36
|
+
'proxy', 'proxytype',
|
|
37
|
+
]);
|
|
38
|
+
|
|
39
|
+
const CAPTCHAFOX_SUBMIT = new Set([
|
|
40
|
+
'method', 'sitekey', 'pageurl', 'api_server', 'useragent', 'json',
|
|
41
|
+
'proxy', 'proxytype',
|
|
42
|
+
]);
|
|
43
|
+
|
|
44
|
+
const FRIENDLY_CAPTCHA_SUBMIT = new Set([
|
|
45
|
+
'method', 'sitekey', 'pageurl', 'version', 'module_script', 'nomodule_script',
|
|
46
|
+
'api_server', 'useragent', 'json', 'proxy', 'proxytype',
|
|
47
|
+
]);
|
|
48
|
+
|
|
49
|
+
// CapSkip solves the puzzle family only. `avatar` is a different challenge
|
|
50
|
+
// behind a different endpoint; the server refuses it at submit time rather than
|
|
51
|
+
// answering it with a puzzle answer, which would bill for a solve the target
|
|
52
|
+
// site rejects.
|
|
53
|
+
const CAPY_VERSIONS = ['puzzle'];
|
|
54
|
+
|
|
55
|
+
// Both spellings the server accepts, and the bare digits it also takes.
|
|
56
|
+
const FRIENDLY_CAPTCHA_VERSIONS = ['v1', 'v2', '1', '2'];
|
|
57
|
+
|
|
34
58
|
// The only values CapSkip maps to a proxy scheme; it answers
|
|
35
59
|
// ERROR_BAD_PARAMETERS for anything else, SOCKS4 included. Matched
|
|
36
60
|
// case-insensitively, as the server does.
|
|
@@ -48,6 +72,15 @@ const PARAM_ALIASES = {
|
|
|
48
72
|
challengeURL: 'challenge_url',
|
|
49
73
|
challengeJson: 'challenge_json',
|
|
50
74
|
challengeJSON: 'challenge_json',
|
|
75
|
+
// The server reads userAgent and useragent interchangeably on every method
|
|
76
|
+
// that takes one, so the SDK settles on the lowercase spelling its parameter
|
|
77
|
+
// tables document and accepts the camelCase one callers arrive with.
|
|
78
|
+
userAgent: 'useragent',
|
|
79
|
+
user_agent: 'useragent',
|
|
80
|
+
captchaKey: 'captchakey',
|
|
81
|
+
moduleScript: 'module_script',
|
|
82
|
+
nomoduleScript: 'nomodule_script',
|
|
83
|
+
noModuleScript: 'nomodule_script',
|
|
51
84
|
};
|
|
52
85
|
|
|
53
86
|
function has(obj, key) {
|
|
@@ -233,6 +266,93 @@ function validateAltchaSubmit(params) {
|
|
|
233
266
|
}
|
|
234
267
|
}
|
|
235
268
|
|
|
269
|
+
/**
|
|
270
|
+
* Drop parameters left unset so an omitted optional is not sent as "undefined".
|
|
271
|
+
*
|
|
272
|
+
* The form body can only carry strings, so a default of undefined would
|
|
273
|
+
* otherwise reach the server stringified. Mirrors the server, which reads a
|
|
274
|
+
* JSON-body `null` as "not sent".
|
|
275
|
+
*/
|
|
276
|
+
function dropUnset(params) {
|
|
277
|
+
const out = {};
|
|
278
|
+
for (const [key, value] of Object.entries(params)) {
|
|
279
|
+
if (value !== undefined && value !== null) {
|
|
280
|
+
out[key] = value;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
return out;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function validateCapySubmit(params) {
|
|
287
|
+
// Both are documented as required; without captchakey the server answers
|
|
288
|
+
// ERROR_BAD_PARAMETERS and without pageurl ERROR_PAGEURL. Fail locally so a
|
|
289
|
+
// missing value does not cost a round-trip.
|
|
290
|
+
for (const key of ['captchakey', 'pageurl']) {
|
|
291
|
+
if (!params[key]) {
|
|
292
|
+
throw new ValidationException(`'${key}' is required for Capy.`);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
const version = params.version;
|
|
297
|
+
if (version !== undefined && version !== null && version !== ''
|
|
298
|
+
&& !CAPY_VERSIONS.includes(String(version).toLowerCase())) {
|
|
299
|
+
throw new ValidationException(
|
|
300
|
+
`Unsupported Capy version '${version}'. CapSkip solves the puzzle family `
|
|
301
|
+
+ "only -- 'avatar' is a different challenge behind a different endpoint, "
|
|
302
|
+
+ 'and the server refuses it rather than returning a puzzle answer the '
|
|
303
|
+
+ 'target site would reject.',
|
|
304
|
+
);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
const unknown = unknownKeys(params, CAPY_SUBMIT);
|
|
308
|
+
if (unknown.length > 0) {
|
|
309
|
+
throw new ValidationException(
|
|
310
|
+
`Unsupported parameters for Capy: ${reprList(unknown)}.`,
|
|
311
|
+
);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function validateCaptchaFoxSubmit(params) {
|
|
316
|
+
for (const key of ['sitekey', 'pageurl']) {
|
|
317
|
+
if (!params[key]) {
|
|
318
|
+
throw new ValidationException(`'${key}' is required for CaptchaFox.`);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
const unknown = unknownKeys(params, CAPTCHAFOX_SUBMIT);
|
|
323
|
+
if (unknown.length > 0) {
|
|
324
|
+
throw new ValidationException(
|
|
325
|
+
`Unsupported parameters for CaptchaFox: ${reprList(unknown)}.`,
|
|
326
|
+
);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function validateFriendlyCaptchaSubmit(params) {
|
|
331
|
+
for (const key of ['sitekey', 'pageurl']) {
|
|
332
|
+
if (!params[key]) {
|
|
333
|
+
throw new ValidationException(`'${key}' is required for Friendly Captcha.`);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
const version = params.version;
|
|
338
|
+
if (version !== undefined && version !== null && version !== ''
|
|
339
|
+
&& !FRIENDLY_CAPTCHA_VERSIONS.includes(String(version).toLowerCase())) {
|
|
340
|
+
throw new ValidationException(
|
|
341
|
+
`Unsupported Friendly Captcha version '${version}'. Use 'v1' or 'v2' `
|
|
342
|
+
+ '(a bare 1 or 2 is accepted too). The two are different protocols '
|
|
343
|
+
+ 'sharing one sitekey namespace, so solving the wrong one returns a '
|
|
344
|
+
+ 'well-formed token the target site rejects.',
|
|
345
|
+
);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
const unknown = unknownKeys(params, FRIENDLY_CAPTCHA_SUBMIT);
|
|
349
|
+
if (unknown.length > 0) {
|
|
350
|
+
throw new ValidationException(
|
|
351
|
+
`Unsupported parameters for Friendly Captcha: ${reprList(unknown)}.`,
|
|
352
|
+
);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
236
356
|
function validateProxyType(params) {
|
|
237
357
|
const proxytype = params.proxytype;
|
|
238
358
|
if (proxytype === undefined || proxytype === null || proxytype === '') {
|
|
@@ -261,6 +381,15 @@ function prepareSubmitParams(params, captchaType, version = 'v2') {
|
|
|
261
381
|
} else if (captchaType === 'altcha') {
|
|
262
382
|
prepared = normalizeAltchaSubmit(prepared);
|
|
263
383
|
validateAltchaSubmit(prepared);
|
|
384
|
+
} else if (captchaType === 'capy') {
|
|
385
|
+
prepared = dropUnset(prepared);
|
|
386
|
+
validateCapySubmit(prepared);
|
|
387
|
+
} else if (captchaType === 'captchafox') {
|
|
388
|
+
prepared = dropUnset(prepared);
|
|
389
|
+
validateCaptchaFoxSubmit(prepared);
|
|
390
|
+
} else if (captchaType === 'friendly_captcha') {
|
|
391
|
+
prepared = dropUnset(prepared);
|
|
392
|
+
validateFriendlyCaptchaSubmit(prepared);
|
|
264
393
|
}
|
|
265
394
|
|
|
266
395
|
// Skipped for 'normal', which rejects proxy outright with a clearer message.
|
|
@@ -278,6 +407,11 @@ module.exports = {
|
|
|
278
407
|
TURNSTILE_SUBMIT,
|
|
279
408
|
GEETEST_SUBMIT,
|
|
280
409
|
ALTCHA_SUBMIT,
|
|
410
|
+
CAPY_SUBMIT,
|
|
411
|
+
CAPTCHAFOX_SUBMIT,
|
|
412
|
+
FRIENDLY_CAPTCHA_SUBMIT,
|
|
413
|
+
CAPY_VERSIONS,
|
|
414
|
+
FRIENDLY_CAPTCHA_VERSIONS,
|
|
281
415
|
PROXY_TYPES,
|
|
282
416
|
PARAM_ALIASES,
|
|
283
417
|
applyParamAliases,
|
|
@@ -288,6 +422,10 @@ module.exports = {
|
|
|
288
422
|
validateGeetestSubmit,
|
|
289
423
|
normalizeAltchaSubmit,
|
|
290
424
|
validateAltchaSubmit,
|
|
425
|
+
dropUnset,
|
|
426
|
+
validateCapySubmit,
|
|
427
|
+
validateCaptchaFoxSubmit,
|
|
428
|
+
validateFriendlyCaptchaSubmit,
|
|
291
429
|
validateProxyType,
|
|
292
430
|
prepareSubmitParams,
|
|
293
431
|
};
|
package/src/solver.js
CHANGED
|
@@ -217,6 +217,75 @@ function applyAltchaSolution(result) {
|
|
|
217
217
|
return result;
|
|
218
218
|
}
|
|
219
219
|
|
|
220
|
+
// A Capy solution is not a token. It is three values that together go into the
|
|
221
|
+
// target form, under the capy_ prefixed names the widget would have filled in.
|
|
222
|
+
const CAPY_FIELDS = ['captchakey', 'challengekey', 'answer', 'respKey'];
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Expand the Capy answer into `captchakey` / `challengekey` / `answer`.
|
|
226
|
+
*
|
|
227
|
+
* `code` keeps the raw answer -- an object when polled with json=1, where the
|
|
228
|
+
* server puts it straight into `request`, or the JSON string it sends after
|
|
229
|
+
* `OK|` in plain-text mode -- so callers that forward it verbatim (or that were
|
|
230
|
+
* written against another solver's API) keep working.
|
|
231
|
+
*
|
|
232
|
+
* If the answer does not parse, the result is returned untouched rather than
|
|
233
|
+
* masking the server's reply.
|
|
234
|
+
*/
|
|
235
|
+
function applyCapySolution(result) {
|
|
236
|
+
let payload = result.solution;
|
|
237
|
+
delete result.solution;
|
|
238
|
+
|
|
239
|
+
if (payload === null || typeof payload !== 'object') {
|
|
240
|
+
const { code } = result;
|
|
241
|
+
if (code !== null && typeof code === 'object') {
|
|
242
|
+
payload = code;
|
|
243
|
+
} else {
|
|
244
|
+
try {
|
|
245
|
+
payload = JSON.parse(code || '');
|
|
246
|
+
} catch (error) {
|
|
247
|
+
return result;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
if (payload === null || typeof payload !== 'object' || Array.isArray(payload)) {
|
|
253
|
+
return result;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
for (const field of CAPY_FIELDS) {
|
|
257
|
+
if (Object.prototype.hasOwnProperty.call(payload, field)) {
|
|
258
|
+
result[field] = payload[field];
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
return result;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Expose a single-token answer as `token`, named for the form field it fills.
|
|
267
|
+
*
|
|
268
|
+
* `code` keeps the raw answer so callers that forward it verbatim keep working;
|
|
269
|
+
* `token` is the same string. The server's createTask-shaped `solution` object
|
|
270
|
+
* carries that same string, so it is consumed here rather than handed back as a
|
|
271
|
+
* second copy -- but it is read first when `request` came through empty, so a
|
|
272
|
+
* client is never left without the token the poll actually carried.
|
|
273
|
+
*/
|
|
274
|
+
function applyTokenSolution(result) {
|
|
275
|
+
const { solution } = result;
|
|
276
|
+
delete result.solution;
|
|
277
|
+
|
|
278
|
+
let code = result.code;
|
|
279
|
+
|
|
280
|
+
if (!code && solution !== null && typeof solution === 'object') {
|
|
281
|
+
code = solution.token || '';
|
|
282
|
+
result.code = code;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
result.token = code || '';
|
|
286
|
+
return result;
|
|
287
|
+
}
|
|
288
|
+
|
|
220
289
|
// CapSkip's in.php returns OK|<id> by default, or {"status":1,"request":"<id>"}
|
|
221
290
|
// when the submit carried json=1. Accept both so submitting with json=1 works.
|
|
222
291
|
function parseSubmitResponse(response) {
|
|
@@ -359,6 +428,131 @@ class CapSkip {
|
|
|
359
428
|
return applyAltchaSolution(result);
|
|
360
429
|
}
|
|
361
430
|
|
|
431
|
+
/**
|
|
432
|
+
* Solve a Capy Puzzle captcha.
|
|
433
|
+
*
|
|
434
|
+
* `sitekey` is the site's public Capy key, conventionally prefixed `PUZZLE_`;
|
|
435
|
+
* it is sent as the `captchakey` the API documents. Pass `api_server` when the
|
|
436
|
+
* widget script points somewhere other than `https://jp.api.capy.me`.
|
|
437
|
+
*
|
|
438
|
+
* The result is not a token. It carries `captchakey`, `challengekey` and
|
|
439
|
+
* `answer`, which go into the target form's `capy_captchakey`,
|
|
440
|
+
* `capy_challengekey` and `capy_answer` fields, plus the raw answer as `code`.
|
|
441
|
+
* Submit `answer` verbatim -- it is the drag path the widget would have
|
|
442
|
+
* recorded, so trimming or re-encoding it invalidates the solve.
|
|
443
|
+
*
|
|
444
|
+
* The challenge key is single-use and short-lived, so submit promptly rather
|
|
445
|
+
* than caching the three values for a later request.
|
|
446
|
+
*/
|
|
447
|
+
async capy(sitekey, url, options = {}) {
|
|
448
|
+
// An unset optional is dropped rather than sent as undefined, so
|
|
449
|
+
// `capy(key, url, { apiServer: undefined })` behaves as if it were omitted.
|
|
450
|
+
const given = {};
|
|
451
|
+
for (const [key, value] of Object.entries(options)) {
|
|
452
|
+
if (value !== undefined && value !== null) {
|
|
453
|
+
given[key] = value;
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
// A Capy solve is one HTTP fetch plus pixel math, not a browser session, so
|
|
458
|
+
// it keeps the default timeout. It is held back to roughly two seconds
|
|
459
|
+
// before the answer is released -- Capy refuses answers that arrive faster
|
|
460
|
+
// than a human could have produced them -- which the default absorbs.
|
|
461
|
+
const result = await this.solve({
|
|
462
|
+
captchakey: sitekey,
|
|
463
|
+
url,
|
|
464
|
+
...given,
|
|
465
|
+
method: 'capy',
|
|
466
|
+
poll_json: 1,
|
|
467
|
+
});
|
|
468
|
+
return applyCapySolution(result);
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
/**
|
|
472
|
+
* Solve a CaptchaFox challenge.
|
|
473
|
+
*
|
|
474
|
+
* `sitekey` is the public key the widget renders with, conventionally prefixed
|
|
475
|
+
* `sk_`, and `url` has to be the page the widget actually runs on: CaptchaFox
|
|
476
|
+
* checks it against the domains the key is registered for and refuses a
|
|
477
|
+
* mismatch permanently rather than intermittently.
|
|
478
|
+
*
|
|
479
|
+
* Pass `api_server` only when the target page does not load the default
|
|
480
|
+
* widget. A page loading the MAM package expects a `MAM_` prefixed token, and
|
|
481
|
+
* sending the wrong source still succeeds -- it just returns a token in a
|
|
482
|
+
* format the site will not accept, which reads as a silent verification
|
|
483
|
+
* failure rather than an error.
|
|
484
|
+
*
|
|
485
|
+
* The result carries the token as both `code` and `token`, for the form's
|
|
486
|
+
* `cf-captcha-response` field, and `userAgent` when the solve reported one.
|
|
487
|
+
* That User-Agent is the browser's own, not any you sent, so submit the token
|
|
488
|
+
* under it.
|
|
489
|
+
*/
|
|
490
|
+
async captchafox(sitekey, url, options = {}) {
|
|
491
|
+
const given = {};
|
|
492
|
+
for (const [key, value] of Object.entries(options)) {
|
|
493
|
+
if (value !== undefined && value !== null) {
|
|
494
|
+
given[key] = value;
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
// A real browser session, like reCAPTCHA and GeeTest, and longer again when
|
|
499
|
+
// an interactive challenge is drawn -- so it gets the longer of the two
|
|
500
|
+
// timeouts unless the caller asked for a specific one.
|
|
501
|
+
const result = await this.solve({
|
|
502
|
+
timeout: this.recaptchaTimeout,
|
|
503
|
+
sitekey,
|
|
504
|
+
url,
|
|
505
|
+
...given,
|
|
506
|
+
method: 'captchafox',
|
|
507
|
+
poll_json: 1,
|
|
508
|
+
});
|
|
509
|
+
return applyTokenSolution(result);
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
/**
|
|
513
|
+
* Solve a Friendly Captcha proof-of-work challenge.
|
|
514
|
+
*
|
|
515
|
+
* Two different protocols ship under this name and a sitekey does not tell you
|
|
516
|
+
* which one a site uses, so say which: pass `version: 'v1'` or `version: 'v2'`,
|
|
517
|
+
* or pass `moduleScript` with the src of the widget's `type="module"` script
|
|
518
|
+
* tag and let CapSkip read the version off the build the site actually loads.
|
|
519
|
+
* With neither, v1 is assumed. Solving the wrong version returns a well-formed
|
|
520
|
+
* token the target site rejects, with nothing to indicate the version was the
|
|
521
|
+
* problem.
|
|
522
|
+
*
|
|
523
|
+
* Pass `api_server: 'eu'` for a sitekey on the EU data-residency tenant; both
|
|
524
|
+
* tenants mint a token for the same sitekey, so the wrong one is only caught
|
|
525
|
+
* by the site's own verification.
|
|
526
|
+
*
|
|
527
|
+
* The result carries the token as both `code` and `token`. It goes into
|
|
528
|
+
* `frc-captcha-solution` on v1 and `frc-captcha-response` on v2 -- the field
|
|
529
|
+
* names differ, which is what catches an integration moved from one to the
|
|
530
|
+
* other. A v2 token is roughly six kilobytes, so size whatever carries it
|
|
531
|
+
* accordingly.
|
|
532
|
+
*/
|
|
533
|
+
async friendlyCaptcha(sitekey, url, options = {}) {
|
|
534
|
+
const given = {};
|
|
535
|
+
for (const [key, value] of Object.entries(options)) {
|
|
536
|
+
if (value !== undefined && value !== null) {
|
|
537
|
+
given[key] = value;
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
// Proof-of-work, but not the millisecond kind ALTCHA does: the service sets
|
|
542
|
+
// the difficulty per request and raises it for addresses it has seen a lot
|
|
543
|
+
// of, and v2 always solves in a browser. Both make solve time variable
|
|
544
|
+
// enough to want the longer timeout.
|
|
545
|
+
const result = await this.solve({
|
|
546
|
+
timeout: this.recaptchaTimeout,
|
|
547
|
+
sitekey,
|
|
548
|
+
url,
|
|
549
|
+
...given,
|
|
550
|
+
method: 'friendly_captcha',
|
|
551
|
+
poll_json: 1,
|
|
552
|
+
});
|
|
553
|
+
return applyTokenSolution(result);
|
|
554
|
+
}
|
|
555
|
+
|
|
362
556
|
async solve(options = {}) {
|
|
363
557
|
const {
|
|
364
558
|
timeout = 0,
|
|
@@ -450,6 +644,15 @@ class CapSkip {
|
|
|
450
644
|
if (method === 'altcha') {
|
|
451
645
|
return prepareSubmitParams(params, 'altcha');
|
|
452
646
|
}
|
|
647
|
+
if (method === 'capy') {
|
|
648
|
+
return prepareSubmitParams(params, 'capy');
|
|
649
|
+
}
|
|
650
|
+
if (method === 'captchafox') {
|
|
651
|
+
return prepareSubmitParams(params, 'captchafox');
|
|
652
|
+
}
|
|
653
|
+
if (method === 'friendly_captcha') {
|
|
654
|
+
return prepareSubmitParams(params, 'friendly_captcha');
|
|
655
|
+
}
|
|
453
656
|
return applyProxy(applyParamAliases(params));
|
|
454
657
|
}
|
|
455
658
|
}
|
|
@@ -463,4 +666,6 @@ module.exports = {
|
|
|
463
666
|
applyPollResult,
|
|
464
667
|
applyGeetestSolution,
|
|
465
668
|
applyAltchaSolution,
|
|
669
|
+
applyCapySolution,
|
|
670
|
+
applyTokenSolution,
|
|
466
671
|
};
|