From 6fbf181d206937547c92a0c20731398f15c2cefa Mon Sep 17 00:00:00 2001 From: Elysia <71698422+aiko-chan-ai@users.noreply.github.com> Date: Fri, 12 Jan 2024 23:24:43 +0700 Subject: [PATCH] Support Proxy + Custom Captcha solver --- package.json | 1 - src/rest/APIRequest.js | 44 +++++++----- src/rest/CaptchaSolver.js | 139 ------------------------------------ src/rest/DiscordAPIError.js | 32 ++++----- src/rest/RESTManager.js | 22 +----- src/rest/RequestHandler.js | 28 +++----- 6 files changed, 53 insertions(+), 213 deletions(-) delete mode 100644 src/rest/CaptchaSolver.js diff --git a/package.json b/package.json index b1c9830..cf1bbc7 100644 --- a/package.json +++ b/package.json @@ -59,7 +59,6 @@ "@sapphire/shapeshift": "^3.9.3", "@types/node-fetch": "^2.6.7", "@types/ws": "^8.5.8", - "chalk": "^4.1.2", "discord-api-types": "^0.37.61", "fetch-cookie": "^2.1.0", "form-data": "^4.0.0", diff --git a/src/rest/APIRequest.js b/src/rest/APIRequest.js index 3edebb8..b186b80 100644 --- a/src/rest/APIRequest.js +++ b/src/rest/APIRequest.js @@ -1,13 +1,14 @@ 'use strict'; const Buffer = require('node:buffer').Buffer; +const http = require('node:http'); const https = require('node:https'); const { setTimeout } = require('node:timers'); const makeFetchCookie = require('fetch-cookie'); const FormData = require('form-data'); const fetchOriginal = require('node-fetch'); const { CookieJar } = require('tough-cookie'); -const { getProxyObject } = require('../util/Util'); +const { UserAgent } = require('../util/Constants'); const cookieJar = new CookieJar(); const fetch = makeFetchCookie(fetchOriginal, cookieJar); @@ -23,6 +24,9 @@ class APIRequest { this.options = options; this.retries = 0; + const { userAgentSuffix } = this.client.options; + this.fullUserAgent = `${UserAgent}${userAgentSuffix.length ? `, ${userAgentSuffix.join(', ')}` : ''}`; + let queryString = ''; if (options.query) { const query = Object.entries(options.query) @@ -33,13 +37,11 @@ class APIRequest { this.path = `${path}${queryString && `?${queryString}`}`; } - make(captchaKey = undefined, captchaRqtoken = undefined) { - if (agent === null) { - if (typeof this.client.options.proxy === 'string' && this.client.options.proxy.length > 0) { - agent = getProxyObject(this.client.options.proxy); - } else if (this.client.options.http.agent instanceof https.Agent) { + make(captchaKey, captchaRqToken) { + if (!agent) { + if (this.client.options.http.agent instanceof http.Agent) { + this.client.options.http.agent.keepAlive = true; agent = this.client.options.http.agent; - agent.keepAlive = true; } else { agent = new https.Agent({ ...this.client.options.http.agent, keepAlive: true }); } @@ -56,7 +58,7 @@ class APIRequest { authority: 'discord.com', accept: '*/*', '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-platform': '"Windows"', 'sec-fetch-dest': 'empty', @@ -65,18 +67,19 @@ class APIRequest { 'x-debug-options': 'bugReporterEnabled', 'x-discord-locale': 'en-US', 'x-discord-timezone': 'Asia/Saigon', - 'x-super-properties': `${Buffer.from( - this.client.options.jsonTransformer(this.client.options.ws.properties), - 'ascii', - ).toString('base64')}`, + 'x-super-properties': `${Buffer.from(JSON.stringify(this.client.options.ws.properties), 'ascii').toString( + 'base64', + )}`, Referer: 'https://discord.com/channels/@me', origin: 'https://discord.com', 'Referrer-Policy': 'strict-origin-when-cross-origin', + 'User-Agent': this.fullUserAgent, }; 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.headers) headers = Object.assign(headers, this.options.headers); + // Delete all headers if undefined for (const [key, value] of Object.entries(headers)) { if (value === undefined) delete headers[key]; @@ -86,9 +89,12 @@ class APIRequest { 'User-Agent': this.client.options.http.headers['User-Agent'], }; } - if (captchaKey && typeof captchaKey == 'string') { - headers['x-captcha-key'] = captchaKey; - if (captchaRqtoken) headers['x-captcha-rqtoken'] = captchaRqtoken; + + // Some options + if (this.options.DiscordContext) { + headers['X-Context-Properties'] = Buffer.from(JSON.stringify(this.options.DiscordContext), 'utf8').toString( + 'base64', + ); } let body; @@ -107,19 +113,21 @@ class APIRequest { headers = Object.assign(headers, body.getHeaders()); // eslint-disable-next-line eqeqeq } else if (this.options.data != null) { - if (this.options.useFormDataPayloadJSON) { + if (this.options.usePayloadJSON) { body = new FormData(); body.append('payload_json', JSON.stringify(this.options.data)); - headers = Object.assign(headers, body.getHeaders()); } else { body = JSON.stringify(this.options.data); 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 timeout = setTimeout(() => controller.abort(), this.client.options.restRequestTimeout).unref(); - return fetch(url, { method: this.method, headers, diff --git a/src/rest/CaptchaSolver.js b/src/rest/CaptchaSolver.js deleted file mode 100644 index 9aae32a..00000000 --- a/src/rest/CaptchaSolver.js +++ /dev/null @@ -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() {} -}; diff --git a/src/rest/DiscordAPIError.js b/src/rest/DiscordAPIError.js index 0e5ff06..f5c6f0c 100644 --- a/src/rest/DiscordAPIError.js +++ b/src/rest/DiscordAPIError.js @@ -25,7 +25,7 @@ class DiscordAPIError extends Error { /** * HTTP error code returned by Discord - * @type {number | string} + * @type {number} */ this.code = error.code; @@ -35,21 +35,6 @@ class DiscordAPIError extends Error { */ this.httpStatus = status; - /** - * @typedef {Object} Captcha - * @property {Array} 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 * @type {HTTPErrorData} @@ -64,6 +49,21 @@ class DiscordAPIError extends Error { * @type {number} */ this.retries = request.retries; + + /** + * @typedef {Object} Captcha + * @property {Array} 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; } /** diff --git a/src/rest/RESTManager.js b/src/rest/RESTManager.js index 87450d6..1f4df50 100644 --- a/src/rest/RESTManager.js +++ b/src/rest/RESTManager.js @@ -4,7 +4,6 @@ const { setInterval } = require('node:timers'); const { Collection } = require('@discordjs/collection'); const APIRequest = require('./APIRequest'); const routeBuilder = require('./APIRouter'); -const CaptchaSolver = require('./CaptchaSolver'); const RequestHandler = require('./RequestHandler'); const { Error } = require('../errors'); const { Endpoints } = require('../util/Constants'); @@ -23,17 +22,6 @@ class RESTManager { this.handlers.sweep(handler => handler._inactive); }, 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() { @@ -41,16 +29,8 @@ class RESTManager { } 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; - if (token) return `Bot ${token}`; - */ + if (token) return token?.replace(/Bot /g, ''); throw new Error('TOKEN_MISSING'); } diff --git a/src/rest/RequestHandler.js b/src/rest/RequestHandler.js index bc6d526..1c79161 100644 --- a/src/rest/RequestHandler.js +++ b/src/rest/RequestHandler.js @@ -2,13 +2,12 @@ const { setTimeout } = require('node:timers'); const { setTimeout: sleep } = require('node:timers/promises'); -const { inspect } = require('util'); const { AsyncQueue } = require('@sapphire/async-queue'); const DiscordAPIError = require('./DiscordAPIError'); const HTTPError = require('./HTTPError'); const RateLimitError = require('./RateLimitError'); 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'); const captchaMessage = [ @@ -23,7 +22,7 @@ const captchaMessage = [ function parseResponse(res) { if (res.headers.get('content-type')?.startsWith('application/json')) return res.json(); - return res.arrayBuffer(); // Cre: TheDevYellowy + return res.arrayBuffer(); } function getAPIOffset(serverDate) { @@ -354,18 +353,9 @@ class RequestHandler { let data; try { 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 ( data?.captcha_service && - this.manager.client.options.captchaService && + typeof this.manager.client.options.captchaSolver == 'function' && request.retries < this.manager.client.options.captchaRetryLimit && captchaMessage.some(s => data.captcha_key[0].includes(s)) ) { @@ -376,13 +366,14 @@ class RequestHandler { Method : ${request.method} Path : ${request.path} Route : ${request.route} - Info : ${inspect(data, { depth: null })}`, + Sitekey : ${data.captcha_sitekey} + rqToken : ${data.captcha_rqtoken}`, ); - const captcha = await this.manager.captchaService.solve( - data, - this.manager.client.options.http.headers['User-Agent'], + const captcha = await this.manager.client.options.captchaSolver( + data.captcha_sitekey, + request.fullUserAgent, + data.captcha_rqtoken, ); - // Sleep: await this.manager.client.sleep(5_000); this.manager.client.emit( DEBUG, `Captcha details: @@ -398,6 +389,7 @@ class RequestHandler { } catch (err) { throw new HTTPError(err.message, err.constructor.name, err.status, request); } + throw new DiscordAPIError(data, res.status, request); }