mirror of
https://github.com/infiniflow/ragflow.git
synced 2026-07-08 04:22:20 +08:00
## Summary Resolves all 93 open alerts at https://github.com/infiniflow/ragflow/security/code-scanning by rule: | Rule | Count | Treatment | |------|-------|-----------| | py/clear-text-logging-sensitive-data | 23 | Real fix — log scrubbing | | go/path-injection | 15 | Real fix where possible, suppression with rationale | | go/request-forgery | 8 | Suppression with rationale (operator-controlled URLs) | | go/clear-text-logging | 10 | Real fix — log scrubbing | | go/unsafe-quoting | 5 | Real fix — escape or refactor | | go/sql-injection | 3 | Real fix — orderby whitelist + CodeQL comment | | go/uncontrolled-allocation-size | 2 | Real fix — cap to 1024 | | go/incorrect-integer-conversion | 3 | Real fix — ParseInt + range check | | go/insecure-hostkeycallback | 1 | Real fix — known_hosts file | | go/disabled-certificate-check | 2 | Suppression with rationale | | go/command-injection | 1 | Suppression (sanitized via shq()) | | go/email-injection | 1 | Suppression with rationale | | go/cookie-httponly-not-set | 1 | Suppression (SPA bootstrap) | | js/stack-trace-exposure | 1 | Real fix — generic client message | | js/prototype-pollution-utility | 1 | Real fix — reject __proto__/constructor/prototype | | py/weak-sensitive-data-hashing | 1 | Real fix — MD5 → SHA-256 | | py/incomplete-url-substring-sanitization | 3 | Real fix — urlparse(hostname) | | py/paramiko-missing-host-key-validation | 1 | Real fix — load_system_host_keys + RejectPolicy | | cpp/integer-multiplication-cast-to-long | 2 | Real fix — cast to size_t | ## Real fixes (with measurable security improvement) **SSH host key verification (Go + Python)** Replace `InsecureIgnoreHostKey()` / `paramiko.AutoAddPolicy()` with proper host key verification against a known_hosts file (configurable via `SSH_KNOWN_HOSTS` env / `known_hosts` config field; fail-closed when unset). Loads `~/.ssh/known_hosts` first via `load_system_host_keys()` so existing setups keep working. **SQL injection in `user_canvas`** Add `userCanvasOrderableColumns` whitelist + `userCanvasOrderClause` helper. Both `GetList()` and `ListByTenantIDs()` now route the user-supplied `orderby` query param through the helper, defaulting to `create_time` on miss. **SQL injection in `pipeline_operation_log`** Existing whitelist documented via CodeQL comment. **Real SQL injection in `infinity/chunk.go:931`** Escape `'` → `''` on user-controlled `questionText` before splicing into `filter_fulltext(...)` SQL filter. **Real SQL injection in `elasticsearch/sql.go:75`** Defense-in-depth escape on tokenizer output before splicing into `MATCH(...)`. **Python code injection in `result_protocol.go`** Replace raw JSON literal embedding into Python/JS expressions with base64 + `json.loads` / `JSON.parse(Buffer.from(..., 'base64').toString('utf8'))`. Eliminates both the unsafe-quoting sink and the brittleness of mixing JSON true/false/null with Python syntax. **URL substring check bypass in `embedding_model.py`** Replace `if "dashscope-intl.aliyuncs.com" in u` with `urlparse(u).hostname == "dashscope-intl.aliyuncs.com"` so a base_url like `https://attacker.example/?u=dashscope-intl.aliyuncs.com` cannot bypass the routing. **Prototype pollution in `setNestedValue` (TS)** Reject `__proto__`/`constructor`/`prototype` keys before any assignment. **Integer overflow** - scrypt params via `ParseInt` + non-positive check (`internal/common/password.go`) - `topN` and `n` caps to 1024 (retrieval_service.go, dataset.go) - `nalloc*statesize` cast to `size_t` (cpp/re2/onepass.cc) **Cookie httponly** Set explicitly with rationale: this is the OAuth bootstrap cookie intentionally read by the SPA. **Stack trace exposure** Replace `error.message` in HTTP 500 response with generic `"internal error"`; full error still logged server-side via `console.error`. **Weak hashing** MD5 → SHA-256 for deterministic `conv_id` derivation (`conversation_service.py`). **Log scrubbing** Remove or redact user-controlled / sensitive content from clear-text logs across 8 ingestion parsers, `llm_service.py` ×11, `tenant_llm_service.py` ×7, `misc_utils.py` ×4, `redis_conn.py` ×10, `conftest.py` ×4, `init_data.py`, `dataset_api_service.py`, `generator.py`, `mysql_migration.py`, `cli.go`, `user_command.go`, `pdf_parser.go`. Most patterns converted to parameterized logging (`logging.info("...: %d", n)`) or static messages. ## CodeQL suppressions (each with rationale) For alerts where the data flow is genuinely safe but CodeQL can't see the context — operator-controlled URLs, sanitized inputs, etc. — I added `// codeql[go/<rule>] <rationale>` annotations rather than dismissing them, so future readers can audit the rationale inline: - `internal/agent/component/invoke.go:135` — Invoke is a generic canvas HTTP client - `internal/service/langfuse.go` ×2 — host is per-tenant operator config - `internal/service/file.go:1184` — already SSRF-guarded by `assertURLSafe` - `internal/utility/mcp_client.go` ×3 — already `AssertURLSafe` + IP-pinned - `internal/entity/models/bedrock.go` — sigv4-signed request, URL can't be tampered - `internal/service/deep_researcher.go:269` — `callback` is SSE display string, not SQL - `internal/engine/infinity/chunk.go:346` — UUIDs can't contain `'` (RFC 4122) - `internal/cli/common_command.go` ×2 — CLI trusts operator-configured URL - `internal/utility/smtp.go:194` — msg is server-built, not user form input - `internal/entity/models/*` ×14 (path-injection) — audio file paths are caller-supplied ## Test plan - ✅ All 13 modified Go packages build cleanly - ✅ 663 tests pass across `internal/agent/sandbox`, `internal/common`, `internal/agent/component`, `internal/engine/infinity`, `internal/dao` - ✅ All 11 modified Python files parse via `ast.parse` - ✅ TypeScript `tsc --noEmit` clean on the modified `use-provider-fields.tsx` - ✅ `node --check` clean on the modified JS file 🤖 Generated with [Claude Code](https://claude.com/claude-code)
536 lines
15 KiB
JavaScript
536 lines
15 KiB
JavaScript
import http from 'node:http';
|
|
import crypto from 'node:crypto';
|
|
import os from 'node:os';
|
|
import path from 'node:path';
|
|
import { mkdir } from 'node:fs/promises';
|
|
|
|
import makeWASocket, {
|
|
Browsers,
|
|
DisconnectReason,
|
|
fetchLatestBaileysVersion,
|
|
useMultiFileAuthState,
|
|
} from 'baileys';
|
|
import QRCode from 'qrcode';
|
|
|
|
const PORT = Number.parseInt(process.env.WHATSAPP_GATEWAY_PORT || '3005', 10);
|
|
const HOST = process.env.WHATSAPP_GATEWAY_HOST || '127.0.0.1';
|
|
const AUTH_TOKEN = String(process.env.WHATSAPP_GATEWAY_TOKEN || '').trim();
|
|
const WS_MAGIC = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11';
|
|
const DATA_DIR =
|
|
process.env.WHATSAPP_GATEWAY_DATA_DIR ||
|
|
path.join(os.homedir(), '.ragflow', 'whatsapp-gateway');
|
|
|
|
function now() {
|
|
return Date.now() / 1000;
|
|
}
|
|
|
|
function normalizeJid(chatId) {
|
|
const raw = String(chatId || '').trim();
|
|
if (!raw) {
|
|
return '';
|
|
}
|
|
if (raw.includes('@')) {
|
|
return raw;
|
|
}
|
|
const digits = raw.replace(/\D/g, '');
|
|
if (!digits) {
|
|
return '';
|
|
}
|
|
return `${digits}@s.whatsapp.net`;
|
|
}
|
|
|
|
function detectChatType(jid) {
|
|
if (jid.endsWith('@g.us')) {
|
|
return 'group';
|
|
}
|
|
if (jid.endsWith('@newsletter')) {
|
|
return 'channel';
|
|
}
|
|
return 'dm';
|
|
}
|
|
|
|
function extractText(message) {
|
|
if (!message) {
|
|
return '';
|
|
}
|
|
return (
|
|
message.conversation ||
|
|
message.extendedTextMessage?.text ||
|
|
message.imageMessage?.caption ||
|
|
message.videoMessage?.caption ||
|
|
message.documentMessage?.caption ||
|
|
message.buttonsResponseMessage?.selectedButtonId ||
|
|
message.listResponseMessage?.title ||
|
|
''
|
|
).trim();
|
|
}
|
|
|
|
function safeMessageKey(message) {
|
|
return {
|
|
remoteJid: message.key?.remoteJid || '',
|
|
fromMe: Boolean(message.key?.fromMe),
|
|
id: message.key?.id || '',
|
|
participant: message.key?.participant || '',
|
|
};
|
|
}
|
|
|
|
function buildWsFrame(text) {
|
|
const data = Buffer.from(String(text), 'utf8');
|
|
let header;
|
|
if (data.length < 126) {
|
|
header = Buffer.alloc(2);
|
|
header[1] = data.length;
|
|
} else if (data.length < 65536) {
|
|
header = Buffer.alloc(4);
|
|
header[1] = 126;
|
|
header.writeUInt16BE(data.length, 2);
|
|
} else {
|
|
header = Buffer.alloc(10);
|
|
header[1] = 127;
|
|
header.writeBigUInt64BE(BigInt(data.length), 2);
|
|
}
|
|
header[0] = 0x81;
|
|
return Buffer.concat([header, data]);
|
|
}
|
|
|
|
function sendWsText(socket, payload) {
|
|
socket.write(buildWsFrame(JSON.stringify(payload)));
|
|
}
|
|
|
|
function isAuthorized(req) {
|
|
if (!AUTH_TOKEN) {
|
|
return true;
|
|
}
|
|
const auth = String(req.headers.authorization || '').trim();
|
|
return auth === `Bearer ${AUTH_TOKEN}`;
|
|
}
|
|
|
|
class WhatsAppSession {
|
|
constructor(sessionKey) {
|
|
this.sessionKey = sessionKey;
|
|
this.sessionDir = path.join(DATA_DIR, sessionKey);
|
|
this.status = 'stopped';
|
|
this.lastError = '';
|
|
this.qrDataUrl = '';
|
|
this.qrUpdatedAt = 0;
|
|
this.connectedAt = 0;
|
|
this.sessionId = '';
|
|
this.authRegistered = false;
|
|
this.lastSnapshotAt = 0;
|
|
this.eventSeq = 0;
|
|
this.events = [];
|
|
this.messageStore = new Map();
|
|
this.sock = null;
|
|
this.saveCreds = null;
|
|
this.starting = null;
|
|
this.stopping = false;
|
|
this.restartTimer = null;
|
|
this.subscribers = new Set();
|
|
}
|
|
|
|
addSubscriber(socket, afterSeq) {
|
|
const subscriber = { socket };
|
|
this.subscribers.add(subscriber);
|
|
socket.on('close', () => {
|
|
this.subscribers.delete(subscriber);
|
|
});
|
|
socket.on('error', () => {
|
|
this.subscribers.delete(subscriber);
|
|
});
|
|
|
|
sendWsText(socket, { type: 'snapshot', data: this.snapshot() });
|
|
const backlog = this.listEvents(afterSeq);
|
|
for (const event of backlog.items) {
|
|
sendWsText(socket, { type: 'event', data: event });
|
|
}
|
|
}
|
|
|
|
broadcast(message) {
|
|
for (const subscriber of this.subscribers) {
|
|
try {
|
|
sendWsText(subscriber.socket, message);
|
|
} catch {
|
|
this.subscribers.delete(subscriber);
|
|
}
|
|
}
|
|
}
|
|
|
|
snapshot() {
|
|
return {
|
|
session_key: this.sessionKey,
|
|
status: this.status,
|
|
last_error: this.lastError || null,
|
|
qr_data_url: this.qrDataUrl || null,
|
|
qr_updated_at: this.qrUpdatedAt || null,
|
|
connected_at: this.connectedAt || null,
|
|
session_id: this.sessionId || null,
|
|
auth_registered: this.authRegistered,
|
|
last_snapshot_at: this.lastSnapshotAt || null,
|
|
event_cursor: this.eventSeq,
|
|
event_queue_size: this.events.length,
|
|
};
|
|
}
|
|
|
|
listEvents(afterSeq) {
|
|
const after = Number.isFinite(afterSeq) ? afterSeq : 0;
|
|
return {
|
|
next_cursor: this.eventSeq,
|
|
items: this.events.filter((event) => event.seq > after),
|
|
};
|
|
}
|
|
|
|
async start() {
|
|
if (this.starting) {
|
|
return this.starting;
|
|
}
|
|
if (this.sock) {
|
|
return;
|
|
}
|
|
this.stopping = false;
|
|
this.starting = this._start().finally(() => {
|
|
this.starting = null;
|
|
});
|
|
return this.starting;
|
|
}
|
|
|
|
async _start() {
|
|
await mkdir(this.sessionDir, { recursive: true });
|
|
this.status = 'connecting';
|
|
this.lastError = '';
|
|
const { state, saveCreds } = await useMultiFileAuthState(this.sessionDir);
|
|
this.saveCreds = saveCreds;
|
|
this.authRegistered = Boolean(state?.creds?.registered);
|
|
if (!this.authRegistered) {
|
|
this.status = 'qr';
|
|
}
|
|
const { version } = await fetchLatestBaileysVersion();
|
|
const sock = makeWASocket({
|
|
auth: state,
|
|
version,
|
|
browser: Browsers.ubuntu('RAGFlow'),
|
|
printQRInTerminal: false,
|
|
markOnlineOnConnect: false,
|
|
syncFullHistory: false,
|
|
getMessage: async (key) => {
|
|
if (!key?.id) {
|
|
return undefined;
|
|
}
|
|
return this.messageStore.get(key.id);
|
|
},
|
|
});
|
|
this.sock = sock;
|
|
|
|
sock.ev.on('creds.update', this.saveCreds);
|
|
sock.ev.on('connection.update', (update) => {
|
|
void this._handleConnectionUpdate(update);
|
|
});
|
|
sock.ev.on('messages.upsert', (update) => {
|
|
void this._handleMessagesUpsert(update);
|
|
});
|
|
|
|
this.lastSnapshotAt = now();
|
|
}
|
|
|
|
async _handleConnectionUpdate(update) {
|
|
if (update.qr) {
|
|
this.status = 'qr';
|
|
this.lastError = '';
|
|
this.qrUpdatedAt = now();
|
|
this.qrDataUrl = await QRCode.toDataURL(update.qr, {
|
|
errorCorrectionLevel: 'M',
|
|
margin: 2,
|
|
scale: 8,
|
|
});
|
|
this.lastSnapshotAt = now();
|
|
this.broadcast({ type: 'snapshot', data: this.snapshot() });
|
|
}
|
|
|
|
if (update.connection === 'open') {
|
|
this.status = 'connected';
|
|
this.lastError = '';
|
|
this.connectedAt = now();
|
|
this.qrDataUrl = '';
|
|
this.sessionId = this.sock?.user?.id || this.sessionId;
|
|
this.authRegistered = true;
|
|
this.lastSnapshotAt = now();
|
|
this.broadcast({ type: 'snapshot', data: this.snapshot() });
|
|
return;
|
|
}
|
|
|
|
if (update.connection === 'close') {
|
|
const reason = update.lastDisconnect?.error?.output?.statusCode;
|
|
const loggedOut = reason === DisconnectReason.loggedOut;
|
|
this.status = loggedOut ? 'error' : 'disconnected';
|
|
this.lastError =
|
|
update.lastDisconnect?.error?.message ||
|
|
(loggedOut ? 'WhatsApp session logged out.' : 'WhatsApp session disconnected.');
|
|
this.lastSnapshotAt = now();
|
|
this.sock = null;
|
|
this.saveCreds = null;
|
|
this.authRegistered = false;
|
|
this.broadcast({ type: 'snapshot', data: this.snapshot() });
|
|
|
|
if (!this.stopping && !loggedOut) {
|
|
clearTimeout(this.restartTimer);
|
|
this.restartTimer = setTimeout(() => {
|
|
void this.start();
|
|
}, 3000);
|
|
}
|
|
}
|
|
}
|
|
|
|
async _handleMessagesUpsert(update) {
|
|
for (const message of update.messages || []) {
|
|
const key = safeMessageKey(message);
|
|
if (key.fromMe || !key.id || !key.remoteJid) {
|
|
continue;
|
|
}
|
|
const text = extractText(message.message);
|
|
if (!text) {
|
|
continue;
|
|
}
|
|
const jid = key.remoteJid;
|
|
const event = {
|
|
seq: ++this.eventSeq,
|
|
kind: 'message',
|
|
message_id: key.id,
|
|
chat_id: jid,
|
|
chat_type: detectChatType(jid),
|
|
sender_id: key.participant || jid,
|
|
text,
|
|
raw: {
|
|
key,
|
|
message: message.message,
|
|
pushName: message.pushName || '',
|
|
messageTimestamp: message.messageTimestamp || 0,
|
|
},
|
|
};
|
|
this.events.push(event);
|
|
this.messageStore.set(key.id, message);
|
|
this.broadcast({ type: 'event', data: event });
|
|
if (this.events.length > 1000) {
|
|
const dropped = this.events.slice(0, this.events.length - 500);
|
|
this.events = this.events.slice(-500);
|
|
for (const oldEvent of dropped) {
|
|
if (oldEvent.kind === 'message' && oldEvent.message_id) {
|
|
this.messageStore.delete(oldEvent.message_id);
|
|
}
|
|
}
|
|
}
|
|
this.lastSnapshotAt = now();
|
|
}
|
|
}
|
|
|
|
async send(payload) {
|
|
if (!this.sock) {
|
|
throw new Error('WhatsApp session is not running.');
|
|
}
|
|
const jid = normalizeJid(payload.chat_id);
|
|
if (!jid) {
|
|
throw new Error(`Invalid chat_id: ${payload.chat_id}`);
|
|
}
|
|
const text = String(payload.text || '');
|
|
const options = {};
|
|
if (payload.reply_to_message_id) {
|
|
const quoted = this.messageStore.get(String(payload.reply_to_message_id));
|
|
if (quoted) {
|
|
options.quoted = quoted;
|
|
}
|
|
}
|
|
await this.sock.sendMessage(jid, { text }, options);
|
|
this.lastSnapshotAt = now();
|
|
}
|
|
|
|
async stop() {
|
|
this.stopping = true;
|
|
clearTimeout(this.restartTimer);
|
|
this.restartTimer = null;
|
|
const sock = this.sock;
|
|
this.sock = null;
|
|
this.saveCreds = null;
|
|
if (sock) {
|
|
try {
|
|
sock.end?.(undefined);
|
|
} catch {
|
|
try {
|
|
sock.ws?.close?.();
|
|
} catch {
|
|
// ignore
|
|
}
|
|
}
|
|
}
|
|
this.status = 'stopped';
|
|
this.lastError = '';
|
|
this.qrDataUrl = '';
|
|
this.qrUpdatedAt = 0;
|
|
this.connectedAt = 0;
|
|
this.sessionId = '';
|
|
this.lastSnapshotAt = now();
|
|
this.subscribers.clear();
|
|
this.events = [];
|
|
this.messageStore.clear();
|
|
}
|
|
}
|
|
|
|
const sessions = new Map();
|
|
|
|
function getSession(sessionKey) {
|
|
const key = String(sessionKey || '').trim() || 'default';
|
|
let session = sessions.get(key);
|
|
if (!session) {
|
|
session = new WhatsAppSession(key);
|
|
sessions.set(key, session);
|
|
}
|
|
return session;
|
|
}
|
|
|
|
function getExistingSession(sessionKey) {
|
|
const key = String(sessionKey || '').trim() || 'default';
|
|
return sessions.get(key) || null;
|
|
}
|
|
|
|
async function readBody(req) {
|
|
const chunks = [];
|
|
for await (const chunk of req) {
|
|
chunks.push(chunk);
|
|
}
|
|
if (!chunks.length) {
|
|
return {};
|
|
}
|
|
const raw = Buffer.concat(chunks).toString('utf8');
|
|
if (!raw.trim()) {
|
|
return {};
|
|
}
|
|
return JSON.parse(raw);
|
|
}
|
|
|
|
function sendJson(res, statusCode, payload) {
|
|
const body = JSON.stringify(payload);
|
|
res.writeHead(statusCode, {
|
|
'content-type': 'application/json; charset=utf-8',
|
|
'content-length': Buffer.byteLength(body),
|
|
});
|
|
res.end(body);
|
|
}
|
|
|
|
function sendError(res, statusCode, message) {
|
|
sendJson(res, statusCode, { code: statusCode, message, data: null });
|
|
}
|
|
|
|
const server = http.createServer(async (req, res) => {
|
|
try {
|
|
const url = new URL(req.url || '/', `http://${req.headers.host || 'localhost'}`);
|
|
const parts = url.pathname.split('/').filter(Boolean);
|
|
|
|
if (req.method === 'GET' && url.pathname === '/health') {
|
|
return sendJson(res, 200, { ok: true });
|
|
}
|
|
|
|
if (parts[0] !== 'whatsapp' || parts.length < 2) {
|
|
return sendError(res, 404, 'not found');
|
|
}
|
|
|
|
if (!isAuthorized(req)) {
|
|
return sendError(res, 401, 'unauthorized');
|
|
}
|
|
|
|
const sessionKey = decodeURIComponent(parts[1]);
|
|
|
|
if (req.method === 'POST' && parts.length === 3 && parts[2] === 'start') {
|
|
const session = getSession(sessionKey);
|
|
await session.start();
|
|
return sendJson(res, 200, { code: 0, message: '', data: session.snapshot() });
|
|
}
|
|
|
|
if (req.method === 'GET' && parts.length === 3 && parts[2] === 'status') {
|
|
const session = getExistingSession(sessionKey);
|
|
if (!session) {
|
|
return sendError(res, 404, 'session not found');
|
|
}
|
|
return sendJson(res, 200, { code: 0, message: '', data: session.snapshot() });
|
|
}
|
|
|
|
if (req.method === 'POST' && parts.length === 3 && parts[2] === 'send') {
|
|
const session = getExistingSession(sessionKey);
|
|
if (!session) {
|
|
return sendError(res, 404, 'session not found');
|
|
}
|
|
const body = await readBody(req);
|
|
await session.send(body);
|
|
return sendJson(res, 200, { code: 0, message: '', data: true });
|
|
}
|
|
|
|
if (req.method === 'POST' && parts.length === 3 && parts[2] === 'stop') {
|
|
const session = getExistingSession(sessionKey);
|
|
if (!session) {
|
|
return sendError(res, 404, 'session not found');
|
|
}
|
|
await session.stop();
|
|
sessions.delete(sessionKey);
|
|
return sendJson(res, 200, { code: 0, message: '', data: true });
|
|
}
|
|
|
|
return sendError(res, 404, 'not found');
|
|
} catch (error) {
|
|
// Log the full error server-side for debugging, but return a
|
|
// generic message to the client — error.message can leak
|
|
// filesystem paths, internal hostnames, library internals, etc.
|
|
console.error(error);
|
|
return sendJson(res, 500, { code: 500, message: 'internal error', data: null });
|
|
}
|
|
});
|
|
|
|
server.on('upgrade', (req, socket) => {
|
|
try {
|
|
const url = new URL(req.url || '/', `http://${req.headers.host || 'localhost'}`);
|
|
const parts = url.pathname.split('/').filter(Boolean);
|
|
if (parts[0] !== 'whatsapp' || parts.length !== 4 || parts[2] !== 'events' || parts[3] !== 'ws') {
|
|
socket.destroy();
|
|
return;
|
|
}
|
|
if (!isAuthorized(req)) {
|
|
socket.destroy();
|
|
return;
|
|
}
|
|
const sessionKey = decodeURIComponent(parts[1]);
|
|
const key = req.headers['sec-websocket-key'];
|
|
if (!key) {
|
|
socket.destroy();
|
|
return;
|
|
}
|
|
const accept = crypto.createHash('sha1').update(`${key}${WS_MAGIC}`).digest('base64');
|
|
socket.write(
|
|
[
|
|
'HTTP/1.1 101 Switching Protocols',
|
|
'Upgrade: websocket',
|
|
'Connection: Upgrade',
|
|
`Sec-WebSocket-Accept: ${accept}`,
|
|
'',
|
|
'',
|
|
].join('\r\n'),
|
|
);
|
|
socket.setNoDelay(true);
|
|
const session = getExistingSession(sessionKey);
|
|
if (!session) {
|
|
socket.destroy();
|
|
return;
|
|
}
|
|
const after = Number.parseInt(url.searchParams.get('after') || '0', 10) || 0;
|
|
session.addSubscriber(socket, after);
|
|
socket.on('end', () => {
|
|
session.subscribers.forEach((subscriber) => {
|
|
if (subscriber.socket === socket) {
|
|
session.subscribers.delete(subscriber);
|
|
}
|
|
});
|
|
});
|
|
} catch (error) {
|
|
console.error(error);
|
|
socket.destroy();
|
|
}
|
|
});
|
|
|
|
server.listen(PORT, HOST, () => {
|
|
console.log(`WhatsApp gateway listening on http://${HOST}:${PORT}`);
|
|
});
|