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/src/solver.js
CHANGED
|
@@ -83,12 +83,140 @@ function applyPollResult(result, polled) {
|
|
|
83
83
|
if (userAgent) {
|
|
84
84
|
result.userAgent = userAgent;
|
|
85
85
|
}
|
|
86
|
+
// ALTCHA's createTask-shaped `solution` object. Carried through so
|
|
87
|
+
// applyAltchaSolution can read the counter the server already worked out,
|
|
88
|
+
// which is the only reliable source for a proof-of-work v2 answer; that
|
|
89
|
+
// function deletes it, so it never reaches the caller.
|
|
90
|
+
if (polled.solution !== null && typeof polled.solution === 'object') {
|
|
91
|
+
result.solution = polled.solution;
|
|
92
|
+
}
|
|
86
93
|
} else {
|
|
87
94
|
result.code = polled;
|
|
88
95
|
}
|
|
89
96
|
return result;
|
|
90
97
|
}
|
|
91
98
|
|
|
99
|
+
// GeeTest answers come back as a JSON string in `request`, keyed with the
|
|
100
|
+
// geetest_ prefix that the target site's own form fields use.
|
|
101
|
+
const GEETEST_FIELDS = [
|
|
102
|
+
['challenge', 'geetest_challenge'],
|
|
103
|
+
['validate', 'geetest_validate'],
|
|
104
|
+
['seccode', 'geetest_seccode'],
|
|
105
|
+
];
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Expand the GeeTest answer into `challenge` / `validate` / `seccode`.
|
|
109
|
+
*
|
|
110
|
+
* `code` keeps the raw JSON string so callers that forward it verbatim (or that
|
|
111
|
+
* were written against another solver's API) keep working. If it does not parse,
|
|
112
|
+
* the result is returned untouched rather than masking the server's reply.
|
|
113
|
+
*/
|
|
114
|
+
function applyGeetestSolution(result) {
|
|
115
|
+
let payload;
|
|
116
|
+
try {
|
|
117
|
+
payload = JSON.parse(result.code || '');
|
|
118
|
+
} catch (err) {
|
|
119
|
+
return result;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (payload === null || typeof payload !== 'object' || Array.isArray(payload)) {
|
|
123
|
+
return result;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
for (const [short, prefixed] of GEETEST_FIELDS) {
|
|
127
|
+
const value = payload[prefixed] !== undefined ? payload[prefixed] : payload[short];
|
|
128
|
+
if (value !== undefined && value !== null) {
|
|
129
|
+
result[short] = value;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return result;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// ALTCHA answers come back as a base64 payload: the challenge document with the
|
|
137
|
+
// winning counter added. That payload is what the site's own `altcha` form field
|
|
138
|
+
// carries, so it is posted back verbatim.
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Expose the answer as `token`, and the winning counter as `number`.
|
|
142
|
+
*
|
|
143
|
+
* `code` keeps the raw answer so callers that forward it verbatim (or that were
|
|
144
|
+
* written against another solver's API) keep working; `token` is the same string,
|
|
145
|
+
* named for the form field it goes into. If the payload does not decode, the
|
|
146
|
+
* result is returned untouched rather than masking the server's reply.
|
|
147
|
+
*/
|
|
148
|
+
/**
|
|
149
|
+
* Dig the winning counter out of a token, whichever scheme produced it.
|
|
150
|
+
*
|
|
151
|
+
* The two ALTCHA generations nest it differently: a legacy payload is the
|
|
152
|
+
* challenge document with a top-level `number` added, while a proof-of-work v2
|
|
153
|
+
* payload is `{ challenge: {...}, solution: { counter: N, ... } }` and has no
|
|
154
|
+
* `number` at all. Returns undefined if the payload does not decode.
|
|
155
|
+
*/
|
|
156
|
+
function tokenCounter(code) {
|
|
157
|
+
let payload;
|
|
158
|
+
try {
|
|
159
|
+
const decoded = Buffer.from(code, 'base64');
|
|
160
|
+
// Buffer.from is lenient: it drops invalid characters instead of throwing,
|
|
161
|
+
// so round-trip to confirm the input really was base64 before trusting it.
|
|
162
|
+
if (decoded.toString('base64').replace(/=+$/, '') !== code.replace(/=+$/, '')) {
|
|
163
|
+
return undefined;
|
|
164
|
+
}
|
|
165
|
+
payload = JSON.parse(decoded.toString('utf8'));
|
|
166
|
+
} catch (err) {
|
|
167
|
+
return undefined;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (payload === null || typeof payload !== 'object' || Array.isArray(payload)) {
|
|
171
|
+
return undefined;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (payload.number !== undefined) {
|
|
175
|
+
return payload.number;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (payload.solution !== null && typeof payload.solution === 'object') {
|
|
179
|
+
return payload.solution.counter;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return undefined;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Expose the answer as `token`, and the winning counter as `number`.
|
|
187
|
+
*
|
|
188
|
+
* `code` keeps the raw answer so callers that forward it verbatim (or that were
|
|
189
|
+
* written against another solver's API) keep working; `token` is the same
|
|
190
|
+
* string, named for the form field it goes into.
|
|
191
|
+
*
|
|
192
|
+
* The counter comes from the server's own `solution` object when the poll
|
|
193
|
+
* carried one, because that is the single field both ALTCHA generations report
|
|
194
|
+
* the same way. Only if it is absent — a plain-text poll — is it dug out of the
|
|
195
|
+
* token, which is shaped differently per scheme. If neither yields one, the
|
|
196
|
+
* result keeps its token and simply has no `number`, rather than masking the
|
|
197
|
+
* server's reply.
|
|
198
|
+
*/
|
|
199
|
+
function applyAltchaSolution(result) {
|
|
200
|
+
const code = result.code || '';
|
|
201
|
+
result.token = code;
|
|
202
|
+
|
|
203
|
+
const { solution } = result;
|
|
204
|
+
delete result.solution;
|
|
205
|
+
|
|
206
|
+
let number = solution !== null && typeof solution === 'object'
|
|
207
|
+
? solution.number
|
|
208
|
+
: undefined;
|
|
209
|
+
if (number === undefined) {
|
|
210
|
+
number = tokenCounter(code);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (number !== undefined) {
|
|
214
|
+
result.number = number;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
return result;
|
|
218
|
+
}
|
|
219
|
+
|
|
92
220
|
// CapSkip's in.php returns OK|<id> by default, or {"status":1,"request":"<id>"}
|
|
93
221
|
// when the submit carried json=1. Accept both so submitting with json=1 works.
|
|
94
222
|
function parseSubmitResponse(response) {
|
|
@@ -112,7 +240,7 @@ function parseSubmitResponse(response) {
|
|
|
112
240
|
throw new ApiException(`cannot recognize response ${response}`);
|
|
113
241
|
}
|
|
114
242
|
|
|
115
|
-
/** Client for the CapSkip local captcha solver (image, reCAPTCHA, Turnstile). */
|
|
243
|
+
/** Client for the CapSkip local captcha solver (image, reCAPTCHA, Turnstile, GeeTest v3, ALTCHA). */
|
|
116
244
|
class CapSkip {
|
|
117
245
|
constructor({
|
|
118
246
|
apiKey = 'capskip',
|
|
@@ -167,6 +295,70 @@ class CapSkip {
|
|
|
167
295
|
});
|
|
168
296
|
}
|
|
169
297
|
|
|
298
|
+
/**
|
|
299
|
+
* Solve a GeeTest v3 slider.
|
|
300
|
+
*
|
|
301
|
+
* `gt` is static per site; `challenge` is single-use and expires in about a
|
|
302
|
+
* minute, so fetch a fresh pair immediately before calling this. Pass
|
|
303
|
+
* `api_server` when the site uses a non-default GeeTest API server domain.
|
|
304
|
+
*
|
|
305
|
+
* The result carries the raw answer as `code` (a JSON string) plus the parsed
|
|
306
|
+
* `challenge`, `validate`, and `seccode` fields to post back to the target site.
|
|
307
|
+
*/
|
|
308
|
+
async geetest(gt, challenge, url, options = {}) {
|
|
309
|
+
// Like reCAPTCHA, this is a real browser solve (load, slide, verify) and can
|
|
310
|
+
// retry internally, so it gets the longer of the two timeouts unless the
|
|
311
|
+
// caller asked for a specific one.
|
|
312
|
+
const result = await this.solve({
|
|
313
|
+
timeout: this.recaptchaTimeout,
|
|
314
|
+
gt,
|
|
315
|
+
challenge,
|
|
316
|
+
url,
|
|
317
|
+
...options,
|
|
318
|
+
method: 'geetest',
|
|
319
|
+
poll_json: 1,
|
|
320
|
+
});
|
|
321
|
+
return applyGeetestSolution(result);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* Solve an ALTCHA proof-of-work challenge.
|
|
326
|
+
*
|
|
327
|
+
* Pass `challengeUrl` for CapSkip to fetch the challenge itself, or
|
|
328
|
+
* `challengeJson` with the document you already have (a JSON string, or an
|
|
329
|
+
* object which is serialized for you). Sending both is allowed -- the inline
|
|
330
|
+
* document wins. A proxy applies only to the `challengeUrl` fetch.
|
|
331
|
+
*
|
|
332
|
+
* Challenges expire fast -- some sites inside two minutes -- and an expired one
|
|
333
|
+
* is refused with a bare "verification failed" that looks exactly like a wrong
|
|
334
|
+
* answer. Fetch the challenge immediately before calling, and post the token
|
|
335
|
+
* promptly.
|
|
336
|
+
*
|
|
337
|
+
* The result carries the raw answer as `code`, the same string as `token`
|
|
338
|
+
* (what the site's `altcha` form field expects, verbatim), and the counter
|
|
339
|
+
* that solved it as `number`.
|
|
340
|
+
*/
|
|
341
|
+
async altcha(url, options = {}) {
|
|
342
|
+
// An unset challenge param is dropped rather than sent as undefined, so
|
|
343
|
+
// `altcha(url, { challengeUrl, challengeJson })` works with either left out.
|
|
344
|
+
const given = {};
|
|
345
|
+
for (const [key, value] of Object.entries(options)) {
|
|
346
|
+
if (value !== undefined && value !== null) {
|
|
347
|
+
given[key] = value;
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// Unlike GeeTest and reCAPTCHA this is CPU proof-of-work measured in
|
|
352
|
+
// milliseconds, not a browser solve, so it keeps the default timeout.
|
|
353
|
+
const result = await this.solve({
|
|
354
|
+
url,
|
|
355
|
+
...given,
|
|
356
|
+
method: 'altcha',
|
|
357
|
+
poll_json: 1,
|
|
358
|
+
});
|
|
359
|
+
return applyAltchaSolution(result);
|
|
360
|
+
}
|
|
361
|
+
|
|
170
362
|
async solve(options = {}) {
|
|
171
363
|
const {
|
|
172
364
|
timeout = 0,
|
|
@@ -252,6 +444,12 @@ class CapSkip {
|
|
|
252
444
|
if (method === 'turnstile') {
|
|
253
445
|
return prepareSubmitParams(params, 'turnstile');
|
|
254
446
|
}
|
|
447
|
+
if (method === 'geetest') {
|
|
448
|
+
return prepareSubmitParams(params, 'geetest');
|
|
449
|
+
}
|
|
450
|
+
if (method === 'altcha') {
|
|
451
|
+
return prepareSubmitParams(params, 'altcha');
|
|
452
|
+
}
|
|
255
453
|
return applyProxy(applyParamAliases(params));
|
|
256
454
|
}
|
|
257
455
|
}
|
|
@@ -263,4 +461,6 @@ module.exports = {
|
|
|
263
461
|
parsePollResponse,
|
|
264
462
|
parseSubmitResponse,
|
|
265
463
|
applyPollResult,
|
|
464
|
+
applyGeetestSolution,
|
|
465
|
+
applyAltchaSolution,
|
|
266
466
|
};
|
package/types/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Type definitions for the CapSkip Node.js SDK.
|
|
2
2
|
|
|
3
|
-
/** Proxy passed to reCAPTCHA / Turnstile solves. */
|
|
3
|
+
/** Proxy passed to reCAPTCHA / Turnstile / GeeTest solves. */
|
|
4
4
|
export interface Proxy {
|
|
5
5
|
/** Proxy type: `HTTP`, `HTTPS`, `SOCKS5`, or `SOCKS5H`. */
|
|
6
6
|
type: string;
|
|
@@ -18,7 +18,7 @@ export interface CapSkipOptions {
|
|
|
18
18
|
port?: number;
|
|
19
19
|
/** Seconds to poll an image captcha before timing out. Default `120`. */
|
|
20
20
|
defaultTimeout?: number;
|
|
21
|
-
/** Seconds to poll reCAPTCHA / Turnstile before timing out. Default `300`. */
|
|
21
|
+
/** Seconds to poll reCAPTCHA / Turnstile / GeeTest before timing out. Default `300`. */
|
|
22
22
|
recaptchaTimeout?: number;
|
|
23
23
|
/** Max seconds between polls; starts at `0.25` and backs off to this. Default `5`. */
|
|
24
24
|
pollingInterval?: number;
|
|
@@ -28,10 +28,28 @@ export interface CapSkipOptions {
|
|
|
28
28
|
export interface SolveResult {
|
|
29
29
|
/** CapSkip's internal id for this solve. */
|
|
30
30
|
captchaId: string;
|
|
31
|
-
/**
|
|
31
|
+
/**
|
|
32
|
+
* The solution: recognized text for images, a token otherwise. For GeeTest
|
|
33
|
+
* this is the raw JSON string CapSkip returns — prefer the parsed
|
|
34
|
+
* `challenge` / `validate` / `seccode` fields below. For ALTCHA it is the
|
|
35
|
+
* base64 token, also exposed as `token`.
|
|
36
|
+
*/
|
|
32
37
|
code: string;
|
|
33
38
|
/** Turnstile only — the User-Agent to use when submitting the token. */
|
|
34
39
|
userAgent?: string;
|
|
40
|
+
/** GeeTest only — the `geetest_challenge` value to post back. */
|
|
41
|
+
challenge?: string;
|
|
42
|
+
/** GeeTest only — the `geetest_validate` value to post back. */
|
|
43
|
+
validate?: string;
|
|
44
|
+
/** GeeTest only — the `geetest_seccode` value to post back. */
|
|
45
|
+
seccode?: string;
|
|
46
|
+
/**
|
|
47
|
+
* ALTCHA only — the base64 payload to post back in the site's `altcha` form
|
|
48
|
+
* field. The same string as `code`, named for where it goes.
|
|
49
|
+
*/
|
|
50
|
+
token?: string;
|
|
51
|
+
/** ALTCHA only — the counter that solved the challenge. */
|
|
52
|
+
number?: number;
|
|
35
53
|
}
|
|
36
54
|
|
|
37
55
|
/** Extra options for {@link CapSkip.normal}. */
|
|
@@ -84,6 +102,45 @@ export interface TurnstileOptions {
|
|
|
84
102
|
[key: string]: unknown;
|
|
85
103
|
}
|
|
86
104
|
|
|
105
|
+
/** Extra options for {@link CapSkip.geetest}. */
|
|
106
|
+
export interface GeetestOptions {
|
|
107
|
+
/** GeeTest API server domain (alias for `api_server`). */
|
|
108
|
+
apiServer?: string;
|
|
109
|
+
/** GeeTest API server domain (alias for `api_server`). */
|
|
110
|
+
api_subdomain?: string;
|
|
111
|
+
/** GeeTest API server domain, e.g. `"api-na.geetest.com"`. */
|
|
112
|
+
api_server?: string;
|
|
113
|
+
/** `1` to request the raw JSON response from CapSkip. */
|
|
114
|
+
json?: number;
|
|
115
|
+
/** Proxy used for solving. */
|
|
116
|
+
proxy?: Proxy | string;
|
|
117
|
+
/** Proxy type when `proxy` is a bare string. */
|
|
118
|
+
proxytype?: string;
|
|
119
|
+
[key: string]: unknown;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** Extra options for {@link CapSkip.altcha}. */
|
|
123
|
+
export interface AltchaOptions {
|
|
124
|
+
/** Endpoint CapSkip fetches the challenge from. */
|
|
125
|
+
challengeUrl?: string;
|
|
126
|
+
/** Endpoint CapSkip fetches the challenge from. */
|
|
127
|
+
challenge_url?: string;
|
|
128
|
+
/**
|
|
129
|
+
* The challenge document itself. An object is serialized for you; a string is
|
|
130
|
+
* sent as-is.
|
|
131
|
+
*/
|
|
132
|
+
challengeJson?: string | Record<string, unknown>;
|
|
133
|
+
/** The challenge document itself. */
|
|
134
|
+
challenge_json?: string | Record<string, unknown>;
|
|
135
|
+
/** `1` to request the raw JSON response from CapSkip. */
|
|
136
|
+
json?: number;
|
|
137
|
+
/** Proxy — used only for the `challengeUrl` fetch, never for the solve. */
|
|
138
|
+
proxy?: Proxy | string;
|
|
139
|
+
/** Proxy type when `proxy` is a bare string. */
|
|
140
|
+
proxytype?: string;
|
|
141
|
+
[key: string]: unknown;
|
|
142
|
+
}
|
|
143
|
+
|
|
87
144
|
/** Options for the {@link CapSkip.solve} manual workflow. */
|
|
88
145
|
export interface SolveOptions {
|
|
89
146
|
/** Poll timeout in seconds (falls back to the configured default). */
|
|
@@ -111,6 +168,26 @@ export class CapSkip {
|
|
|
111
168
|
recaptcha(sitekey: string, url: string, options?: RecaptchaOptions): Promise<SolveResult>;
|
|
112
169
|
/** Solve Cloudflare Turnstile (widget or challenge page). */
|
|
113
170
|
turnstile(sitekey: string, url: string, options?: TurnstileOptions): Promise<SolveResult>;
|
|
171
|
+
/**
|
|
172
|
+
* Solve a GeeTest v3 slider.
|
|
173
|
+
*
|
|
174
|
+
* `gt` is static per site; `challenge` is single-use and expires in about a
|
|
175
|
+
* minute, so fetch a fresh pair immediately before calling this.
|
|
176
|
+
*/
|
|
177
|
+
geetest(
|
|
178
|
+
gt: string,
|
|
179
|
+
challenge: string,
|
|
180
|
+
url: string,
|
|
181
|
+
options?: GeetestOptions,
|
|
182
|
+
): Promise<SolveResult>;
|
|
183
|
+
/**
|
|
184
|
+
* Solve an ALTCHA proof-of-work challenge.
|
|
185
|
+
*
|
|
186
|
+
* Pass `challengeUrl` for CapSkip to fetch the challenge, or `challengeJson`
|
|
187
|
+
* with the document itself. Sending both is allowed — the inline document
|
|
188
|
+
* wins. Challenges expire fast, so fetch one immediately before calling.
|
|
189
|
+
*/
|
|
190
|
+
altcha(url: string, options?: AltchaOptions): Promise<SolveResult>;
|
|
114
191
|
/** Submit then poll to completion. Used by the higher-level solve methods. */
|
|
115
192
|
solve(options?: SolveOptions): Promise<SolveResult>;
|
|
116
193
|
/** Submit a captcha without polling; resolves to the captcha id. */
|