capskip 1.0.2
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 +55 -0
- package/LICENSE +21 -0
- package/README.md +274 -0
- package/SECURITY.md +35 -0
- package/docs/API_REFERENCE.md +292 -0
- package/docs/GETTING_STARTED.md +167 -0
- package/docs/TROUBLESHOOTING.md +206 -0
- package/docs/TUTORIAL.md +504 -0
- package/examples/async_example.js +34 -0
- package/examples/captcha.png +0 -0
- package/examples/image_captcha.js +20 -0
- package/examples/recaptcha.js +19 -0
- package/examples/turnstile.js +20 -0
- package/examples/verify_connection.js +50 -0
- package/package.json +50 -0
- package/src/apiClient.js +96 -0
- package/src/apiParams.js +175 -0
- package/src/exceptions.js +37 -0
- package/src/http.js +158 -0
- package/src/index.js +31 -0
- package/src/solver.js +266 -0
- package/types/index.d.ts +167 -0
package/src/solver.js
ADDED
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fsSync = require('fs');
|
|
4
|
+
|
|
5
|
+
const { ApiClient } = require('./apiClient');
|
|
6
|
+
const { request } = require('./http');
|
|
7
|
+
const {
|
|
8
|
+
applyParamAliases,
|
|
9
|
+
applyProxy,
|
|
10
|
+
prepareSubmitParams,
|
|
11
|
+
} = require('./apiParams');
|
|
12
|
+
const {
|
|
13
|
+
CapSkipError,
|
|
14
|
+
ApiException,
|
|
15
|
+
NetworkException,
|
|
16
|
+
TimeoutException,
|
|
17
|
+
ValidationException,
|
|
18
|
+
} = require('./exceptions');
|
|
19
|
+
|
|
20
|
+
// First poll fires this soon after submitting (in seconds), then the interval
|
|
21
|
+
// backs off (doubling) up to the configured pollingInterval ceiling. Keeps
|
|
22
|
+
// latency low for fast local solves (e.g. image captchas) without hammering on
|
|
23
|
+
// slow ones.
|
|
24
|
+
const INITIAL_POLLING_INTERVAL = 0.25;
|
|
25
|
+
|
|
26
|
+
function sleep(seconds) {
|
|
27
|
+
return new Promise((resolve) => { setTimeout(resolve, seconds * 1000); });
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function nextPollInterval(interval, ceiling) {
|
|
31
|
+
return Math.min(interval * 2, ceiling);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function reprList(values) {
|
|
35
|
+
return `[${values.map((value) => `'${value}'`).join(', ')}]`;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function parsePollResponse(response, jsonMode = 0) {
|
|
39
|
+
const text = (response || '').trim();
|
|
40
|
+
|
|
41
|
+
// CapSkip returns an empty body whenever no result is available yet: briefly
|
|
42
|
+
// right after submit (before it starts reporting CAPCHA_NOT_READY), for an
|
|
43
|
+
// unknown id, and after a solved token has already been read once. Treat it
|
|
44
|
+
// like CAPCHA_NOT_READY so the caller keeps polling instead of failing.
|
|
45
|
+
if (!text) {
|
|
46
|
+
throw new NetworkException();
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (jsonMode) {
|
|
50
|
+
let data;
|
|
51
|
+
try {
|
|
52
|
+
data = JSON.parse(text);
|
|
53
|
+
} catch (err) {
|
|
54
|
+
throw new ApiException(`invalid JSON response: ${response}`);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (data.status === 0 && data.request === 'CAPCHA_NOT_READY') {
|
|
58
|
+
throw new NetworkException();
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (data.status !== 1) {
|
|
62
|
+
throw new ApiException(`cannot recognize response ${JSON.stringify(data)}`);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return data;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (text === 'CAPCHA_NOT_READY') {
|
|
69
|
+
throw new NetworkException();
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (!text.startsWith('OK|')) {
|
|
73
|
+
throw new ApiException(`cannot recognize response ${response}`);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return text.slice(3);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function applyPollResult(result, polled) {
|
|
80
|
+
if (polled !== null && typeof polled === 'object') {
|
|
81
|
+
result.code = polled.request !== undefined ? polled.request : '';
|
|
82
|
+
const userAgent = polled.useragent || polled.userAgent;
|
|
83
|
+
if (userAgent) {
|
|
84
|
+
result.userAgent = userAgent;
|
|
85
|
+
}
|
|
86
|
+
} else {
|
|
87
|
+
result.code = polled;
|
|
88
|
+
}
|
|
89
|
+
return result;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// CapSkip's in.php returns OK|<id> by default, or {"status":1,"request":"<id>"}
|
|
93
|
+
// when the submit carried json=1. Accept both so submitting with json=1 works.
|
|
94
|
+
function parseSubmitResponse(response) {
|
|
95
|
+
const text = (response || '').trim();
|
|
96
|
+
|
|
97
|
+
if (text.startsWith('OK|')) {
|
|
98
|
+
return text.slice(3);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
let data;
|
|
102
|
+
try {
|
|
103
|
+
data = JSON.parse(text);
|
|
104
|
+
} catch (err) {
|
|
105
|
+
data = null;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (data !== null && typeof data === 'object' && data.status === 1 && data.request !== undefined) {
|
|
109
|
+
return String(data.request);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
throw new ApiException(`cannot recognize response ${response}`);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** Client for the CapSkip local captcha solver (image, reCAPTCHA, Turnstile). */
|
|
116
|
+
class CapSkip {
|
|
117
|
+
constructor({
|
|
118
|
+
apiKey = 'capskip',
|
|
119
|
+
host = '127.0.0.1',
|
|
120
|
+
port = 8080,
|
|
121
|
+
defaultTimeout = 120,
|
|
122
|
+
recaptchaTimeout = 300,
|
|
123
|
+
pollingInterval = 5,
|
|
124
|
+
} = {}) {
|
|
125
|
+
this.apiKey = apiKey;
|
|
126
|
+
this.defaultTimeout = defaultTimeout;
|
|
127
|
+
this.recaptchaTimeout = recaptchaTimeout;
|
|
128
|
+
this.pollingInterval = pollingInterval;
|
|
129
|
+
this.apiClient = new ApiClient({ host, port });
|
|
130
|
+
this.exceptions = CapSkipError;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
async normal(file, options = {}) {
|
|
134
|
+
const unsupported = Object.keys(options).filter((key) => key !== 'json').sort();
|
|
135
|
+
if (unsupported.length > 0) {
|
|
136
|
+
throw new ValidationException(
|
|
137
|
+
`Unsupported parameters for image captcha: ${reprList(unsupported)}. `
|
|
138
|
+
+ 'Only json is supported besides the image input.',
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
const method = await this.getMethod(file);
|
|
142
|
+
return this.solve({ ...method, ...options });
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
async recaptcha(sitekey, url, options = {}) {
|
|
146
|
+
const { version = 'v2', enterprise = 0, ...rest } = options;
|
|
147
|
+
const params = {
|
|
148
|
+
googlekey: sitekey,
|
|
149
|
+
url,
|
|
150
|
+
method: 'userrecaptcha',
|
|
151
|
+
enterprise,
|
|
152
|
+
...rest,
|
|
153
|
+
};
|
|
154
|
+
if (String(version).toLowerCase() === 'v3') {
|
|
155
|
+
params.version = 'v3';
|
|
156
|
+
}
|
|
157
|
+
return this.solve({ timeout: this.recaptchaTimeout, ...params });
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
async turnstile(sitekey, url, options = {}) {
|
|
161
|
+
return this.solve({
|
|
162
|
+
sitekey,
|
|
163
|
+
url,
|
|
164
|
+
...options,
|
|
165
|
+
method: 'turnstile',
|
|
166
|
+
poll_json: 1,
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
async solve(options = {}) {
|
|
171
|
+
const {
|
|
172
|
+
timeout = 0,
|
|
173
|
+
polling_interval: pollingInterval = 0,
|
|
174
|
+
poll_json: pollJson = 0,
|
|
175
|
+
...params
|
|
176
|
+
} = options;
|
|
177
|
+
|
|
178
|
+
const useJson = Number(pollJson) || 0;
|
|
179
|
+
const captchaId = await this.send(params);
|
|
180
|
+
const result = { captchaId };
|
|
181
|
+
const solveTimeout = timeout || this.defaultTimeout;
|
|
182
|
+
const interval = pollingInterval || this.pollingInterval;
|
|
183
|
+
const polled = await this.waitResult(captchaId, solveTimeout, interval, useJson);
|
|
184
|
+
return applyPollResult(result, polled);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
async waitResult(id, timeout, pollingInterval, json = 0) {
|
|
188
|
+
const deadline = Date.now() + timeout * 1000;
|
|
189
|
+
let interval = Math.min(INITIAL_POLLING_INTERVAL, pollingInterval);
|
|
190
|
+
while (Date.now() < deadline) {
|
|
191
|
+
try {
|
|
192
|
+
return await this.getResult(id, json);
|
|
193
|
+
} catch (err) {
|
|
194
|
+
if (!(err instanceof NetworkException)) {
|
|
195
|
+
throw err;
|
|
196
|
+
}
|
|
197
|
+
await sleep(interval);
|
|
198
|
+
interval = nextPollInterval(interval, pollingInterval);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
throw new TimeoutException(`timeout ${timeout} exceeded`);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
async getMethod(file) {
|
|
205
|
+
if (!file) {
|
|
206
|
+
throw new ValidationException('File required');
|
|
207
|
+
}
|
|
208
|
+
if (file.startsWith('data:')) {
|
|
209
|
+
return { method: 'base64', body: file.slice(file.indexOf(',') + 1) };
|
|
210
|
+
}
|
|
211
|
+
if (!file.includes('.') && file.length > 50) {
|
|
212
|
+
return { method: 'base64', body: file };
|
|
213
|
+
}
|
|
214
|
+
if (file.startsWith('http')) {
|
|
215
|
+
const resp = await request('GET', file);
|
|
216
|
+
if (resp.statusCode !== 200) {
|
|
217
|
+
throw new ValidationException(`File could not be downloaded from url: ${file}`);
|
|
218
|
+
}
|
|
219
|
+
return { method: 'base64', body: resp.body.toString('base64') };
|
|
220
|
+
}
|
|
221
|
+
if (!fsSync.existsSync(file)) {
|
|
222
|
+
throw new ValidationException(`File not found: ${file}`);
|
|
223
|
+
}
|
|
224
|
+
return { method: 'post', file };
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
async send(params = {}) {
|
|
228
|
+
const prepared = this._prepareSendParams({ ...params, key: this.apiKey });
|
|
229
|
+
const files = prepared.files || {};
|
|
230
|
+
delete prepared.files;
|
|
231
|
+
const response = await this.apiClient.in_({ files, ...prepared });
|
|
232
|
+
return parseSubmitResponse(response);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
async getResult(id, json = 0) {
|
|
236
|
+
const query = { key: this.apiKey, action: 'get', id };
|
|
237
|
+
if (json) {
|
|
238
|
+
query.json = 1;
|
|
239
|
+
}
|
|
240
|
+
const response = await this.apiClient.res(query);
|
|
241
|
+
return parsePollResponse(response, json ? 1 : 0);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
_prepareSendParams(params) {
|
|
245
|
+
const { method } = params;
|
|
246
|
+
if (method === 'post' || method === 'base64') {
|
|
247
|
+
return prepareSubmitParams(params, 'normal');
|
|
248
|
+
}
|
|
249
|
+
if (method === 'userrecaptcha') {
|
|
250
|
+
return prepareSubmitParams(params, 'recaptcha', params.version || 'v2');
|
|
251
|
+
}
|
|
252
|
+
if (method === 'turnstile') {
|
|
253
|
+
return prepareSubmitParams(params, 'turnstile');
|
|
254
|
+
}
|
|
255
|
+
return applyProxy(applyParamAliases(params));
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
module.exports = {
|
|
260
|
+
CapSkip,
|
|
261
|
+
INITIAL_POLLING_INTERVAL,
|
|
262
|
+
nextPollInterval,
|
|
263
|
+
parsePollResponse,
|
|
264
|
+
parseSubmitResponse,
|
|
265
|
+
applyPollResult,
|
|
266
|
+
};
|
package/types/index.d.ts
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
// Type definitions for the CapSkip Node.js SDK.
|
|
2
|
+
|
|
3
|
+
/** Proxy passed to reCAPTCHA / Turnstile solves. */
|
|
4
|
+
export interface Proxy {
|
|
5
|
+
/** Proxy type: `HTTP`, `HTTPS`, `SOCKS5`, or `SOCKS5H`. */
|
|
6
|
+
type: string;
|
|
7
|
+
/** Proxy address: `login:password@host:port` or bare `host:port`. */
|
|
8
|
+
uri: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/** Options accepted by the {@link CapSkip} constructor. */
|
|
12
|
+
export interface CapSkipOptions {
|
|
13
|
+
/** CapSkip API key (any string when key validation is disabled). Default `"capskip"`. */
|
|
14
|
+
apiKey?: string;
|
|
15
|
+
/** CapSkip host. Default `"127.0.0.1"`. */
|
|
16
|
+
host?: string;
|
|
17
|
+
/** CapSkip port. Default `8080`. */
|
|
18
|
+
port?: number;
|
|
19
|
+
/** Seconds to poll an image captcha before timing out. Default `120`. */
|
|
20
|
+
defaultTimeout?: number;
|
|
21
|
+
/** Seconds to poll reCAPTCHA / Turnstile before timing out. Default `300`. */
|
|
22
|
+
recaptchaTimeout?: number;
|
|
23
|
+
/** Max seconds between polls; starts at `0.25` and backs off to this. Default `5`. */
|
|
24
|
+
pollingInterval?: number;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** The dictionary every solve method resolves to. */
|
|
28
|
+
export interface SolveResult {
|
|
29
|
+
/** CapSkip's internal id for this solve. */
|
|
30
|
+
captchaId: string;
|
|
31
|
+
/** The solution: recognized text for images, a token otherwise. */
|
|
32
|
+
code: string;
|
|
33
|
+
/** Turnstile only — the User-Agent to use when submitting the token. */
|
|
34
|
+
userAgent?: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Extra options for {@link CapSkip.normal}. */
|
|
38
|
+
export interface NormalOptions {
|
|
39
|
+
/** `1` to request the raw JSON response from CapSkip. */
|
|
40
|
+
json?: number;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Extra options for {@link CapSkip.recaptcha}. */
|
|
44
|
+
export interface RecaptchaOptions {
|
|
45
|
+
/** `"v2"` (default) or `"v3"`. */
|
|
46
|
+
version?: string;
|
|
47
|
+
/** `1` for reCAPTCHA Enterprise. Default `0`. */
|
|
48
|
+
enterprise?: number;
|
|
49
|
+
/** `1` for invisible reCAPTCHA v2. */
|
|
50
|
+
invisible?: number;
|
|
51
|
+
/** v3 action from `grecaptcha.execute()`. */
|
|
52
|
+
action?: string;
|
|
53
|
+
/** v3 minimum acceptable score (alias for `min_score`). */
|
|
54
|
+
score?: number;
|
|
55
|
+
/** v3 minimum acceptable score (alias for `min_score`). */
|
|
56
|
+
minScore?: number;
|
|
57
|
+
/** v3 minimum acceptable score. */
|
|
58
|
+
min_score?: number;
|
|
59
|
+
/** Google Search / services `data-s` value (alias for `data-s`). */
|
|
60
|
+
datas?: string;
|
|
61
|
+
/** Google Search / services `data-s` value (alias for `data-s`). */
|
|
62
|
+
data_s?: string;
|
|
63
|
+
/** `1` to request the raw JSON response from CapSkip. */
|
|
64
|
+
json?: number;
|
|
65
|
+
/** Proxy used for solving. */
|
|
66
|
+
proxy?: Proxy | string;
|
|
67
|
+
/** Proxy type when `proxy` is a bare string. */
|
|
68
|
+
proxytype?: string;
|
|
69
|
+
[key: string]: unknown;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Extra options for {@link CapSkip.turnstile}. */
|
|
73
|
+
export interface TurnstileOptions {
|
|
74
|
+
/** Action from `data-action` or `turnstile.render()`. */
|
|
75
|
+
action?: string;
|
|
76
|
+
/** `cData` / `data-cdata` value for challenge pages. */
|
|
77
|
+
data?: string;
|
|
78
|
+
/** `chlPageData` value for challenge pages. */
|
|
79
|
+
pagedata?: string;
|
|
80
|
+
/** Proxy used for solving. */
|
|
81
|
+
proxy?: Proxy | string;
|
|
82
|
+
/** Proxy type when `proxy` is a bare string. */
|
|
83
|
+
proxytype?: string;
|
|
84
|
+
[key: string]: unknown;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Options for the {@link CapSkip.solve} manual workflow. */
|
|
88
|
+
export interface SolveOptions {
|
|
89
|
+
/** Poll timeout in seconds (falls back to the configured default). */
|
|
90
|
+
timeout?: number;
|
|
91
|
+
/** Max seconds between polls (falls back to the configured default). */
|
|
92
|
+
polling_interval?: number;
|
|
93
|
+
/** `1` to poll with `json=1`. */
|
|
94
|
+
poll_json?: number;
|
|
95
|
+
[key: string]: unknown;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** Client for the CapSkip local captcha solver. */
|
|
99
|
+
export class CapSkip {
|
|
100
|
+
constructor(options?: CapSkipOptions);
|
|
101
|
+
apiKey: string;
|
|
102
|
+
defaultTimeout: number;
|
|
103
|
+
recaptchaTimeout: number;
|
|
104
|
+
pollingInterval: number;
|
|
105
|
+
apiClient: ApiClient;
|
|
106
|
+
exceptions: typeof CapSkipError;
|
|
107
|
+
|
|
108
|
+
/** Solve an image captcha from a file path, URL, base64 string, or data-URI. */
|
|
109
|
+
normal(file: string, options?: NormalOptions): Promise<SolveResult>;
|
|
110
|
+
/** Solve reCAPTCHA v2/v3 (invisible, enterprise, proxy). */
|
|
111
|
+
recaptcha(sitekey: string, url: string, options?: RecaptchaOptions): Promise<SolveResult>;
|
|
112
|
+
/** Solve Cloudflare Turnstile (widget or challenge page). */
|
|
113
|
+
turnstile(sitekey: string, url: string, options?: TurnstileOptions): Promise<SolveResult>;
|
|
114
|
+
/** Submit then poll to completion. Used by the higher-level solve methods. */
|
|
115
|
+
solve(options?: SolveOptions): Promise<SolveResult>;
|
|
116
|
+
/** Submit a captcha without polling; resolves to the captcha id. */
|
|
117
|
+
send(params?: Record<string, unknown>): Promise<string>;
|
|
118
|
+
/** Poll a result once; rejects with `NetworkException` while not ready. */
|
|
119
|
+
getResult(id: string, json?: number): Promise<string | Record<string, unknown>>;
|
|
120
|
+
/** Poll until solved or the timeout (seconds) elapses. */
|
|
121
|
+
waitResult(
|
|
122
|
+
id: string,
|
|
123
|
+
timeout: number,
|
|
124
|
+
pollingInterval: number,
|
|
125
|
+
json?: number,
|
|
126
|
+
): Promise<string | Record<string, unknown>>;
|
|
127
|
+
/** Resolve an image input into the `in.php` `method`/`body`/`file` fields. */
|
|
128
|
+
getMethod(file: string): Promise<Record<string, string>>;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** Alias of {@link CapSkip}; Node is async-native, so there is no separate sync client. */
|
|
132
|
+
export const AsyncCapSkip: typeof CapSkip;
|
|
133
|
+
|
|
134
|
+
/** Options accepted by the {@link ApiClient} constructor. */
|
|
135
|
+
export interface ApiClientOptions {
|
|
136
|
+
host?: string;
|
|
137
|
+
port?: number;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** Low-level HTTP client for the CapSkip in.php / res.php endpoints. */
|
|
141
|
+
export class ApiClient {
|
|
142
|
+
constructor(options?: ApiClientOptions);
|
|
143
|
+
host: string;
|
|
144
|
+
port: number;
|
|
145
|
+
readonly baseUrl: string;
|
|
146
|
+
in_(options?: Record<string, unknown>): Promise<string>;
|
|
147
|
+
res(params?: Record<string, unknown>): Promise<string>;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** Alias of {@link ApiClient}. */
|
|
151
|
+
export const AsyncApiClient: typeof ApiClient;
|
|
152
|
+
|
|
153
|
+
/** Base error for all CapSkip SDK failures. */
|
|
154
|
+
export class CapSkipError extends Error {}
|
|
155
|
+
/** Alias of {@link CapSkipError}. */
|
|
156
|
+
export const SolverExceptions: typeof CapSkipError;
|
|
157
|
+
/** Invalid or unsupported parameters. */
|
|
158
|
+
export class ValidationException extends CapSkipError {}
|
|
159
|
+
/** Connection failure or captcha not ready. */
|
|
160
|
+
export class NetworkException extends CapSkipError {}
|
|
161
|
+
/** CapSkip API returned an error. */
|
|
162
|
+
export class ApiException extends CapSkipError {}
|
|
163
|
+
/** Polling exceeded the configured timeout. */
|
|
164
|
+
export class TimeoutException extends CapSkipError {}
|
|
165
|
+
|
|
166
|
+
/** Installed SDK version. */
|
|
167
|
+
export const version: string;
|