返回 JoyAI-Echo
server.ts
1 /**
2 * WebSocket server for Python-Node.js bridge communication.
3 * Security: binds to 127.0.0.1 only; requires BRIDGE_TOKEN auth; rejects browser Origin headers.
4 */
5
6 import { WebSocketServer, WebSocket } from 'ws';
7 import { WhatsAppClient, InboundMessage } from './whatsapp.js';
8
9 interface SendCommand {
10 type: 'send';
11 to: string;
12 text: string;
13 }
14
15 interface SendMediaCommand {
16 type: 'send_media';
17 to: string;
18 filePath: string;
19 mimetype: string;
20 caption?: string;
21 fileName?: string;
22 }
23
24 type BridgeCommand = SendCommand | SendMediaCommand;
25
26 interface BridgeMessage {
27 type: 'message' | 'status' | 'qr' | 'error';
28 [key: string]: unknown;
29 }
30
31 export class BridgeServer {
32 private wss: WebSocketServer | null = null;
33 private wa: WhatsAppClient | null = null;
34 private clients: Set<WebSocket> = new Set();
35
36 constructor(private port: number, private authDir: string, private token: string) {}
37
38 async start(): Promise<void> {
39 if (!this.token.trim()) {
40 throw new Error('BRIDGE_TOKEN is required');
41 }
42
43 // Bind to localhost only — never expose to external network
44 this.wss = new WebSocketServer({
45 host: '127.0.0.1',
46 port: this.port,
47 verifyClient: (info, done) => {
48 const origin = info.origin || info.req.headers.origin;
49 if (origin) {
50 console.warn(`Rejected WebSocket connection with Origin header: ${origin}`);
51 done(false, 403, 'Browser-originated WebSocket connections are not allowed');
52 return;
53 }
54 done(true);
55 },
56 });
57 console.log(`🌉 Bridge server listening on ws://127.0.0.1:${this.port}`);
58 console.log('🔒 Token authentication enabled');
59
60 // Initialize WhatsApp client
61 this.wa = new WhatsAppClient({
62 authDir: this.authDir,
63 onMessage: (msg) => this.broadcast({ type: 'message', ...msg }),
64 onQR: (qr) => this.broadcast({ type: 'qr', qr }),
65 onStatus: (status) => this.broadcast({ type: 'status', status }),
66 });
67
68 // Handle WebSocket connections
69 this.wss.on('connection', (ws) => {
70 // Require auth handshake as first message
71 const timeout = setTimeout(() => ws.close(4001, 'Auth timeout'), 5000);
72 ws.once('message', (data) => {
73 clearTimeout(timeout);
74 try {
75 const msg = JSON.parse(data.toString());
76 if (msg.type === 'auth' && msg.token === this.token) {
77 console.log('🔗 Python client authenticated');
78 this.setupClient(ws);
79 } else {
80 ws.close(4003, 'Invalid token');
81 }
82 } catch {
83 ws.close(4003, 'Invalid auth message');
84 }
85 });
86 });
87
88 // Connect to WhatsApp
89 await this.wa.connect();
90 }
91
92 private setupClient(ws: WebSocket): void {
93 this.clients.add(ws);
94
95 ws.on('message', async (data) => {
96 try {
97 const cmd = JSON.parse(data.toString()) as BridgeCommand;
98 await this.handleCommand(cmd);
99 ws.send(JSON.stringify({ type: 'sent', to: cmd.to }));
100 } catch (error) {
101 console.error('Error handling command:', error);
102 ws.send(JSON.stringify({ type: 'error', error: String(error) }));
103 }
104 });
105
106 ws.on('close', () => {
107 console.log('🔌 Python client disconnected');
108 this.clients.delete(ws);
109 });
110
111 ws.on('error', (error) => {
112 console.error('WebSocket error:', error);
113 this.clients.delete(ws);
114 });
115 }
116
117 private async handleCommand(cmd: BridgeCommand): Promise<void> {
118 if (!this.wa) return;
119
120 if (cmd.type === 'send') {
121 await this.wa.sendMessage(cmd.to, cmd.text);
122 } else if (cmd.type === 'send_media') {
123 await this.wa.sendMedia(cmd.to, cmd.filePath, cmd.mimetype, cmd.caption, cmd.fileName);
124 }
125 }
126
127 private broadcast(msg: BridgeMessage): void {
128 const data = JSON.stringify(msg);
129 for (const client of this.clients) {
130 if (client.readyState === WebSocket.OPEN) {
131 client.send(data);
132 }
133 }
134 }
135
136 async stop(): Promise<void> {
137 // Close all client connections
138 for (const client of this.clients) {
139 client.close();
140 }
141 this.clients.clear();
142
143 // Close WebSocket server
144 if (this.wss) {
145 this.wss.close();
146 this.wss = null;
147 }
148
149 // Disconnect WhatsApp
150 if (this.wa) {
151 await this.wa.disconnect();
152 this.wa = null;
153 }
154 }
155 }
156
156 lines TYPESCRIPT