diff --git a/Example/example.ts b/Example/example.ts index 685ad7c..84d2774 100644 --- a/Example/example.ts +++ b/Example/example.ts @@ -8,6 +8,7 @@ import { MessageLogLevel, WA_MESSAGE_STUB_TYPES, ReconnectMode, + ProxyAgent, } from '../src/WAConnection/WAConnection' import * as fs from 'fs' @@ -16,6 +17,7 @@ async function example() { conn.autoReconnect = ReconnectMode.onConnectionLost // only automatically reconnect when the connection breaks conn.logLevel = MessageLogLevel.info // set to unhandled to see what kind of stuff you can implement + // loads the auth file credentials if present fs.existsSync('./auth_info.json') && conn.loadAuthInfo ('./auth_info.json') @@ -23,6 +25,8 @@ async function example() { conn.connectOptions.timeoutMs = 60*1000 // attempt to reconnect at most 10 times conn.connectOptions.maxRetries = 10 + // uncomment the following line to proxy the connection; some random proxy I got off of: https://proxyscrape.com/free-proxy-list + //conn.connectOptions.agent = ProxyAgent ('http://1.0.180.120:8080') await conn.connect() const unread = await conn.loadAllUnreadMessages () diff --git a/README.md b/README.md index 636ae43..8259733 100644 --- a/README.md +++ b/README.md @@ -51,7 +51,6 @@ async function connectToWhatsApp () { const unread = await conn.loadAllUnreadMessages () console.log ("you have " + unread.length + " unread messages") } - // run in main file connectToWhatsApp () .catch (err => console.log("unexpected error: " + err) ) // catch any errors @@ -69,6 +68,18 @@ Do note, the `chats` object returned is now a [KeyedDB](https://github.com/adiwa - Most applications require pagination of chats (Use `chats.paginated()`) - Most applications require **O(1)** access to chats via the chat ID. (Use `chats.get(jid)` with `KeyedDB`) +## Connecting via an HTTPS proxy + +``` ts +import { WAConnection, ProxyAgent } from '@adiwajshing/baileys' + +const conn = new WAConnecion () +conn.connectOptions.agent = ProxyAgent ('http://some-host:1234') + +await conn.connect () +console.log ("oh hello " + conn.user.name + "! You connected via a proxy") +``` + ## Saving & Restoring Sessions You obviously don't want to keep scanning the QR code every time you want to connect. diff --git a/package.json b/package.json index 5cff011..f8a0839 100644 --- a/package.json +++ b/package.json @@ -34,6 +34,7 @@ "@adiwajshing/keyed-db": "^0.1.2", "curve25519-js": "^0.0.4", "futoin-hkdf": "^1.3.2", + "https-proxy-agent": "^5.0.0", "jimp": "^0.16.1", "node-fetch": "^2.6.0", "protobufjs": "^6.10.1", diff --git a/src/Tests/Tests.Misc.ts b/src/Tests/Tests.Misc.ts index e7c3c88..fc76e16 100644 --- a/src/Tests/Tests.Misc.ts +++ b/src/Tests/Tests.Misc.ts @@ -1,4 +1,4 @@ -import { Presence, ChatModification, delay } from '../WAConnection/WAConnection' +import { Presence, ChatModification, delay, DEFAULT_ORIGIN } from '../WAConnection/WAConnection' import { promises as fs } from 'fs' import * as assert from 'assert' import fetch from 'node-fetch' @@ -51,7 +51,7 @@ WAConnectionTest('Misc', (conn) => { await delay (5000) const ppUrl = await conn.getProfilePicture(conn.user.jid) - const fetched = await fetch(ppUrl, { headers: { Origin: 'https://web.whatsapp.com' } }) + const fetched = await fetch(ppUrl) const buff = await fetched.buffer () const newPP = await fs.readFile ('./Media/cat.jpeg') diff --git a/src/WAConnection/0.Base.ts b/src/WAConnection/0.Base.ts index 9944f1b..c7ecd36 100644 --- a/src/WAConnection/0.Base.ts +++ b/src/WAConnection/0.Base.ts @@ -3,6 +3,7 @@ import WS from 'ws' import * as Utils from './Utils' import Encoder from '../Binary/Encoder' import Decoder from '../Binary/Decoder' +import fetch from 'node-fetch' import { AuthenticationCredentials, WAUser, @@ -21,6 +22,7 @@ import { ReconnectMode, WAConnectOptions, MediaConnInfo, + DEFAULT_ORIGIN, } from './Constants' import { EventEmitter } from 'events' import KeyedDB from '@adiwajshing/keyed-db' @@ -340,6 +342,17 @@ export class WAConnection extends EventEmitter { } }) } + /** + * Does a fetch request with the configuration of the connection + */ + protected fetchRequest = (endpoint: string, method: string = 'GET', body?: any) => ( + fetch(endpoint, { + method, + body, + headers: { Origin: DEFAULT_ORIGIN }, + agent: this.connectOptions.agent + }) + ) generateMessageTag () { return `${Utils.unixTimestampSeconds(this.referenceDate)}.--${this.msgCount}` } diff --git a/src/WAConnection/3.Connect.ts b/src/WAConnection/3.Connect.ts index d5d38a6..f136d99 100644 --- a/src/WAConnection/3.Connect.ts +++ b/src/WAConnection/3.Connect.ts @@ -1,5 +1,5 @@ import * as Utils from './Utils' -import { WAMessage, WAChat, MessageLogLevel, WANode, KEEP_ALIVE_INTERVAL_MS, BaileysError, WAConnectOptions, DisconnectReason, UNAUTHORIZED_CODES, WAContact, TimedOutError, CancelledError, WAOpenResult } from './Constants' +import { WAMessage, WAChat, MessageLogLevel, WANode, KEEP_ALIVE_INTERVAL_MS, BaileysError, WAConnectOptions, DisconnectReason, UNAUTHORIZED_CODES, WAContact, TimedOutError, CancelledError, WAOpenResult, DEFAULT_ORIGIN, WS_URL } from './Constants' import {WAConnection as Base} from './1.Validation' import Decoder from '../Binary/Decoder' import WS from 'ws' @@ -73,7 +73,7 @@ export class WAConnection extends Base { const reconnectID = shouldUseReconnect ? this.user.jid.replace ('@s.whatsapp.net', '@c.us') : null - this.conn = new WS('wss://web.whatsapp.com/ws', null, { origin: 'https://web.whatsapp.com', timeout: timeoutMs }) + this.conn = new WS(WS_URL, null, { origin: DEFAULT_ORIGIN, timeout: timeoutMs, agent: options.agent }) this.conn.on('message', data => this.onMessageRecieved(data as any)) this.conn.on ('open', () => { diff --git a/src/WAConnection/6.MessagesSend.ts b/src/WAConnection/6.MessagesSend.ts index f2812eb..51ac4d0 100644 --- a/src/WAConnection/6.MessagesSend.ts +++ b/src/WAConnection/6.MessagesSend.ts @@ -1,5 +1,4 @@ import {WAConnection as Base} from './5.User' -import fetch from 'node-fetch' import {promises as fs} from 'fs' import { MessageOptions, @@ -114,18 +113,14 @@ export class WAConnection extends Base { for (let host of json.hosts) { const hostname = `https://${host.hostname}${MediaPathMap[mediaType]}/${fileEncSha256B64}?auth=${auth}&token=${fileEncSha256B64}` try { - const urlFetch = await fetch(hostname, { - method: 'POST', - body: body, - headers: { Origin: 'https://web.whatsapp.com' }, - }) + const urlFetch = await this.fetchRequest(hostname, 'POST', body) mediaUrl = (await urlFetch.json())?.url if (mediaUrl) break else throw new Error (`upload failed`) } catch (error) { const isLast = host.hostname === json.hosts[json.hosts.length-1].hostname - this.log (`Error in uploading to ${host}${isLast ? '' : ', retrying...'}`, MessageLogLevel.info) + this.log (`Error in uploading to ${host.hostname}${isLast ? '' : ', retrying...'}`, MessageLogLevel.info) } } if (!mediaUrl) throw new Error('Media upload failed on all hosts') @@ -212,15 +207,14 @@ export class WAConnection extends Base { * Renews the download url automatically, if necessary. */ async downloadMediaMessage (message: WAMessage) { - const fetchHeaders = { } try { - const buff = await decodeMediaMessageBuffer (message.message, fetchHeaders) + const buff = await decodeMediaMessageBuffer (message.message, this.fetchRequest) return buff } catch (error) { if (error instanceof BaileysError && error.status === 404) { // media needs to be updated this.log (`updating media of message: ${message.key.id}`, MessageLogLevel.info) await this.updateMediaMessage (message) - const buff = await decodeMediaMessageBuffer (message.message, fetchHeaders) + const buff = await decodeMediaMessageBuffer (message.message, this.fetchRequest) return buff } throw error diff --git a/src/WAConnection/Constants.ts b/src/WAConnection/Constants.ts index a97a0e4..b6d89a2 100644 --- a/src/WAConnection/Constants.ts +++ b/src/WAConnection/Constants.ts @@ -1,8 +1,13 @@ import { WA } from '../Binary/Constants' import { proto } from '../../WAMessage/WAMessage' +import { Agent } from 'https' + +export const WS_URL = 'wss://web.whatsapp.com/ws' +export const DEFAULT_ORIGIN = 'https://web.whatsapp.com' export const KEEP_ALIVE_INTERVAL_MS = 20*1000 + // export the WAMessage Prototypes export { proto as WAMessageProto } export type WANode = WA.Node @@ -15,6 +20,7 @@ export type WAContextInfo = proto.IContextInfo export import WA_MESSAGE_STUB_TYPE = proto.WebMessageInfo.WEB_MESSAGE_INFO_STUBTYPE export import WA_MESSAGE_STATUS_TYPE = proto.WebMessageInfo.WEB_MESSAGE_INFO_STATUS + export interface WALocationMessage { degreesLatitude: number degreesLongitude: number @@ -67,6 +73,8 @@ export type WAConnectOptions = { waitForChats?: boolean connectCooldownMs?: number + /** agent which can be used for proxying connections */ + agent?: Agent } export type WAConnectionState = 'open' | 'connecting' | 'close' diff --git a/src/WAConnection/Utils.ts b/src/WAConnection/Utils.ts index 45ce5c5..50e9971 100644 --- a/src/WAConnection/Utils.ts +++ b/src/WAConnection/Utils.ts @@ -2,12 +2,14 @@ import * as Crypto from 'crypto' import HKDF from 'futoin-hkdf' import Jimp from 'jimp' import {promises as fs} from 'fs' -import fetch from 'node-fetch' import { exec } from 'child_process' import {platform, release} from 'os' +import HttpsProxyAgent from 'https-proxy-agent' +import { URL } from 'url' +import { Agent } from 'https' import Decoder from '../Binary/Decoder' -import { MessageType, HKDFInfoKeys, MessageOptions, WAChat, WAMessageContent, BaileysError, WAMessageProto, TimedOutError, CancelledError } from './Constants' +import { MessageType, HKDFInfoKeys, MessageOptions, WAChat, WAMessageContent, BaileysError, WAMessageProto, TimedOutError, CancelledError, DEFAULT_ORIGIN } from './Constants' const platformMap = { 'aix': 'AIX', @@ -220,6 +222,8 @@ export const generateProfilePicture = async (buffer: Buffer) => { preview: await cropped.resize(96, 96).getBufferAsync (Jimp.MIME_JPEG) } } +export const ProxyAgent = (host: string | URL) => HttpsProxyAgent(host) as any as Agent + /** generates a thumbnail for a given media, if required */ export async function generateThumbnail(buffer: Buffer, mediaType: MessageType, info: MessageOptions) { if (info.thumbnail === null || info.thumbnail) { @@ -249,7 +253,7 @@ export async function generateThumbnail(buffer: Buffer, mediaType: MessageType, * Decode a media message (video, image, document, audio) & return decrypted buffer * @param message the media message you want to decode */ -export async function decodeMediaMessageBuffer(message: WAMessageContent, fetchHeaders: {[k: string]: string} = {}) { +export async function decodeMediaMessageBuffer(message: WAMessageContent, fetchRequest: (host: string, method: string) => any) { /* One can infer media type from the key in the message it is usually written as [mediaType]Message. Eg. imageMessage, audioMessage etc. @@ -274,8 +278,7 @@ export async function decodeMediaMessageBuffer(message: WAMessageContent, fetchH } // download the message - const headers = { Origin: 'https://web.whatsapp.com' } - const fetched = await fetch(messageContent.url, { headers }) + const fetched = await fetchRequest(messageContent.url, 'GET') const buffer = await fetched.buffer() if (buffer.length <= 10) { diff --git a/yarn.lock b/yarn.lock index 2e13e64..3d031b3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -399,6 +399,13 @@ dependencies: "@types/node" "*" +agent-base@6: + version "6.0.1" + resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-6.0.1.tgz#808007e4e5867decb0ab6ab2f928fbdb5a596db4" + integrity sha512-01q25QQDwLSsyfhrKbn8yuur+JNw0H+0Y4JiGIKd3z9aYk/w/2kxD/Upc+t2ZBBSUNff50VjPsSW2YxM8QYKVg== + dependencies: + debug "4" + ansi-colors@4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-4.1.1.tgz#cbb9ae256bf750af1eab344f229aa27fe94ba348" @@ -653,7 +660,7 @@ dateformat@~1.0.4-1.2.3: get-stdin "^4.0.1" meow "^3.3.0" -debug@4.1.1: +debug@4, debug@4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/debug/-/debug-4.1.1.tgz#3b72260255109c6b589cee050f1d516139664791" integrity sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw== @@ -946,6 +953,14 @@ hosted-git-info@^2.1.4: resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.8.tgz#7539bd4bc1e0e0a895815a2e0262420b12858488" integrity sha512-f/wzC2QaWBs7t9IYqB4T3sR1xviIViXJRJTWBlx2Gf3g0Xi5vI7Yy4koXQ1c9OYDGHN9sBy1DQ2AB8fqZBWhUg== +https-proxy-agent@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz#e2a90542abb68a762e0a0850f6c9edadfd8506b2" + integrity sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA== + dependencies: + agent-base "6" + debug "4" + ieee754@^1.1.4: version "1.1.13" resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.1.13.tgz#ec168558e95aa181fd87d37f55c32bbcb6708b84"