diff --git a/docs/site/content/en/docs/how-to/config/index.md b/docs/site/content/en/docs/how-to/config/index.md index 4ae4901c..15530899 100644 --- a/docs/site/content/en/docs/how-to/config/index.md +++ b/docs/site/content/en/docs/how-to/config/index.md @@ -39,15 +39,27 @@ The following environment variables can be used to configure the WAHA. - `WHATSAPP_START_SESSION=session1,session2`: This variable can be used to start sessions with the specified names right after launching the API. Separate session names with a comma. +### Swagger +- `WHATSAPP_SWAGGER_CONFIG_ADVANCED=true` - enables advanced configuration options for Swagger documentation - you can customize host, port and base URL for the requests. + Disabled by default. + +### Proxy +If you need to use a proxy, you can set the following environment variables: + +- `WHATSAPP_PROXY_SERVER=localhost:3128`: Use this variable to set the proxy server in the format `host:port`, without http or https. +- `WHATSAPP_PROXY_SERVER_USERNAME=username` and `WHATSAPP_PROXY_SERVER_PASSWORD=password`: Use these variables to set up credentials for the proxy. +- `WHATSAPP_PROXY_SERVER_LIST=host1.example.com:3138,host2.example.com:3138`: Use this variable to set a comma-separated list of addresses to use, using a round-robin algorithm to choose the server for the session. +- `WHATSAPP_PROXY_SERVER_INDEX_PREFIX=proxy-`: Use this variable to parse the session name for the prefix and find the appropriate session. + For example, if you have set `WHATSAPP_PROXY_SERVER_LIST=host-first:80,host-second:80,host-third:80` and `WHATSAPP_PROXY_SERVER_INDEX_PREFIX=proxy-` and you run `proxy-3` session, the `host-third:80` proxy will be chosen for that session. + This is a way to select a proxy from while you start session. + Note that a proxy configuration will be added in the `POST /api/session/start` request soon, so you'll be able to define the proxy there. + ### Security ![](/images/versions/plus.png) - `WHATSAPP_API_KEY=mysecret`: If you set this variable, you must include the `X-Api-Key: mysecret` header in all requests to the API. This will protect the API with a secret code. - `WHATSAPP_SWAGGER_USERNAME=admin` and `WHATSAPP_SWAGGER_PASSWORD=admin`: These variables can be used to protect the Swagger panel with `admin / admin` credentials. This does not affect API access. -### Swagger -- `WHATSAPP_SWAGGER_CONFIG_ADVANCED=true` - enables advanced configuration options for Swagger documentation - you can customize host, port and base URL for the requests. - Disabled by default. ### Files ![](/images/versions/plus.png) diff --git a/docs/site/content/en/docs/overview/changelog.md b/docs/site/content/en/docs/overview/changelog.md index 57bfa8b3..59a8ce03 100644 --- a/docs/site/content/en/docs/overview/changelog.md +++ b/docs/site/content/en/docs/overview/changelog.md @@ -12,6 +12,10 @@ menu: weight: 199 toc: true --- +## 2023.7 +July 2023 +- Added [proxy configuration](https://waha.devlike.pro/docs/how-to/config/#proxy) with supporting proxy authentication. + Thanks **puntolino** for the contribution! ## 2023.6 June 2023 diff --git a/package.json b/package.json index 3e214ad1..f5fdc1b9 100644 --- a/package.json +++ b/package.json @@ -37,6 +37,7 @@ "del": "^6.0.0", "express-basic-auth": "^1.2.1", "file-type": "16.5.4", + "https-proxy-agent": "^7.0.0", "link-preview-js": "^3.0.4", "lodash": "^4.17.21", "mime-types": "^2.1.27", diff --git a/src/config.service.ts b/src/config.service.ts index 5a6d47a7..efc31102 100644 --- a/src/config.service.ts +++ b/src/config.service.ts @@ -54,6 +54,31 @@ export class WhatsappConfigService { return parseBool(value); } + get proxyServer(): string[] | string | undefined { + const single = this.configService.get('WHATSAPP_PROXY_SERVER', undefined); + const multipleValues = this.configService.get( + 'WHATSAPP_PROXY_SERVER_LIST', + undefined, + ); + const multiple = multipleValues ? multipleValues.split(',') : undefined; + return single ? single : multiple; + } + + get proxyServerIndexPrefix(): string | undefined { + return this.configService.get( + 'WHATSAPP_PROXY_SERVER_INDEX_PREFIX', + undefined, + ); + } + + get proxyServerUsername(): string | undefined { + return this.configService.get('WHATSAPP_PROXY_SERVER_USERNAME', undefined); + } + + get proxyServerPassword(): string | undefined { + return this.configService.get('WHATSAPP_PROXY_SERVER_PASSWORD', undefined); + } + getWebhookUrl(): string | undefined { return this.get('WHATSAPP_HOOK_URL'); } diff --git a/src/core/abc/session.abc.ts b/src/core/abc/session.abc.ts index 1ea951f2..bcce9b1d 100644 --- a/src/core/abc/session.abc.ts +++ b/src/core/abc/session.abc.ts @@ -56,8 +56,15 @@ export interface WhatsAppSessionConfig { storage: MediaStorage; log: ConsoleLogger; sessionStorage: LocalSessionStorage; + proxyConfig?: ProxyConfig; } +export type ProxyConfig = { + proxyServer: string; + proxyUsername?: string; + proxyPassword?: string; +}; + export abstract class WhatsappSession { public status: WhatsappStatus; public events: EventEmitter; @@ -67,9 +74,16 @@ export abstract class WhatsappSession { protected storage: MediaStorage; protected log: ConsoleLogger; protected sessionStorage: LocalSessionStorage; + protected proxyConfig?: ProxyConfig; - public constructor({ name, log, sessionStorage }: WhatsAppSessionConfig) { + public constructor({ + name, + log, + sessionStorage, + proxyConfig, + }: WhatsAppSessionConfig) { this.name = name; + this.proxyConfig = proxyConfig; this.status = WhatsappStatus.STARTING; this.log = log; this.sessionStorage = sessionStorage; diff --git a/src/core/helpers.proxy.ts b/src/core/helpers.proxy.ts new file mode 100644 index 00000000..2b583105 --- /dev/null +++ b/src/core/helpers.proxy.ts @@ -0,0 +1,67 @@ +import { WhatsappConfigService } from '../config.service'; +import { WhatsappSession, ProxyConfig } from './abc/session.abc'; +import { HttpsProxyAgent } from 'https-proxy-agent'; +import { Agent } from 'https'; +import { URL } from 'url'; + +export function getProxyConfig( + config: WhatsappConfigService, + sessions: Record, + sessionName?: string, +): ProxyConfig | undefined { + // Single proxy server configuration + if (typeof config.proxyServer === 'string') { + return { + proxyServer: config.proxyServer, + proxyUsername: config.proxyServerUsername, + proxyPassword: config.proxyServerPassword, + }; + } + + // Multiple proxy server configuration + if (Array.isArray(config.proxyServer)) { + const prefix = config.proxyServerIndexPrefix; + let indexToUse: number = undefined; + if (prefix && sessionName.includes(prefix)) { + // match numbers at the end of the string + const matches = sessionName.match(/\d+$/); + indexToUse = matches ? parseInt(matches[0], 10) : undefined; + } + let idx = 0; + idx = Object.keys(sessions).length % config.proxyServer.length; + const index = indexToUse ? indexToUse % config.proxyServer.length : idx; + return { + proxyServer: config.proxyServer[index], + proxyUsername: config.proxyServerUsername, + proxyPassword: config.proxyServerPassword, + }; + } + + // No proxy server configuration + return undefined; +} + +function getAuthenticatedUrl( + url: string, + username: string, + password: string, +): string { + const parsedUrl = new URL(url); + parsedUrl.protocol = parsedUrl.protocol || 'http'; + parsedUrl.username = username; + parsedUrl.password = password; + return parsedUrl.toString(); +} + +/** + * Return https Agent proxy for the config + * @param proxyConfig + */ +export function createAgentProxy(proxyConfig: ProxyConfig): Agent | undefined { + const url = getAuthenticatedUrl( + proxyConfig.proxyServer, + proxyConfig.proxyUsername, + proxyConfig.proxyPassword, + ); + return new HttpsProxyAgent(url); +} diff --git a/src/core/manager.core.ts b/src/core/manager.core.ts index 573a2c9b..ce0d4916 100644 --- a/src/core/manager.core.ts +++ b/src/core/manager.core.ts @@ -6,6 +6,7 @@ import { } from '@nestjs/common'; import { SessionManager } from './abc/manager.abc'; import { + ProxyConfig, WAHAInternalEvent, WhatsappSession, WhatsAppSessionConfig, @@ -25,6 +26,7 @@ import { WebhookConductorCore } from './webhooks.core'; import { MediaStorageCore, SessionStorageCore } from './storage.core'; import { WhatsappSessionNoWebCore } from './session.noweb.core'; import { LocalSessionStorage } from './abc/storage.abc'; +import { getProxyConfig } from './helpers.proxy'; export class OnlyDefaultSessionIsAllowed extends UnprocessableEntityException { constructor() { @@ -116,6 +118,7 @@ export class SessionManagerCore extends SessionManager { storage, log, sessionStorage: this.sessionStorage, + proxyConfig: this.getProxyConfig(), }; // @ts-ignore const session = new this.EngineClass(sessionConfig); @@ -128,6 +131,10 @@ export class SessionManagerCore extends SessionManager { return { name: session.name, status: session.status }; } + protected getProxyConfig(): ProxyConfig | undefined { + return getProxyConfig(this.config, { [this.DEFAULT]: this.session }); + } + async stop(request: SessionStopRequest): Promise { this.onlyDefault(request.name); diff --git a/src/core/session.noweb.core.ts b/src/core/session.noweb.core.ts index 68b530b3..9fded1fc 100644 --- a/src/core/session.noweb.core.ts +++ b/src/core/session.noweb.core.ts @@ -44,12 +44,11 @@ import { ParticipantsRequest, } from '../structures/groups.dto'; import { QR } from './QR'; -import { MediaStorage } from './abc/storage.abc'; -import { ConsoleLogger, UnprocessableEntityException } from '@nestjs/common'; +import { UnprocessableEntityException } from '@nestjs/common'; import { Message } from 'whatsapp-web.js'; import * as fs from 'fs'; -import * as path from 'path'; -import * as os from 'os'; +import { Agent } from 'https'; +import { createAgentProxy } from './helpers.proxy'; // eslint-disable-next-line @typescript-eslint/no-var-requires const QRCode = require('qrcode'); @@ -88,7 +87,10 @@ export class WhatsappSessionNoWebCore extends WhatsappSession { async makeSocket() { const { state, saveCreds } = await useMultiFileAuthState(this.authFolder); + const agent = this.makeAgent(); const sock: any = makeWASocket({ + agent: agent, + fetchAgent: agent, auth: state, printQRInTerminal: true, browser: ['Linux', 'Chrome', '111.0.5563.64'], @@ -98,6 +100,13 @@ export class WhatsappSessionNoWebCore extends WhatsappSession { return sock; } + protected makeAgent(): Agent { + if (!this.proxyConfig) { + return undefined; + } + return createAgentProxy(this.proxyConfig); + } + connectStore() { if (!this.store) { this.store = makeInMemoryStore({}); diff --git a/src/core/session.venom.core.ts b/src/core/session.venom.core.ts index fc4d7e13..d1ab9693 100644 --- a/src/core/session.venom.core.ts +++ b/src/core/session.venom.core.ts @@ -15,7 +15,7 @@ import { MessageTextRequest, } from '../structures/chatting.dto'; import { WAMessage, WANumberExistResult } from '../structures/responses.dto'; -import { create, Message, Whatsapp } from 'venom-bot'; +import { create, CreateConfig, Message, Whatsapp } from 'venom-bot'; import { WAEvents, WhatsappEngine, @@ -37,7 +37,7 @@ export class WhatsappSessionVenomCore extends WhatsappSession { } protected buildClient() { - return create(this.name, this.getCatchQR(), undefined, { + const venomOptions: CreateConfig = { headless: true, devtools: false, debug: false, @@ -45,7 +45,22 @@ export class WhatsappSessionVenomCore extends WhatsappSession { browserArgs: this.getBrowserArgsForPuppeteer(), autoClose: 60000, puppeteerOptions: {}, - }); + }; + this.addProxyConfig(venomOptions); + return create(this.name, this.getCatchQR(), undefined, venomOptions); + } + + protected addProxyConfig(venomOptions: CreateConfig) { + if (this.proxyConfig?.proxyServer !== undefined) { + venomOptions.addProxy = [this.proxyConfig?.proxyServer]; + } + if ( + this.proxyConfig?.proxyUsername !== undefined && + this.proxyConfig?.proxyPassword !== undefined + ) { + venomOptions.userProxy = this.proxyConfig?.proxyUsername; + venomOptions.userPass = this.proxyConfig?.proxyPassword; + } } protected getCatchQR() { diff --git a/src/core/session.webjs.core.ts b/src/core/session.webjs.core.ts index 1edb301c..bbecbe80 100644 --- a/src/core/session.webjs.core.ts +++ b/src/core/session.webjs.core.ts @@ -3,6 +3,7 @@ import { Buttons, Chat, Client, + ClientOptions, Contact, Events, GroupChat, @@ -53,13 +54,32 @@ export class WhatsappSessionWebJSCore extends WhatsappSession { whatsapp: Client; protected buildClient() { - return new Client({ + const clientOptions: ClientOptions = { puppeteer: { headless: true, executablePath: this.getBrowserExecutablePath(), args: this.getBrowserArgsForPuppeteer(), }, - }); + }; + this.addProxyConfig(clientOptions); + return new Client(clientOptions); + } + + protected addProxyConfig(clientOptions: ClientOptions) { + if (this.proxyConfig?.proxyServer !== undefined) { + // push the proxy server to the args + clientOptions.puppeteer.args.push( + `--proxy-server=${this.proxyConfig?.proxyServer}`, + ); + + // Authenticate + if (this.proxyConfig?.proxyUsername && this.proxyConfig?.proxyPassword) { + clientOptions.proxyAuthentication = { + username: this.proxyConfig?.proxyUsername, + password: this.proxyConfig?.proxyPassword, + }; + } + } } async start() { diff --git a/yarn.lock b/yarn.lock index 7d65f87e..3096d0b0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1194,9 +1194,9 @@ integrity sha512-iiUgKzV9AuaEkZqkOLDIvlQiL6ltuZd9tGcW3gwpnX8JbuiuhFlEGmmFXEXkN50Cvq7Os88IY2v0dkDqXYWVgA== "@types/node@*": - version "20.2.1" - resolved "https://registry.yarnpkg.com/@types/node/-/node-20.2.1.tgz#de559d4b33be9a808fd43372ccee822c70f39704" - integrity sha512-DqJociPbZP1lbZ5SQPk4oag6W7AyaGMO6gSfRwq3PWl4PXTwJpRQJhDq4W0kzrg3w6tJ1SwlvGZ5uKFHY13LIg== + version "20.2.5" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.2.5.tgz#26d295f3570323b2837d322180dfbf1ba156fefb" + integrity sha512-JJulVEQXmiY9Px5axXHeYGLSjhkZEnD+MDPDGbCbIAbMslkKwmygtZFy1X6s/075Yo94sf8GuSlFfPzysQrWZQ== "@types/node@>=13.7.0": version "18.15.5" @@ -1686,6 +1686,13 @@ agent-base@6: dependencies: debug "4" +agent-base@^7.0.2: + version "7.1.0" + resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-7.1.0.tgz#536802b76bc0b34aa50195eb2442276d613e3434" + integrity sha512-o/zjMZRhJxny7OyEF+Op8X+efiELC7k7yOjMzgfzVqOzXqkBkWI79YoTdOtsuWd5BWhAGAuOY/Xa6xpiaWXiNg== + dependencies: + debug "^4.3.4" + aggregate-error@^3.0.0: version "3.1.0" resolved "https://registry.yarnpkg.com/aggregate-error/-/aggregate-error-3.1.0.tgz#92670ff50f5359bdb7a3e0d40d0ec30c5737687a" @@ -4621,6 +4628,14 @@ https-proxy-agent@5.0.1, https-proxy-agent@^5.0.0: agent-base "6" debug "4" +https-proxy-agent@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-7.0.0.tgz#75cb70d04811685667183b31ab158d006750418a" + integrity sha512-0euwPCRyAPSgGdzD1IVN9nJYHtBhJwb6XPfbpQcYbPCwrBidX6GzxmchnaF4sfF/jPb74Ojx5g4yTg3sixlyPw== + dependencies: + agent-base "^7.0.2" + debug "4" + human-signals@2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-2.1.0.tgz#dc91fcba42e4d06e4abaed33b3e7a3c02f514ea0" @@ -9048,8 +9063,8 @@ webpack@5.75.0: webpack-sources "^3.2.3" "whatsapp-web.js@https://github.com/pedroslopez/whatsapp-web.js": - version "1.20.0" - resolved "https://github.com/pedroslopez/whatsapp-web.js#915763a8d04c37c2ddcb93faa512cfe208f1798e" + version "1.21.0" + resolved "https://github.com/pedroslopez/whatsapp-web.js#a91bf1c762536055ca09a862e963ddb5e29ffe3a" dependencies: "@pedroslopez/moduleraid" "^5.0.2" fluent-ffmpeg "^2.1.2"