feat: implement ProxyAgent support and update HTTP options for improved proxy handling

This commit is contained in:
Elysia
2024-12-13 20:19:09 +07:00
parent d4bace98b3
commit 1bb7203a66
7 changed files with 50 additions and 33 deletions

View File

@@ -14,10 +14,17 @@ const client = new Discord.Client({
agent: proxy, // WebSocket Proxy agent: proxy, // WebSocket Proxy
}, },
http: { http: {
agent: proxy, // REST Proxy // API Proxy
// Read more: https://github.com/nodejs/undici/blob/main/docs/docs/api/ProxyAgent.md
// agent: ProxyAgentOptions
agent: 'my.proxy.server',
// or new URL('my.proxy.server')
// or { uri: 'my.proxy.server' }
}, },
}); });
// So if you only need to use the API Proxy (for the purpose of saving data), you don't need to install `proxy-agent`.
client.on('ready', async () => { client.on('ready', async () => {
console.log('Ready!', client.user.tag); console.log('Ready!', client.user.tag);
}); });

View File

@@ -588,7 +588,7 @@ class Client extends BaseClient {
* await client.acceptInvite('https://discord.gg/genshinimpact', { bypassOnboarding: true, bypassVerify: true }) * await client.acceptInvite('https://discord.gg/genshinimpact', { bypassOnboarding: true, bypassVerify: true })
*/ */
async acceptInvite(invite, options = { bypassOnboarding: true, bypassVerify: true }) { async acceptInvite(invite, options = { bypassOnboarding: true, bypassVerify: true }) {
throw new Error('METHOD_WARNING'); // ! throw new Error('METHOD_WARNING');
const code = DataResolver.resolveInviteCode(invite); const code = DataResolver.resolveInviteCode(invite);
if (!code) throw new Error('INVITE_RESOLVE_CODE'); if (!code) throw new Error('INVITE_RESOLVE_CODE');
const i = await this.fetchInvite(code); const i = await this.fetchInvite(code);
@@ -711,7 +711,7 @@ class Client extends BaseClient {
}) })
*/ */
authorizeURL(url, options = { authorize: true }) { authorizeURL(url, options = { authorize: true }) {
throw new Error('METHOD_WARNING'); // ! throw new Error('METHOD_WARNING');
const pathnameAPI = /\/api\/(v\d{1,2}\/)?oauth2\/authorize/; const pathnameAPI = /\/api\/(v\d{1,2}\/)?oauth2\/authorize/;
const pathnameURL = /\/oauth2\/authorize/; const pathnameURL = /\/oauth2\/authorize/;
const url_ = new URL(url); const url_ = new URL(url);
@@ -803,9 +803,6 @@ class Client extends BaseClient {
* @private * @private
*/ */
_validateOptions(options = this.options) { _validateOptions(options = this.options) {
options.captchaSolver = () => {
throw new Error('METHOD_WARNING');
};
if (typeof options.makeCache !== 'function') { if (typeof options.makeCache !== 'function') {
throw new TypeError('CLIENT_INVALID_OPTION', 'makeCache', 'a function'); throw new TypeError('CLIENT_INVALID_OPTION', 'makeCache', 'a function');
} }

View File

@@ -1,11 +1,10 @@
'use strict'; 'use strict';
const Buffer = require('node:buffer').Buffer; const Buffer = require('node:buffer').Buffer;
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 { CookieJar } = require('tough-cookie'); const { CookieJar } = require('tough-cookie');
const { fetch: fetchOriginal, FormData } = require('undici'); const { fetch: fetchOriginal, FormData, buildConnector, Client, ProxyAgent } = require('undici');
const { ciphers } = require('../util/Constants'); const { ciphers } = require('../util/Constants');
const Util = require('../util/Util'); const Util = require('../util/Util');
@@ -39,23 +38,14 @@ class APIRequest {
make(captchaKey, captchaRqToken) { make(captchaKey, captchaRqToken) {
if (!agent) { if (!agent) {
if (Util.verifyProxyAgent(this.client.options.http.agent)) { const r_ = Util.checkUndiciProxyAgent(this.client.options.http.agent);
// Bad code if (!r_) {
for (const [k, v] of Object.entries({ agent = new Client('https://discord.com', {
keepAlive: true, connect: buildConnector({ ciphers: ciphers.join(':') }),
honorCipherOrder: true, });
secureProtocol: 'TLSv1_2_method',
ciphers: ciphers.join(':'),
})) {
this.client.options.http.agent.httpsAgent[k] = v;
}
agent = this.client.options.http.agent;
} else { } else {
agent = new https.Agent({ agent = new ProxyAgent({
...this.client.options.http.agent, ...r_,
keepAlive: true,
honorCipherOrder: true,
secureProtocol: 'TLSv1_2_method',
ciphers: ciphers.join(':'), ciphers: ciphers.join(':'),
}); });
} }
@@ -70,7 +60,7 @@ class APIRequest {
let headers = { let headers = {
accept: '*/*', accept: '*/*',
'accept-language': 'en-US', 'accept-language': 'en-US',
'sec-ch-ua': '"Not?A_Brand";v="8", "Chromium";v="108"', 'sec-ch-ua': '"Chromium";v="131", "Not_A Brand";v="24"',
'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',
@@ -86,6 +76,7 @@ class APIRequest {
origin: 'https://discord.com', origin: 'https://discord.com',
...this.client.options.http.headers, ...this.client.options.http.headers,
'User-Agent': this.fullUserAgent, 'User-Agent': this.fullUserAgent,
priority: 'u=1, i',
}; };
if (this.options.auth !== false) headers.Authorization = this.rest.getAuth(); if (this.options.auth !== false) headers.Authorization = this.rest.getAuth();
@@ -146,10 +137,11 @@ class APIRequest {
return fetch(url, { return fetch(url, {
method: this.method.toUpperCase(), // Undici doesn't normalize "patch" into "PATCH" (which surprisingly follows the spec). method: this.method.toUpperCase(), // Undici doesn't normalize "patch" into "PATCH" (which surprisingly follows the spec).
headers, headers,
agent,
body, body,
signal: controller.signal, signal: controller.signal,
redirect: 'follow', redirect: 'follow',
dispatcher: agent,
credentials: 'include',
}).finally(() => clearTimeout(timeout)); }).finally(() => clearTimeout(timeout));
} }
} }

View File

@@ -9,10 +9,10 @@ const { Error, RangeError, TypeError } = require('../errors');
exports.MaxBulkDeletableMessageAge = 1_209_600_000; exports.MaxBulkDeletableMessageAge = 1_209_600_000;
exports.UserAgent = exports.UserAgent =
'Mozilla/5.0 (Windows NT 10.0; WOW64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.5359.215 Safari/537.36'; 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Electron/33.0.0 Safari/537.36';
/** /**
* Google Chrome v108 TLS ciphers * Google Chrome v131 TLS ciphers
* @see {@link https://tls.browserleaks.com/tls} * @see {@link https://tls.browserleaks.com/tls}
* @see {@link https://github.com/yifeikong/curl-impersonate} * @see {@link https://github.com/yifeikong/curl-impersonate}
* @typedef {Array<string>} Ciphers * @typedef {Array<string>} Ciphers

View File

@@ -128,11 +128,17 @@ const Intents = require('./Intents');
* @see {@link https://nodejs.org/api/http.html#http_new_agent_options} * @see {@link https://nodejs.org/api/http.html#http_new_agent_options}
*/ */
/**
* ProxyAgent options.
* @typedef {Object} ProxyAgentOptions
* @see {@link https://github.com/nodejs/undici/blob/main/docs/docs/api/ProxyAgent.md}
*/
/** /**
* HTTP options * HTTP options
* @typedef {Object} HTTPOptions * @typedef {Object} HTTPOptions
* @property {number} [version=9] API version to use * @property {number} [version=9] API version to use
* @property {AgentOptions} [agent={}] HTTPS Agent options * @property {ProxyAgentOptions} [agent={}] ProxyAgent options
* @property {string} [api='https://discord.com/api'] Base URL of the API * @property {string} [api='https://discord.com/api'] Base URL of the API
* @property {string} [cdn='https://cdn.discordapp.com'] Base URL of the CDN * @property {string} [cdn='https://cdn.discordapp.com'] Base URL of the CDN
* @property {string} [invite='https://discord.gg'] Base URL of invites * @property {string} [invite='https://discord.gg'] Base URL of invites
@@ -181,14 +187,14 @@ class Options extends null {
device: '', device: '',
system_locale: 'vi-VN', system_locale: 'vi-VN',
browser_user_agent: UserAgent, browser_user_agent: UserAgent,
browser_version: '108.0.5359.215', browser_version: '131.0.0.0',
os_version: '10', os_version: '10',
referrer: '', referrer: '',
referring_domain: '', referring_domain: '',
referrer_current: '', referrer_current: '',
referring_domain_current: '', referring_domain_current: '',
release_channel: 'stable', release_channel: 'stable',
client_build_number: 338907, client_build_number: 353304,
client_event_source: null, client_event_source: null,
}, },
compress: false, compress: false,

View File

@@ -874,6 +874,21 @@ class Util extends null {
return typeof object == 'object' && object.httpAgent instanceof Agent && object.httpsAgent instanceof Agent; return typeof object == 'object' && object.httpAgent instanceof Agent && object.httpsAgent instanceof Agent;
} }
static checkUndiciProxyAgent(data) {
if (typeof data === 'string') {
return {
uri: data,
};
}
if (data instanceof URL) {
return {
uri: data.toString(),
};
}
if (typeof data === 'object' && typeof data.uri === 'string') return data;
return false;
}
static createPromiseInteraction(client, nonce, timeoutMs = 5_000, isHandlerDeferUpdate = false, parent) { static createPromiseInteraction(client, nonce, timeoutMs = 5_000, isHandlerDeferUpdate = false, parent) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
// Waiting for MsgCreate / ModalCreate // Waiting for MsgCreate / ModalCreate

4
typings/index.d.ts vendored
View File

@@ -61,7 +61,7 @@ import {
import { ChildProcess, ChildProcessWithoutNullStreams } from 'node:child_process'; import { ChildProcess, ChildProcessWithoutNullStreams } from 'node:child_process';
import { EventEmitter } from 'node:events'; import { EventEmitter } from 'node:events';
import { AgentOptions } from 'node:https'; import { AgentOptions } from 'node:https';
import { Response } from 'undici'; import { Response, ProxyAgent } from 'undici';
import { Readable, Writable, Stream } from 'node:stream'; import { Readable, Writable, Stream } from 'node:stream';
import { MessagePort, Worker } from 'node:worker_threads'; import { MessagePort, Worker } from 'node:worker_threads';
import * as WebSocket from 'ws'; import * as WebSocket from 'ws';
@@ -6726,7 +6726,7 @@ export interface HTTPErrorData {
} }
export interface HTTPOptions { export interface HTTPOptions {
agent?: Omit<AgentOptions, 'keepAlive'>; agent?: Omit<ProxyAgent.Options, 'keepAlive'>;
api?: string; api?: string;
version?: number; version?: number;
host?: string; host?: string;