From c65c2de45a210c402db516f2dfa70bab8e55aa68 Mon Sep 17 00:00:00 2001 From: Saadat01 Date: Sun, 18 Oct 2026 18:46:27 +0600 Subject: [PATCH 1/2] Add SOCKS5 proxy support --- compose.yaml | 16 ++++----- env.example | 2 +- package-lock.json | 71 ++++++++++++++++++++++++++++++++++++++++ package.json | 1 + src/browser/Browser.ts | 23 ++++++++----- src/index.ts | 54 +++++++++++++++++++++++++++--- src/interface/Account.ts | 11 +++++++ src/util/Http.ts | 4 ++- src/util/ProxyBridge.ts | 50 ++++++++++++++++++++++++++++ src/util/Validator.ts | 8 ++--- 10 files changed, 213 insertions(+), 27 deletions(-) create mode 100644 src/util/ProxyBridge.ts diff --git a/compose.yaml b/compose.yaml index d0ccc729..0833decd 100644 --- a/compose.yaml +++ b/compose.yaml @@ -28,13 +28,13 @@ services: # ── API mode ────────────────────────────────────── # Set API_MODE=true to run the API server for additional control options - #API_MODE: 'true' - #API_TOKEN: '${API_TOKEN}' # required for API mode, see example.env + API_MODE: 'true' + API_TOKEN: '${API_TOKEN}' # required for API mode, see example.env # API write/reveal capabilities are separate opt-ins and are off by default. - #API_ALLOW_SCHEDULE_WRITE: 'true' - #API_ALLOW_ENV_OVERRIDES: 'true' - #API_ALLOW_CONFIG_REVEAL: 'true' - #API_ALLOW_CONFIG_WRITE: 'true' +API_ALLOW_SCHEDULE_WRITE=true +API_ALLOW_ENV_OVERRIDES=true +API_ALLOW_CONFIG_REVEAL=true +API_ALLOW_CONFIG_WRITE=true # ── Configuration ────────────────────────────────────── # Uncomment to override defaults, a full list of configuration options are in the README. @@ -53,8 +53,8 @@ services: # ── API mode port mapping ────────────────────────────────────── # Uncomment when API_MODE=true - #ports: - # - '3010:3010' + ports: + - '3010:3010' healthcheck: test: ['CMD-SHELL', 'scripts/docker/healthcheck.sh'] diff --git a/env.example b/env.example index 63868736..d9d832f6 100644 --- a/env.example +++ b/env.example @@ -18,7 +18,7 @@ ACCOUNT_1_EMAIL=email@example.com #ACCOUNT_1_RECOVERY_EMAIL= #ACCOUNT_1_GEO_LOCALE=auto #ACCOUNT_1_LANG_CODE=en -# Browser proxy: HTTP/HTTPS/SOCKS4/SOCKS5. SOCKS proxies must not require authentication. +# Browser proxy: HTTP/HTTPS/SOCKS4/SOCKS5. #ACCOUNT_1_PROXY_HTTP=false #ACCOUNT_1_PROXY_URL= #ACCOUNT_1_PROXY_PORT=0 diff --git a/package-lock.json b/package-lock.json index 46a74a14..39af5b52 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,6 +19,7 @@ "otpauth": "^9.5.1", "p-queue": "^9.3.0", "patchright": "^1.61.1", + "proxy-chain": "^3.0.1", "semver": "^7.8.1", "ts-node": "^10.9.2", "zod": "^4.4.3" @@ -670,6 +671,15 @@ "node": ">=14.0" } }, + "node_modules/agent-base": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-9.0.0.tgz", + "integrity": "sha512-TQf59BsZnytt8GdJKLPfUZ54g/iaUL2OWDSFCCvMOhsHduDQxO8xC4PNeyIkVcA5KwL2phPSv0douC0fgWzmnA==", + "license": "MIT", + "engines": { + "node": ">= 20" + } + }, "node_modules/ajv": { "version": "6.15.0", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", @@ -1590,6 +1600,15 @@ "node": ">=0.8.19" } }, + "node_modules/ip-address": { + "version": "10.7.4", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.7.4.tgz", + "integrity": "sha512-DWBMKyeM25D70nrIiTkgQth59/G+naKOTep6huF9j+GmeK3DMD60Zvxg5d5qQnz+38AHgou6ean2LECX+lrKDg==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -2048,6 +2067,20 @@ "url": "https://github.com/prettier/prettier?sponsor=1" } }, + "node_modules/proxy-chain": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/proxy-chain/-/proxy-chain-3.0.1.tgz", + "integrity": "sha512-ZISgL5pwUBXcijycj9ofC8wbJ9GmSEka71kn4dzjphDxfZcecQ6uvU2NWuyQcGdEf0XfscXWDs3KKgjxeQXTZw==", + "license": "Apache-2.0", + "dependencies": { + "socks": "^2.8.3", + "socks-proxy-agent": "^10.1.0", + "tslib": "^2.3.1" + }, + "engines": { + "node": ">=20.11" + } + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -2113,6 +2146,44 @@ "node": ">=8" } }, + "node_modules/smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "license": "MIT", + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks": { + "version": "2.8.10", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.10.tgz", + "integrity": "sha512-e0VyvkVTwVYViNovRkZ9aodhxVlyoMn7eJhVUPxZ+eK9P/7CBkxvvsBOHqFPEH416726W8tLXXXjKwqgTErrCQ==", + "license": "MIT", + "dependencies": { + "ip-address": "^10.1.1", + "smart-buffer": "^4.2.0" + }, + "engines": { + "node": ">= 10.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks-proxy-agent": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-10.1.0.tgz", + "integrity": "sha512-WlMj/67cEJ6MDI1OcsnjuYKDNDoyPCCYZ249kuuXPiMDw9F8PXkVaQ7YWu3siTydfQ/4BEZcvGzu+aYvz7dDCQ==", + "license": "MIT", + "dependencies": { + "agent-base": "9.0.0", + "debug": "^4.3.4", + "socks": "^2.8.3" + }, + "engines": { + "node": ">= 20" + } + }, "node_modules/strnum": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.4.2.tgz", diff --git a/package.json b/package.json index 82c63c8e..ad6914eb 100644 --- a/package.json +++ b/package.json @@ -58,6 +58,7 @@ "otpauth": "^9.5.1", "p-queue": "^9.3.0", "patchright": "^1.61.1", + "proxy-chain": "^3.0.1", "semver": "^7.8.1", "ts-node": "^10.9.2", "zod": "^4.4.3" diff --git a/src/browser/Browser.ts b/src/browser/Browser.ts index e50279c9..b491dea3 100644 --- a/src/browser/Browser.ts +++ b/src/browser/Browser.ts @@ -57,15 +57,20 @@ class Browser { let browser: rebrowser.Browser try { const proxyConfig = account.proxy.url - ? { - server: formatBrowserProxyServer(account.proxy.url, account.proxy.port), - ...(account.proxy.username && - account.proxy.password && { - username: account.proxy.username, - password: account.proxy.password - }) - } - : undefined + ? { + server: + account.proxy.bridgeUrl ?? + formatBrowserProxyServer(account.proxy.url, account.proxy.port), + + ...(!account.proxy.bridgeUrl && + account.proxy.username && + account.proxy.password && { + username: account.proxy.username, + password: account.proxy.password + }) + } + : undefined + const runningAsRoot = typeof process.getuid === 'function' && process.getuid() === 0 const sandboxDisabled = process.platform === 'linux' && runningAsRoot diff --git a/src/index.ts b/src/index.ts index ae19c491..74deebf8 100644 --- a/src/index.ts +++ b/src/index.ts @@ -25,6 +25,8 @@ import { SearchManager } from './functions/activities/search/SearchManager' import type { Account } from './interface/Account' import HttpClient from './util/Http' +import { parseBrowserProxyUrl } from './util/Proxy' +import { startAuthenticatedSocks5Bridge } from './util/ProxyBridge' import { sendDiscord, flushDiscordQueue } from './logging/Discord' import { sendNtfy, flushNtfyQueue } from './logging/Ntfy' import { sendTelegram, flushTelegramQueue } from './logging/Telegram' @@ -419,6 +421,10 @@ export class MicrosoftRewardsBot { this.userData.userName = this.utils.getEmailUsername(accountEmail) this.userData.timezoneOffset = String(new Date().getTimezoneOffset()) + let proxyBridge: + | Awaited> + | undefined + try { const cachedRegion = account.geoLocale === 'auto' ? loadResolvedRegion(this.config.sessionPath, accountEmail) : undefined @@ -434,11 +440,33 @@ export class MicrosoftRewardsBot { }` ) - this.http = new HttpClient(account.proxy, { - 'Accept-Language': this.accountLocale.acceptLanguage - }) + +const configuredProxy = account.proxy.url + ? parseBrowserProxyUrl(account.proxy.url) + : undefined + +if ( + configuredProxy?.protocol === 'socks5:' && + account.proxy.username && + account.proxy.password +) { + proxyBridge = await startAuthenticatedSocks5Bridge(account.proxy) + + // Browser.ts and Http.ts use this runtime-only URL. + account.proxy.bridgeUrl = proxyBridge.url + + this.logger.info( + 'main', + 'PROXY-BRIDGE', + `Authenticated SOCKS5 bridge started for ${accountEmail}` + ) +} - const result: AccountRunResult | undefined = await this.Main(account).catch(error => { +this.http = new HttpClient(account.proxy, { + 'Accept-Language': this.accountLocale.acceptLanguage +}) + +const result: AccountRunResult | undefined = await this.Main(account).catch(error => { void this.logger.error( true, 'FLOW', @@ -516,6 +544,24 @@ export class MicrosoftRewardsBot { error: error instanceof Error ? error.message : String(error) }) } + + finally { + try { + if (proxyBridge) { + await proxyBridge.close() + } + } catch (error) { + this.logger.warn( + 'main', + 'PROXY-BRIDGE', + `Failed to close SOCKS5 bridge for ${accountEmail}: ${ + error instanceof Error ? error.message : String(error) + }` + ) + } finally { + delete account.proxy.bridgeUrl + } +} } if (this.config.clusters <= 1 && cluster.isPrimary) { diff --git a/src/interface/Account.ts b/src/interface/Account.ts index 1e3c66f9..7566ade0 100644 --- a/src/interface/Account.ts +++ b/src/interface/Account.ts @@ -21,3 +21,14 @@ export interface ConfigSaveFingerprint { mobile: boolean desktop: boolean } + +export interface AccountProxy { + proxyHttp: boolean + url: string + port: number + password: string + username: string + + // Internal runtime property; not configured through .env. + bridgeUrl?: string +} \ No newline at end of file diff --git a/src/util/Http.ts b/src/util/Http.ts index 4988e28a..305f25a4 100644 --- a/src/util/Http.ts +++ b/src/util/Http.ts @@ -177,7 +177,9 @@ class HttpClient { this.account = account this.defaultHeaders = { ...defaultHeaders } - const proxyUrl = this.account.url && this.account.proxyHttp ? this.buildProxyUrl(this.account) : undefined + const proxyUrl = this.account.url && this.account.proxyHttp + ? (this.account.bridgeUrl ?? this.buildProxyUrl(this.account)) + : undefined this.instance = new Impit({ browser: 'chrome', proxyUrl, timeout: DEFAULT_TIMEOUT }) } diff --git a/src/util/ProxyBridge.ts b/src/util/ProxyBridge.ts new file mode 100644 index 00000000..74fe25e3 --- /dev/null +++ b/src/util/ProxyBridge.ts @@ -0,0 +1,50 @@ +import { anonymizeProxy, closeAnonymizedProxy } from 'proxy-chain' +import type { AccountProxy } from '../interface/Account' +import { parseBrowserProxyUrl } from './Proxy' + +export async function startAuthenticatedSocks5Bridge( + proxy: AccountProxy +): Promise<{ url: string; close: () => Promise }> { + const parsed = parseBrowserProxyUrl(proxy.url) + + if (parsed.protocol !== 'socks5:') { + throw new Error('Authenticated SOCKS5 bridge requires a socks5:// proxy') + } + + if (!proxy.username || !proxy.password) { + throw new Error('SOCKS5 bridge requires both username and password') + } + + if (!Number.isInteger(proxy.port) || proxy.port < 1 || proxy.port > 65535) { + throw new Error('Invalid SOCKS5 proxy port') + } + + // Use socks5h so hostname resolution is performed through the upstream proxy. + const upstream = new URL(`socks5h://${parsed.hostname}:${proxy.port}`) + upstream.username = encodeURIComponent(proxy.username) + upstream.password = encodeURIComponent(proxy.password) + + const upstreamUrl = upstream.toString() + + // proxy-chain opens a temporary loopback HTTP proxy. + const localUrl = await anonymizeProxy({ + url: upstreamUrl, + port: 0 + }) + + // Fail closed: never silently continue with the upstream URL directly. + if (!localUrl.startsWith('http://127.0.0.1:')) { + if (localUrl === upstreamUrl) { + throw new Error('Failed to create local SOCKS5 proxy bridge') + } + + throw new Error('Unexpected local proxy bridge address') + } + + return { + url: localUrl, + close: async () => { + await closeAnonymizedProxy(localUrl, true) + } + } +} \ No newline at end of file diff --git a/src/util/Validator.ts b/src/util/Validator.ts index 907cc998..64b7267e 100644 --- a/src/util/Validator.ts +++ b/src/util/Validator.ts @@ -222,13 +222,13 @@ const AccountProxySchema = z }) } - if ((url.protocol === 'socks4:' || url.protocol === 'socks5:') && (hasUsername || hasPassword)) { + if (url.protocol === 'socks4:' && (hasUsername || hasPassword)) { ctx.addIssue({ code: 'custom', path: ['username'], - message: `${url.protocol.slice(0, -1).toUpperCase()} proxy authentication is not supported by Patchright` - }) - } + message: 'SOCKS4 proxy authentication is not supported' + }) +} }) export const AccountSchema = z.object({ From b68feddb411c758e983995ce5ce432b84a9d3c72 Mon Sep 17 00:00:00 2001 From: Saadat01 Date: Sun, 18 Oct 2026 18:56:19 +0600 Subject: [PATCH 2/2] Add authenticated SOCKS5 proxy support --- compose.yaml | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/compose.yaml b/compose.yaml index 0833decd..d0ccc729 100644 --- a/compose.yaml +++ b/compose.yaml @@ -28,13 +28,13 @@ services: # ── API mode ────────────────────────────────────── # Set API_MODE=true to run the API server for additional control options - API_MODE: 'true' - API_TOKEN: '${API_TOKEN}' # required for API mode, see example.env + #API_MODE: 'true' + #API_TOKEN: '${API_TOKEN}' # required for API mode, see example.env # API write/reveal capabilities are separate opt-ins and are off by default. -API_ALLOW_SCHEDULE_WRITE=true -API_ALLOW_ENV_OVERRIDES=true -API_ALLOW_CONFIG_REVEAL=true -API_ALLOW_CONFIG_WRITE=true + #API_ALLOW_SCHEDULE_WRITE: 'true' + #API_ALLOW_ENV_OVERRIDES: 'true' + #API_ALLOW_CONFIG_REVEAL: 'true' + #API_ALLOW_CONFIG_WRITE: 'true' # ── Configuration ────────────────────────────────────── # Uncomment to override defaults, a full list of configuration options are in the README. @@ -53,8 +53,8 @@ API_ALLOW_CONFIG_WRITE=true # ── API mode port mapping ────────────────────────────────────── # Uncomment when API_MODE=true - ports: - - '3010:3010' + #ports: + # - '3010:3010' healthcheck: test: ['CMD-SHELL', 'scripts/docker/healthcheck.sh']