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/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
- /** The solution: recognized text for images, a token otherwise. */
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. */