Support Proxy + Custom Captcha solver

This commit is contained in:
Elysia 2024-01-12 23:24:43 +07:00
parent e48e9e8dd8
commit 6fbf181d20
6 changed files with 53 additions and 213 deletions

View File

@ -59,7 +59,6 @@
"@sapphire/shapeshift": "^3.9.3", "@sapphire/shapeshift": "^3.9.3",
"@types/node-fetch": "^2.6.7", "@types/node-fetch": "^2.6.7",
"@types/ws": "^8.5.8", "@types/ws": "^8.5.8",
"chalk": "^4.1.2",
"discord-api-types": "^0.37.61", "discord-api-types": "^0.37.61",
"fetch-cookie": "^2.1.0", "fetch-cookie": "^2.1.0",
"form-data": "^4.0.0", "form-data": "^4.0.0",

View File

@ -1,13 +1,14 @@
'use strict'; 'use strict';
const Buffer = require('node:buffer').Buffer; const Buffer = require('node:buffer').Buffer;
const http = require('node:http');
const https = require('node:https'); const https = require('node:https');
const { setTimeout } = require('node:timers'); const { setTimeout } = require('node:timers');
const makeFetchCookie = require('fetch-cookie'); const makeFetchCookie = require('fetch-cookie');
const FormData = require('form-data'); const FormData = require('form-data');
const fetchOriginal = require('node-fetch'); const fetchOriginal = require('node-fetch');
const { CookieJar } = require('tough-cookie'); const { CookieJar } = require('tough-cookie');
const { getProxyObject } = require('../util/Util'); const { UserAgent } = require('../util/Constants');
const cookieJar = new CookieJar(); const cookieJar = new CookieJar();
const fetch = makeFetchCookie(fetchOriginal, cookieJar); const fetch = makeFetchCookie(fetchOriginal, cookieJar);
@ -23,6 +24,9 @@ class APIRequest {
this.options = options; this.options = options;
this.retries = 0; this.retries = 0;
const { userAgentSuffix } = this.client.options;
this.fullUserAgent = `${UserAgent}${userAgentSuffix.length ? `, ${userAgentSuffix.join(', ')}` : ''}`;
let queryString = ''; let queryString = '';
if (options.query) { if (options.query) {
const query = Object.entries(options.query) const query = Object.entries(options.query)
@ -33,13 +37,11 @@ class APIRequest {
this.path = `${path}${queryString && `?${queryString}`}`; this.path = `${path}${queryString && `?${queryString}`}`;
} }
make(captchaKey = undefined, captchaRqtoken = undefined) { make(captchaKey, captchaRqToken) {
if (agent === null) { if (!agent) {
if (typeof this.client.options.proxy === 'string' && this.client.options.proxy.length > 0) { if (this.client.options.http.agent instanceof http.Agent) {
agent = getProxyObject(this.client.options.proxy); this.client.options.http.agent.keepAlive = true;
} else if (this.client.options.http.agent instanceof https.Agent) {
agent = this.client.options.http.agent; agent = this.client.options.http.agent;
agent.keepAlive = true;
} else { } else {
agent = new https.Agent({ ...this.client.options.http.agent, keepAlive: true }); agent = new https.Agent({ ...this.client.options.http.agent, keepAlive: true });
} }
@ -56,7 +58,7 @@ class APIRequest {
authority: 'discord.com', authority: 'discord.com',
accept: '*/*', accept: '*/*',
'accept-language': 'en-US', 'accept-language': 'en-US',
'sec-ch-ua': `"Not?A_Brand";v="8", "Chromium";v="108"`, 'sec-ch-ua': '"Not?A_Brand";v="8", "Chromium";v="108"',
'sec-ch-ua-mobile': '?0', 'sec-ch-ua-mobile': '?0',
'sec-ch-ua-platform': '"Windows"', 'sec-ch-ua-platform': '"Windows"',
'sec-fetch-dest': 'empty', 'sec-fetch-dest': 'empty',
@ -65,18 +67,19 @@ class APIRequest {
'x-debug-options': 'bugReporterEnabled', 'x-debug-options': 'bugReporterEnabled',
'x-discord-locale': 'en-US', 'x-discord-locale': 'en-US',
'x-discord-timezone': 'Asia/Saigon', 'x-discord-timezone': 'Asia/Saigon',
'x-super-properties': `${Buffer.from( 'x-super-properties': `${Buffer.from(JSON.stringify(this.client.options.ws.properties), 'ascii').toString(
this.client.options.jsonTransformer(this.client.options.ws.properties), 'base64',
'ascii', )}`,
).toString('base64')}`,
Referer: 'https://discord.com/channels/@me', Referer: 'https://discord.com/channels/@me',
origin: 'https://discord.com', origin: 'https://discord.com',
'Referrer-Policy': 'strict-origin-when-cross-origin', 'Referrer-Policy': 'strict-origin-when-cross-origin',
'User-Agent': this.fullUserAgent,
}; };
if (this.options.auth !== false) headers.Authorization = this.rest.getAuth(); if (this.options.auth !== false) headers.Authorization = this.rest.getAuth();
if (this.options.reason) headers['X-Audit-Log-Reason'] = encodeURIComponent(this.options.reason); if (this.options.reason) headers['X-Audit-Log-Reason'] = encodeURIComponent(this.options.reason);
if (this.options.headers) headers = Object.assign(headers, this.options.headers); if (this.options.headers) headers = Object.assign(headers, this.options.headers);
// Delete all headers if undefined // Delete all headers if undefined
for (const [key, value] of Object.entries(headers)) { for (const [key, value] of Object.entries(headers)) {
if (value === undefined) delete headers[key]; if (value === undefined) delete headers[key];
@ -86,9 +89,12 @@ class APIRequest {
'User-Agent': this.client.options.http.headers['User-Agent'], 'User-Agent': this.client.options.http.headers['User-Agent'],
}; };
} }
if (captchaKey && typeof captchaKey == 'string') {
headers['x-captcha-key'] = captchaKey; // Some options
if (captchaRqtoken) headers['x-captcha-rqtoken'] = captchaRqtoken; if (this.options.DiscordContext) {
headers['X-Context-Properties'] = Buffer.from(JSON.stringify(this.options.DiscordContext), 'utf8').toString(
'base64',
);
} }
let body; let body;
@ -107,19 +113,21 @@ class APIRequest {
headers = Object.assign(headers, body.getHeaders()); headers = Object.assign(headers, body.getHeaders());
// eslint-disable-next-line eqeqeq // eslint-disable-next-line eqeqeq
} else if (this.options.data != null) { } else if (this.options.data != null) {
if (this.options.useFormDataPayloadJSON) { if (this.options.usePayloadJSON) {
body = new FormData(); body = new FormData();
body.append('payload_json', JSON.stringify(this.options.data)); body.append('payload_json', JSON.stringify(this.options.data));
headers = Object.assign(headers, body.getHeaders());
} else { } else {
body = JSON.stringify(this.options.data); body = JSON.stringify(this.options.data);
headers['Content-Type'] = 'application/json'; headers['Content-Type'] = 'application/json';
} }
} }
// Captcha
if (captchaKey && typeof captchaKey == 'string') headers['X-Captcha-Key'] = captchaKey;
if (captchaRqToken && typeof captchaRqToken == 'string') headers['X-Captcha-Rqtoken'] = captchaRqToken;
const controller = new AbortController(); const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), this.client.options.restRequestTimeout).unref(); const timeout = setTimeout(() => controller.abort(), this.client.options.restRequestTimeout).unref();
return fetch(url, { return fetch(url, {
method: this.method, method: this.method,
headers, headers,

View File

@ -1,139 +0,0 @@
'use strict';
const proxyParser = proxy => {
const protocolSplit = proxy.split('://');
const protocol = protocolSplit.length === 1 ? null : protocolSplit[0];
const rest = protocolSplit.length === 1 ? protocolSplit[0] : protocolSplit[1];
const authSplit = rest.split('@');
if (authSplit.length === 1) {
const host = authSplit[0].split(':')[0];
const port = Number(authSplit[0].split(':')[1]);
const proxyConfig = {
host,
port,
};
if (protocol != null) {
proxyConfig.protocol = protocol;
}
return proxyConfig;
}
const host = authSplit[1].split(':')[0];
const port = Number(authSplit[1].split(':')[1]);
const [username, password] = authSplit[0].split(':');
const proxyConfig = {
host,
port,
auth: {
username,
password,
},
};
if (protocol != null) {
proxyConfig.protocol = protocol;
}
return proxyConfig;
};
module.exports = class CaptchaSolver {
constructor(service, key, defaultCaptchaSolver, proxyString = '') {
this.service = 'custom';
this.solver = undefined;
this.defaultCaptchaSolver = defaultCaptchaSolver;
this.key = null;
this.proxy = proxyString.length ? proxyParser(proxyString) : null;
this._setup(service, key);
}
_missingModule(name) {
return new Error(`${name} module not found, please install it with \`npm i ${name}\``);
}
_setup(service, key) {
switch (service) {
case '2captcha': {
if (!key || typeof key !== 'string') throw new Error('2captcha key is not provided');
try {
const lib = require('2captcha');
this.service = '2captcha';
this.key = key;
this.solver = new lib.Solver(key);
this.solve = (data, userAgent) =>
new Promise((resolve, reject) => {
const siteKey = data.captcha_sitekey;
let postD = {
invisible: 1,
userAgent,
};
if (this.proxy !== null) {
postD = {
...postD,
proxytype: this.proxy.protocol?.toUpperCase(),
proxy: `${'auth' in this.proxy ? `${this.proxy.auth.username}:${this.proxy.auth.password}@` : ''}${
this.proxy.host
}:${this.proxy.port}`,
};
}
if (data.captcha_rqdata) {
postD = {
...postD,
data: data.captcha_rqdata,
};
}
this.solver
.hcaptcha(siteKey, 'discord.com', postD)
.then(res => {
if (typeof res.data == 'string') {
resolve(res.data);
} else {
reject(new Error('Unknown Response'));
}
})
.catch(reject);
});
break;
} catch (e) {
throw this._missingModule('2captcha');
}
}
case 'capmonster': {
if (!key || typeof key !== 'string') throw new Error('Capmonster key is not provided');
try {
const { HCaptchaTask } = require('node-capmonster');
this.service = 'capmonster';
this.key = key;
const client = new HCaptchaTask(this.key);
this.solve = (captchaData, userAgent) =>
new Promise((resolve, reject) => {
if (userAgent) client.setUserAgent(userAgent);
if (this.proxy !== null) {
client.setGlobalProxy(
this.proxy.protocol,
this.proxy.host,
this.proxy.port,
'auth' in this.proxy ? this.proxy.auth.username : undefined,
'auth' in this.proxy ? this.proxy.auth.password : undefined,
);
}
client
.createWithTask(
client.task({
websiteURL: 'https://discord.com/channels/@me',
websiteKey: captchaData.captcha_sitekey,
isInvisible: !!captchaData.captcha_rqdata,
data: captchaData.captcha_rqdata,
}),
)
.then(id => client.joinTaskResult(id))
.then(result => resolve(result.gRecaptchaResponse))
.catch(reject);
});
} catch (e) {
throw this._missingModule('node-capmonster');
}
break;
}
default: {
this.solve = this.defaultCaptchaSolver;
}
}
}
solve() {}
};

View File

@ -25,7 +25,7 @@ class DiscordAPIError extends Error {
/** /**
* HTTP error code returned by Discord * HTTP error code returned by Discord
* @type {number | string} * @type {number}
*/ */
this.code = error.code; this.code = error.code;
@ -35,21 +35,6 @@ class DiscordAPIError extends Error {
*/ */
this.httpStatus = status; this.httpStatus = status;
/**
* @typedef {Object} Captcha
* @property {Array<string>} captcha_key ['message']
* @property {string} captcha_sitekey Captcha sitekey (hcaptcha)
* @property {string} captcha_service hcaptcha
* @property {string} [captcha_rqdata]
* @property {string} [captcha_rqtoken]
*/
/**
* Captcha response data if the request requires a captcha
* @type {Captcha | null}
*/
this.captcha = error?.captcha_service ? error : null;
/** /**
* The data associated with the request that caused this error * The data associated with the request that caused this error
* @type {HTTPErrorData} * @type {HTTPErrorData}
@ -64,6 +49,21 @@ class DiscordAPIError extends Error {
* @type {number} * @type {number}
*/ */
this.retries = request.retries; this.retries = request.retries;
/**
* @typedef {Object} Captcha
* @property {Array<string>} captcha_key ['message']
* @property {string} captcha_sitekey Captcha sitekey (hcaptcha)
* @property {string} captcha_service hcaptcha
* @property {string} [captcha_rqdata]
* @property {string} [captcha_rqtoken]
*/
/**
* Captcha response data if the request requires a captcha
* @type {Captcha | null}
*/
this.captcha = error?.captcha_service ? error : null;
} }
/** /**

View File

@ -4,7 +4,6 @@ const { setInterval } = require('node:timers');
const { Collection } = require('@discordjs/collection'); const { Collection } = require('@discordjs/collection');
const APIRequest = require('./APIRequest'); const APIRequest = require('./APIRequest');
const routeBuilder = require('./APIRouter'); const routeBuilder = require('./APIRouter');
const CaptchaSolver = require('./CaptchaSolver');
const RequestHandler = require('./RequestHandler'); const RequestHandler = require('./RequestHandler');
const { Error } = require('../errors'); const { Error } = require('../errors');
const { Endpoints } = require('../util/Constants'); const { Endpoints } = require('../util/Constants');
@ -23,17 +22,6 @@ class RESTManager {
this.handlers.sweep(handler => handler._inactive); this.handlers.sweep(handler => handler._inactive);
}, client.options.restSweepInterval * 1_000).unref(); }, client.options.restSweepInterval * 1_000).unref();
} }
this.captchaService = null;
this.setup();
}
setup() {
this.captchaService = new CaptchaSolver(
this.client.options.captchaService,
this.client.options.captchaKey,
this.client.options.captchaSolver,
this.client.options.captchaWithProxy ? this.client.options.proxy : '',
);
} }
get api() { get api() {
@ -41,16 +29,8 @@ class RESTManager {
} }
getAuth() { getAuth() {
if ((this.client.token && this.client.user && this.client.user.bot) || this.client.accessToken) {
return `Bot ${this.client.token}`;
} else if (this.client.token) {
return this.client.token;
}
/*
// v13.7
const token = this.client.token ?? this.client.accessToken; const token = this.client.token ?? this.client.accessToken;
if (token) return `Bot ${token}`; if (token) return token?.replace(/Bot /g, '');
*/
throw new Error('TOKEN_MISSING'); throw new Error('TOKEN_MISSING');
} }

View File

@ -2,13 +2,12 @@
const { setTimeout } = require('node:timers'); const { setTimeout } = require('node:timers');
const { setTimeout: sleep } = require('node:timers/promises'); const { setTimeout: sleep } = require('node:timers/promises');
const { inspect } = require('util');
const { AsyncQueue } = require('@sapphire/async-queue'); const { AsyncQueue } = require('@sapphire/async-queue');
const DiscordAPIError = require('./DiscordAPIError'); const DiscordAPIError = require('./DiscordAPIError');
const HTTPError = require('./HTTPError'); const HTTPError = require('./HTTPError');
const RateLimitError = require('./RateLimitError'); const RateLimitError = require('./RateLimitError');
const { const {
Events: { DEBUG, RATE_LIMIT, INVALID_REQUEST_WARNING, API_RESPONSE, API_REQUEST, CAPTCHA_REQUIRED }, Events: { DEBUG, RATE_LIMIT, INVALID_REQUEST_WARNING, API_RESPONSE, API_REQUEST },
} = require('../util/Constants'); } = require('../util/Constants');
const captchaMessage = [ const captchaMessage = [
@ -23,7 +22,7 @@ const captchaMessage = [
function parseResponse(res) { function parseResponse(res) {
if (res.headers.get('content-type')?.startsWith('application/json')) return res.json(); if (res.headers.get('content-type')?.startsWith('application/json')) return res.json();
return res.arrayBuffer(); // Cre: TheDevYellowy return res.arrayBuffer();
} }
function getAPIOffset(serverDate) { function getAPIOffset(serverDate) {
@ -354,18 +353,9 @@ class RequestHandler {
let data; let data;
try { try {
data = await parseResponse(res); data = await parseResponse(res);
if (data?.captcha_service) {
/**
* Emitted when a request is blocked by a captcha
* @event Client#captchaRequired
* @param {Request} request The request that was blocked
* @param {Captcha} data The data returned by Discord
*/
this.manager.client.emit(CAPTCHA_REQUIRED, request, data);
}
if ( if (
data?.captcha_service && data?.captcha_service &&
this.manager.client.options.captchaService && typeof this.manager.client.options.captchaSolver == 'function' &&
request.retries < this.manager.client.options.captchaRetryLimit && request.retries < this.manager.client.options.captchaRetryLimit &&
captchaMessage.some(s => data.captcha_key[0].includes(s)) captchaMessage.some(s => data.captcha_key[0].includes(s))
) { ) {
@ -376,13 +366,14 @@ class RequestHandler {
Method : ${request.method} Method : ${request.method}
Path : ${request.path} Path : ${request.path}
Route : ${request.route} Route : ${request.route}
Info : ${inspect(data, { depth: null })}`, Sitekey : ${data.captcha_sitekey}
rqToken : ${data.captcha_rqtoken}`,
); );
const captcha = await this.manager.captchaService.solve( const captcha = await this.manager.client.options.captchaSolver(
data, data.captcha_sitekey,
this.manager.client.options.http.headers['User-Agent'], request.fullUserAgent,
data.captcha_rqtoken,
); );
// Sleep: await this.manager.client.sleep(5_000);
this.manager.client.emit( this.manager.client.emit(
DEBUG, DEBUG,
`Captcha details: `Captcha details:
@ -398,6 +389,7 @@ class RequestHandler {
} catch (err) { } catch (err) {
throw new HTTPError(err.message, err.constructor.name, err.status, request); throw new HTTPError(err.message, err.constructor.name, err.status, request);
} }
throw new DiscordAPIError(data, res.status, request); throw new DiscordAPIError(data, res.status, request);
} }