capskip 1.3.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 +77 -0
- package/README.md +118 -7
- package/docs/API_REFERENCE.md +267 -1
- package/examples/amazon_waf.js +97 -0
- package/examples/datadome.js +142 -0
- package/package.json +6 -2
- package/src/apiParams.js +314 -0
- package/src/index.js +1 -1
- package/src/solver.js +160 -4
- package/types/index.d.ts +122 -6
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
|
|
@@ -222,7 +228,7 @@ function applyAltchaSolution(result) {
|
|
|
222
228
|
const CAPY_FIELDS = ['captchakey', 'challengekey', 'answer', 'respKey'];
|
|
223
229
|
|
|
224
230
|
/**
|
|
225
|
-
*
|
|
231
|
+
* Copy `fields` out of an answer that is an object rather than a token.
|
|
226
232
|
*
|
|
227
233
|
* `code` keeps the raw answer -- an object when polled with json=1, where the
|
|
228
234
|
* server puts it straight into `request`, or the JSON string it sends after
|
|
@@ -232,7 +238,7 @@ const CAPY_FIELDS = ['captchakey', 'challengekey', 'answer', 'respKey'];
|
|
|
232
238
|
* If the answer does not parse, the result is returned untouched rather than
|
|
233
239
|
* masking the server's reply.
|
|
234
240
|
*/
|
|
235
|
-
function
|
|
241
|
+
function expandObjectAnswer(result, fields) {
|
|
236
242
|
let payload = result.solution;
|
|
237
243
|
delete result.solution;
|
|
238
244
|
|
|
@@ -253,7 +259,7 @@ function applyCapySolution(result) {
|
|
|
253
259
|
return result;
|
|
254
260
|
}
|
|
255
261
|
|
|
256
|
-
for (const field of
|
|
262
|
+
for (const field of fields) {
|
|
257
263
|
if (Object.prototype.hasOwnProperty.call(payload, field)) {
|
|
258
264
|
result[field] = payload[field];
|
|
259
265
|
}
|
|
@@ -262,6 +268,39 @@ function applyCapySolution(result) {
|
|
|
262
268
|
return result;
|
|
263
269
|
}
|
|
264
270
|
|
|
271
|
+
/** Expand the Capy answer into `captchakey` / `challengekey` / `answer`. */
|
|
272
|
+
function applyCapySolution(result) {
|
|
273
|
+
return expandObjectAnswer(result, CAPY_FIELDS);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// An AWS WAF solution is two values that only work as a pair: the voucher AWS
|
|
277
|
+
// issued for the answered puzzle, and the token it was issued for. The site's
|
|
278
|
+
// own /voucher endpoint trades them for the aws-waf-token cookie.
|
|
279
|
+
const AMAZON_WAF_FIELDS = ['captcha_voucher', 'existing_token'];
|
|
280
|
+
|
|
281
|
+
/** Expand the AWS WAF answer into `captcha_voucher` / `existing_token`. */
|
|
282
|
+
function applyAmazonWafSolution(result) {
|
|
283
|
+
return expandObjectAnswer(result, AMAZON_WAF_FIELDS);
|
|
284
|
+
}
|
|
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
|
+
|
|
265
304
|
/**
|
|
266
305
|
* Expose a single-token answer as `token`, named for the form field it fills.
|
|
267
306
|
*
|
|
@@ -309,7 +348,11 @@ function parseSubmitResponse(response) {
|
|
|
309
348
|
throw new ApiException(`cannot recognize response ${response}`);
|
|
310
349
|
}
|
|
311
350
|
|
|
312
|
-
/**
|
|
351
|
+
/**
|
|
352
|
+
* Client for the CapSkip local captcha solver (image, reCAPTCHA, Turnstile,
|
|
353
|
+
* GeeTest v3, ALTCHA, Capy Puzzle, CaptchaFox, Friendly Captcha, AWS WAF,
|
|
354
|
+
* DataDome).
|
|
355
|
+
*/
|
|
313
356
|
class CapSkip {
|
|
314
357
|
constructor({
|
|
315
358
|
apiKey = 'capskip',
|
|
@@ -553,6 +596,110 @@ class CapSkip {
|
|
|
553
596
|
return applyTokenSolution(result);
|
|
554
597
|
}
|
|
555
598
|
|
|
599
|
+
/**
|
|
600
|
+
* Solve an AWS WAF (Amazon WAF) CAPTCHA.
|
|
601
|
+
*
|
|
602
|
+
* Only `sitekey` and `url` are required:
|
|
603
|
+
*
|
|
604
|
+
* - On a challenge page (the HTTP 405 page with `window.gokuProps`), `sitekey`
|
|
605
|
+
* is `gokuProps.key`, and `iv` and `context` are `gokuProps.iv` and
|
|
606
|
+
* `gokuProps.context`. Send both or neither -- left out, CapSkip reads them
|
|
607
|
+
* from `url` at solve time, which is the safer choice when your copy of the
|
|
608
|
+
* page is more than a few minutes old. `challenge_script` and
|
|
609
|
+
* `captcha_script` are the full URLs of the page's challenge.js and
|
|
610
|
+
* captcha.js.
|
|
611
|
+
* - On a widget page (`AwsWafCaptcha.renderCaptcha`), `sitekey` is the
|
|
612
|
+
* widget's `apiKey`; pass `jsapiScript` with the full URL of jsapi.js and no
|
|
613
|
+
* `iv` / `context`.
|
|
614
|
+
*
|
|
615
|
+
* Every script URL must be an https URL on an awswaf.com host; anything else
|
|
616
|
+
* is refused before the request is sent.
|
|
617
|
+
*
|
|
618
|
+
* The result carries `captcha_voucher` and `existing_token`, plus the raw
|
|
619
|
+
* answer as `code`. The two only work as a pair: POST them as JSON to the
|
|
620
|
+
* `voucher` endpoint next to the page's challenge.js and the response's
|
|
621
|
+
* `token` is the aws-waf-token cookie. Exchange them as soon as they arrive
|
|
622
|
+
* rather than storing them for later.
|
|
623
|
+
*/
|
|
624
|
+
async amazonWaf(sitekey, url, options = {}) {
|
|
625
|
+
// An unset optional is dropped rather than sent as undefined; an empty one
|
|
626
|
+
// is dropped during validation, since the server reads '' as "not sent".
|
|
627
|
+
const given = {};
|
|
628
|
+
for (const [key, value] of Object.entries(options)) {
|
|
629
|
+
if (value !== undefined && value !== null) {
|
|
630
|
+
given[key] = value;
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
// A real browser mints the token the voucher is issued for, and the puzzle
|
|
635
|
+
// rounds go through AWS on top of that, so it gets the longer of the two
|
|
636
|
+
// timeouts unless the caller asked for a specific one.
|
|
637
|
+
const result = await this.solve({
|
|
638
|
+
timeout: this.recaptchaTimeout,
|
|
639
|
+
sitekey,
|
|
640
|
+
url,
|
|
641
|
+
...given,
|
|
642
|
+
method: 'amazon_waf',
|
|
643
|
+
poll_json: 1,
|
|
644
|
+
});
|
|
645
|
+
return applyAmazonWafSolution(result);
|
|
646
|
+
}
|
|
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
|
+
|
|
556
703
|
async solve(options = {}) {
|
|
557
704
|
const {
|
|
558
705
|
timeout = 0,
|
|
@@ -653,6 +800,12 @@ class CapSkip {
|
|
|
653
800
|
if (method === 'friendly_captcha') {
|
|
654
801
|
return prepareSubmitParams(params, 'friendly_captcha');
|
|
655
802
|
}
|
|
803
|
+
if (method === 'amazon_waf') {
|
|
804
|
+
return prepareSubmitParams(params, 'amazon_waf');
|
|
805
|
+
}
|
|
806
|
+
if (method === 'datadome') {
|
|
807
|
+
return prepareSubmitParams(params, 'datadome');
|
|
808
|
+
}
|
|
656
809
|
return applyProxy(applyParamAliases(params));
|
|
657
810
|
}
|
|
658
811
|
}
|
|
@@ -667,5 +820,8 @@ module.exports = {
|
|
|
667
820
|
applyGeetestSolution,
|
|
668
821
|
applyAltchaSolution,
|
|
669
822
|
applyCapySolution,
|
|
823
|
+
applyAmazonWafSolution,
|
|
824
|
+
applyDataDomeSolution,
|
|
825
|
+
expandObjectAnswer,
|
|
670
826
|
applyTokenSolution,
|
|
671
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 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;
|
|
@@ -36,14 +36,17 @@ export interface SolveResult {
|
|
|
36
36
|
* The solution: recognized text for images, a token otherwise. For GeeTest
|
|
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
|
-
* base64 token, also exposed as `token`. For Capy it is the raw
|
|
40
|
-
* object, expanded into the
|
|
39
|
+
* base64 token, also exposed as `token`. For Capy and AWS WAF it is the raw
|
|
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. */
|
|
@@ -73,6 +76,26 @@ export interface SolveResult {
|
|
|
73
76
|
* 2Captcha's documented response; it carries nothing.
|
|
74
77
|
*/
|
|
75
78
|
respKey?: string;
|
|
79
|
+
/**
|
|
80
|
+
* AWS WAF only — the voucher AWS issued for the answered puzzle. Only valid
|
|
81
|
+
* together with `existing_token`: POST both as JSON to the `voucher` endpoint
|
|
82
|
+
* beside the page's challenge.js, whose response `token` is the
|
|
83
|
+
* `aws-waf-token` cookie. Exchange immediately.
|
|
84
|
+
*/
|
|
85
|
+
captcha_voucher?: string;
|
|
86
|
+
/** AWS WAF only — the token the voucher was issued for. Pair with `captcha_voucher`. */
|
|
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;
|
|
76
99
|
}
|
|
77
100
|
|
|
78
101
|
/** Extra options for {@link CapSkip.normal}. */
|
|
@@ -239,6 +262,68 @@ export interface FriendlyCaptchaOptions {
|
|
|
239
262
|
[key: string]: unknown;
|
|
240
263
|
}
|
|
241
264
|
|
|
265
|
+
/** Extra options for {@link CapSkip.amazonWaf}. */
|
|
266
|
+
export interface AmazonWafOptions {
|
|
267
|
+
/**
|
|
268
|
+
* Challenge page: `gokuProps.iv`. Send together with `context`, or leave both
|
|
269
|
+
* out and CapSkip reads them from the page at solve time.
|
|
270
|
+
*/
|
|
271
|
+
iv?: string;
|
|
272
|
+
/** Challenge page: `gokuProps.context`. Send together with `iv`. */
|
|
273
|
+
context?: string;
|
|
274
|
+
/** Full https URL of the page's `challenge.js` on an `awswaf.com` host. */
|
|
275
|
+
challenge_script?: string;
|
|
276
|
+
/** Full https URL of the page's `challenge.js` (alias for `challenge_script`). */
|
|
277
|
+
challengeScript?: string;
|
|
278
|
+
/** Full https URL of the page's `captcha.js` on an `awswaf.com` host. */
|
|
279
|
+
captcha_script?: string;
|
|
280
|
+
/** Full https URL of the page's `captcha.js` (alias for `captcha_script`). */
|
|
281
|
+
captchaScript?: string;
|
|
282
|
+
/** Widget page: full https URL of `jsapi.js` on an `awswaf.com` host. */
|
|
283
|
+
jsapiScript?: string;
|
|
284
|
+
/** Widget page: full https URL of `jsapi.js` (alias for `jsapiScript`). */
|
|
285
|
+
jsapi_script?: string;
|
|
286
|
+
/** Poll timeout in seconds. Default: the client's `recaptchaTimeout`. */
|
|
287
|
+
timeout?: number;
|
|
288
|
+
/** `1` to request the raw JSON response from CapSkip. */
|
|
289
|
+
json?: number;
|
|
290
|
+
/** Proxy used for solving. */
|
|
291
|
+
proxy?: Proxy | string;
|
|
292
|
+
/** Proxy type when `proxy` is a bare string. */
|
|
293
|
+
proxytype?: string;
|
|
294
|
+
[key: string]: unknown;
|
|
295
|
+
}
|
|
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
|
+
|
|
242
327
|
/** Options for the {@link CapSkip.solve} manual workflow. */
|
|
243
328
|
export interface SolveOptions {
|
|
244
329
|
/** Poll timeout in seconds (falls back to the configured default). */
|
|
@@ -320,6 +405,37 @@ export class CapSkip {
|
|
|
320
405
|
url: string,
|
|
321
406
|
options?: FriendlyCaptchaOptions,
|
|
322
407
|
): Promise<SolveResult>;
|
|
408
|
+
/**
|
|
409
|
+
* Solve an AWS WAF CAPTCHA.
|
|
410
|
+
*
|
|
411
|
+
* On a challenge page, `sitekey` is `gokuProps.key` and `iv` / `context` are
|
|
412
|
+
* `gokuProps.iv` / `gokuProps.context` (both or neither). On a widget page,
|
|
413
|
+
* `sitekey` is the widget's `apiKey`; pass `jsapiScript`. Script URLs must be
|
|
414
|
+
* https on an `awswaf.com` host. The result carries `captcha_voucher` and
|
|
415
|
+
* `existing_token`, which the site's `voucher` endpoint trades for the
|
|
416
|
+
* `aws-waf-token` cookie.
|
|
417
|
+
*/
|
|
418
|
+
amazonWaf(
|
|
419
|
+
sitekey: string,
|
|
420
|
+
url: string,
|
|
421
|
+
options?: AmazonWafOptions,
|
|
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>;
|
|
323
439
|
/** Submit then poll to completion. Used by the higher-level solve methods. */
|
|
324
440
|
solve(options?: SolveOptions): Promise<SolveResult>;
|
|
325
441
|
/** Submit a captcha without polling; resolves to the captcha id. */
|