whatsapp: route Baileys socket and version refresh through proxy

This commit is contained in:
nando 2026-03-19 00:13:31 +00:00
parent bea90b72e6
commit 46843211c8
2 changed files with 176 additions and 1 deletions

View File

@ -5,6 +5,41 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { resetLogger, setLoggerOverride } from "../../../src/logging.js";
import { baileys, getLastSocket, resetBaileysMocks, resetLoadConfigMock } from "./test-helpers.js";
const { httpsRequestMock, httpsProxyAgentSpy, undiciProxyAgentSpy } = vi.hoisted(() => ({
httpsRequestMock: vi.fn(),
httpsProxyAgentSpy: vi.fn(),
undiciProxyAgentSpy: vi.fn(),
}));
vi.mock("node:https", () => ({
request: httpsRequestMock,
}));
vi.mock("https-proxy-agent", () => ({
HttpsProxyAgent: vi.fn(function MockHttpsProxyAgent(
this: { proxyUrl: string },
proxyUrl: string,
) {
this.proxyUrl = proxyUrl;
httpsProxyAgentSpy(proxyUrl);
}),
}));
vi.mock("undici", async (importOriginal) => {
const actual = await importOriginal<typeof import("undici")>();
class MockProxyAgent {
proxyUrl: string;
constructor(proxyUrl: string) {
this.proxyUrl = proxyUrl;
undiciProxyAgentSpy(proxyUrl);
}
}
return {
...actual,
ProxyAgent: MockProxyAgent,
};
});
const { createWaSocket, formatError, logWebSelfId, waitForWaConnection } =
await import("./session.js");
const useMultiFileAuthStateMock = vi.mocked(baileys.useMultiFileAuthState);
@ -59,6 +94,10 @@ describe("web session", () => {
vi.clearAllMocks();
resetBaileysMocks();
resetLoadConfigMock();
delete process.env.http_proxy;
delete process.env.https_proxy;
delete process.env.HTTP_PROXY;
delete process.env.HTTPS_PROXY;
});
afterEach(() => {
@ -85,6 +124,58 @@ describe("web session", () => {
expect(saveCreds).toHaveBeenCalled();
});
it("adds explicit proxy agents when https proxy env is configured", async () => {
process.env.HTTPS_PROXY = "http://proxy.test:3128";
await createWaSocket(false, false);
const makeWASocket = baileys.makeWASocket as ReturnType<typeof vi.fn>;
const passed = makeWASocket.mock.calls[0]?.[0] as
| { agent?: unknown; fetchAgent?: unknown }
| undefined;
expect(passed?.agent).toBeDefined();
expect(passed?.fetchAgent).toBeDefined();
expect(httpsProxyAgentSpy).toHaveBeenCalledWith("http://proxy.test:3128");
expect(undiciProxyAgentSpy).toHaveBeenCalledWith("http://proxy.test:3128");
});
it("refreshes the WhatsApp Web version through the proxy when Baileys falls back", async () => {
process.env.HTTPS_PROXY = "http://proxy.test:3128";
vi.mocked(baileys.fetchLatestBaileysVersion).mockResolvedValueOnce({
version: [2, 3000, 1027934701],
isLatest: false,
});
httpsRequestMock.mockImplementationOnce(
(
_url: string,
_opts: Record<string, unknown>,
cb: (res: EventEmitter & { statusCode?: number }) => void,
) => {
const res = new EventEmitter() as EventEmitter & { statusCode?: number };
res.statusCode = 200;
const req = new EventEmitter() as EventEmitter & {
end: () => void;
destroy: (_err?: Error) => void;
};
req.end = () => {
cb(res);
res.emit("data", 'self.client_revision="1035441841";');
res.emit("end");
};
req.destroy = () => {};
return req;
},
);
await createWaSocket(false, false);
const makeWASocket = baileys.makeWASocket as ReturnType<typeof vi.fn>;
const passed = makeWASocket.mock.calls[0]?.[0] as { version?: unknown } | undefined;
expect(passed?.version).toEqual([2, 3000, 1035441841]);
expect(httpsRequestMock).toHaveBeenCalled();
});
it("waits for connection open", async () => {
const ev = new EventEmitter();
const promise = waitForWaConnection({ ev } as unknown as ReturnType<

View File

@ -1,5 +1,6 @@
import { randomUUID } from "node:crypto";
import fsSync from "node:fs";
import { request as httpsRequest } from "node:https";
import {
DisconnectReason,
fetchLatestBaileysVersion,
@ -7,12 +8,15 @@ import {
makeWASocket,
useMultiFileAuthState,
} from "@whiskeysockets/baileys";
import { HttpsProxyAgent } from "https-proxy-agent";
import { formatCliCommand } from "openclaw/plugin-sdk/cli-runtime";
import { VERSION } from "openclaw/plugin-sdk/cli-runtime";
import { danger, success } from "openclaw/plugin-sdk/runtime-env";
import { getChildLogger, toPinoLikeLogger } from "openclaw/plugin-sdk/runtime-env";
import { ensureDir, resolveUserPath } from "openclaw/plugin-sdk/text-runtime";
import qrcode from "qrcode-terminal";
import { ProxyAgent } from "undici";
import { resolveEnvHttpProxyUrl } from "../../../src/infra/net/proxy-env.js";
import {
maybeRestoreCredsFromBackup,
readCredsJsonRaw,
@ -34,6 +38,8 @@ export {
// Per-authDir queues so multi-account creds saves don't block each other.
const credsSaveQueues = new Map<string, Promise<void>>();
const CREDS_SAVE_FLUSH_TIMEOUT_MS = 15_000;
const WHATSAPP_WEB_SW_URL = "https://web.whatsapp.com/sw.js";
function enqueueSaveCreds(
authDir: string,
saveCreds: () => Promise<void> | void,
@ -90,6 +96,79 @@ async function safeSaveCreds(
}
}
function extractWhatsAppWebVersion(source: string): [number, number, number] | null {
const match = source.match(/client_revision[^0-9]*(\d+)/);
if (!match) {
return null;
}
const revision = Number.parseInt(match[1] ?? "", 10);
if (!Number.isFinite(revision) || revision <= 0) {
return null;
}
return [2, 3000, revision];
}
async function fetchLatestWhatsAppWebVersionViaProxy(
proxyUrl: string,
): Promise<[number, number, number] | null> {
return await new Promise((resolve) => {
const agent = new HttpsProxyAgent(proxyUrl);
const req = httpsRequest(
WHATSAPP_WEB_SW_URL,
{
agent,
timeout: 10_000,
},
(res) => {
const chunks: Buffer[] = [];
res.on("data", (chunk: string | Buffer) => {
chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
});
res.on("end", () => {
const statusCode = typeof res.statusCode === "number" ? res.statusCode : 0;
if (statusCode < 200 || statusCode >= 300) {
resolve(null);
return;
}
resolve(extractWhatsAppWebVersion(Buffer.concat(chunks).toString("utf8")));
});
},
);
req.on("timeout", () => {
req.destroy(new Error("Timed out fetching WhatsApp Web version"));
});
req.on("error", () => resolve(null));
req.end();
});
}
async function resolveWhatsAppWebVersion(
sessionLogger: ReturnType<typeof getChildLogger>,
): Promise<[number, number, number]> {
const latest = await fetchLatestBaileysVersion();
if (latest.isLatest) {
return latest.version;
}
const proxyUrl = resolveEnvHttpProxyUrl("https");
if (!proxyUrl) {
return latest.version;
}
const proxyVersion = await fetchLatestWhatsAppWebVersionViaProxy(proxyUrl);
if (!proxyVersion) {
return latest.version;
}
sessionLogger.info(
{
version: proxyVersion,
},
"using proxy-refreshed WhatsApp Web version",
);
return proxyVersion;
}
/**
* Create a Baileys socket backed by the multi-file auth store we keep on disk.
* Consumers can opt into QR printing for interactive login flows.
@ -111,7 +190,10 @@ export async function createWaSocket(
const sessionLogger = getChildLogger({ module: "web-session" });
maybeRestoreCredsFromBackup(authDir);
const { state, saveCreds } = await useMultiFileAuthState(authDir);
const { version } = await fetchLatestBaileysVersion();
const version = await resolveWhatsAppWebVersion(sessionLogger);
const proxyUrl = resolveEnvHttpProxyUrl("https");
const wsAgent = proxyUrl ? new HttpsProxyAgent(proxyUrl) : undefined;
const fetchAgent = proxyUrl ? new ProxyAgent(proxyUrl) : undefined;
const sock = makeWASocket({
auth: {
creds: state.creds,
@ -123,6 +205,8 @@ export async function createWaSocket(
browser: ["openclaw", "cli", VERSION],
syncFullHistory: false,
markOnlineOnConnect: false,
...(wsAgent ? { agent: wsAgent } : {}),
...(fetchAgent ? { fetchAgent } : {}),
});
sock.ev.on("creds.update", () => enqueueSaveCreds(authDir, saveCreds, sessionLogger));