/** * @ruvector/edge-net WebRTC Signaling Server * * Real signaling server for WebRTC peer connections * Enables true P2P connections between nodes * * @module @ruvector/edge-net/signaling */ import { EventEmitter } from 'events'; import { createServer } from 'http'; import { randomBytes, createHash } from 'crypto'; // ============================================ // SIGNALING SERVER // ============================================ /** * WebRTC Signaling Server * Routes offers, answers, and ICE candidates between peers */ export class SignalingServer extends EventEmitter { constructor(options = {}) { super(); this.port = options.port || 8765; this.server = null; this.wss = null; this.peers = new Map(); // peerId -> { ws, info, rooms } this.rooms = new Map(); // roomId -> Set this.pendingOffers = new Map(); // offerId -> { from, to, offer } this.stats = { connections: 0, messages: 0, offers: 0, answers: 0, iceCandidates: 0, }; } /** * Start the signaling server */ async start() { return new Promise(async (resolve, reject) => { try { // Create HTTP server this.server = createServer((req, res) => { if (req.url === '/health') { res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ status: 'ok', peers: this.peers.size })); } else if (req.url === '/stats') { res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify(this.getStats())); } else { res.writeHead(404); res.end('Not found'); } }); // Create WebSocket server const { WebSocketServer } = await import('ws'); this.wss = new WebSocketServer({ server: this.server }); this.wss.on('connection', (ws, req) => { this.handleConnection(ws, req); }); this.server.listen(this.port, () => { console.log(`[Signaling] Server running on port ${this.port}`); this.emit('ready', { port: this.port }); resolve(this); }); } catch (error) { reject(error); } }); } /** * Handle new WebSocket connection */ handleConnection(ws, req) { const peerId = `peer-${randomBytes(8).toString('hex')}`; const peerInfo = { id: peerId, ws, info: {}, rooms: new Set(), connectedAt: Date.now(), lastSeen: Date.now(), }; this.peers.set(peerId, peerInfo); this.stats.connections++; // Send welcome message this.sendTo(peerId, { type: 'welcome', peerId, serverTime: Date.now(), }); ws.on('message', (data) => { try { const message = JSON.parse(data.toString()); this.handleMessage(peerId, message); } catch (error) { console.error('[Signaling] Invalid message:', error.message); } }); ws.on('close', () => { this.handleDisconnect(peerId); }); ws.on('error', (error) => { console.error(`[Signaling] Peer ${peerId} error:`, error.message); }); this.emit('peer-connected', { peerId }); } /** * Handle incoming message from peer */ handleMessage(peerId, message) { const peer = this.peers.get(peerId); if (!peer) return; peer.lastSeen = Date.now(); this.stats.messages++; switch (message.type) { case 'register': this.handleRegister(peerId, message); break; case 'join-room': this.handleJoinRoom(peerId, message); break; case 'leave-room': this.handleLeaveRoom(peerId, message); break; case 'offer': this.handleOffer(peerId, message); break; case 'answer': this.handleAnswer(peerId, message); break; case 'ice-candidate': this.handleIceCandidate(peerId, message); break; case 'discover': this.handleDiscover(peerId, message); break; case 'broadcast': this.handleBroadcast(peerId, message); break; case 'ping': this.sendTo(peerId, { type: 'pong', timestamp: Date.now() }); break; default: console.log(`[Signaling] Unknown message type: ${message.type}`); } } /** * Handle peer registration */ handleRegister(peerId, message) { const peer = this.peers.get(peerId); if (!peer) return; peer.info = { nodeId: message.nodeId, capabilities: message.capabilities || [], publicKey: message.publicKey, region: message.region, }; this.sendTo(peerId, { type: 'registered', peerId, info: peer.info, }); this.emit('peer-registered', { peerId, info: peer.info }); } /** * Handle room join */ handleJoinRoom(peerId, message) { const roomId = message.roomId || 'default'; const peer = this.peers.get(peerId); if (!peer) return; // Create room if doesn't exist if (!this.rooms.has(roomId)) { this.rooms.set(roomId, new Set()); } const room = this.rooms.get(roomId); room.add(peerId); peer.rooms.add(roomId); // Get existing peers in room const existingPeers = Array.from(room) .filter(id => id !== peerId) .map(id => { const p = this.peers.get(id); return { peerId: id, info: p?.info }; }); // Notify joining peer of existing peers this.sendTo(peerId, { type: 'room-joined', roomId, peers: existingPeers, }); // Notify existing peers of new peer for (const otherPeerId of room) { if (otherPeerId !== peerId) { this.sendTo(otherPeerId, { type: 'peer-joined', roomId, peerId, info: peer.info, }); } } this.emit('room-join', { roomId, peerId }); } /** * Handle room leave */ handleLeaveRoom(peerId, message) { const roomId = message.roomId; const peer = this.peers.get(peerId); if (!peer) return; const room = this.rooms.get(roomId); if (!room) return; room.delete(peerId); peer.rooms.delete(roomId); // Notify other peers for (const otherPeerId of room) { this.sendTo(otherPeerId, { type: 'peer-left', roomId, peerId, }); } // Clean up empty room if (room.size === 0) { this.rooms.delete(roomId); } } /** * Handle WebRTC offer */ handleOffer(peerId, message) { this.stats.offers++; const targetPeerId = message.to; const target = this.peers.get(targetPeerId); if (!target) { this.sendTo(peerId, { type: 'error', error: 'Peer not found', targetPeerId, }); return; } // Forward offer to target this.sendTo(targetPeerId, { type: 'offer', from: peerId, offer: message.offer, connectionId: message.connectionId, }); this.emit('offer', { from: peerId, to: targetPeerId }); } /** * Handle WebRTC answer */ handleAnswer(peerId, message) { this.stats.answers++; const targetPeerId = message.to; const target = this.peers.get(targetPeerId); if (!target) return; // Forward answer to target this.sendTo(targetPeerId, { type: 'answer', from: peerId, answer: message.answer, connectionId: message.connectionId, }); this.emit('answer', { from: peerId, to: targetPeerId }); } /** * Handle ICE candidate */ handleIceCandidate(peerId, message) { this.stats.iceCandidates++; const targetPeerId = message.to; const target = this.peers.get(targetPeerId); if (!target) return; // Forward ICE candidate to target this.sendTo(targetPeerId, { type: 'ice-candidate', from: peerId, candidate: message.candidate, connectionId: message.connectionId, }); } /** * Handle peer discovery request */ handleDiscover(peerId, message) { const capabilities = message.capabilities || []; const limit = message.limit || 10; const matches = []; for (const [id, peer] of this.peers) { if (id === peerId) continue; // Check capability match if (capabilities.length > 0) { const peerCaps = peer.info.capabilities || []; const hasMatch = capabilities.some(cap => peerCaps.includes(cap)); if (!hasMatch) continue; } matches.push({ peerId: id, info: peer.info, lastSeen: peer.lastSeen, }); if (matches.length >= limit) break; } this.sendTo(peerId, { type: 'discover-result', peers: matches, total: this.peers.size - 1, }); } /** * Handle broadcast to room */ handleBroadcast(peerId, message) { const roomId = message.roomId; const room = this.rooms.get(roomId); if (!room) return; for (const otherPeerId of room) { if (otherPeerId !== peerId) { this.sendTo(otherPeerId, { type: 'broadcast', from: peerId, roomId, data: message.data, }); } } } /** * Handle peer disconnect */ handleDisconnect(peerId) { const peer = this.peers.get(peerId); if (!peer) return; // Leave all rooms for (const roomId of peer.rooms) { const room = this.rooms.get(roomId); if (room) { room.delete(peerId); // Notify other peers for (const otherPeerId of room) { this.sendTo(otherPeerId, { type: 'peer-left', roomId, peerId, }); } // Clean up empty room if (room.size === 0) { this.rooms.delete(roomId); } } } this.peers.delete(peerId); this.emit('peer-disconnected', { peerId }); } /** * Send message to peer */ sendTo(peerId, message) { const peer = this.peers.get(peerId); if (peer && peer.ws.readyState === 1) { peer.ws.send(JSON.stringify(message)); return true; } return false; } /** * Get server stats */ getStats() { return { peers: this.peers.size, rooms: this.rooms.size, ...this.stats, uptime: Date.now() - (this.startTime || Date.now()), }; } /** * Stop the server */ async stop() { return new Promise((resolve) => { // Close all peer connections for (const [peerId, peer] of this.peers) { peer.ws.close(); } this.peers.clear(); this.rooms.clear(); if (this.wss) { this.wss.close(); } if (this.server) { this.server.close(() => { console.log('[Signaling] Server stopped'); resolve(); }); } else { resolve(); } }); } } // ============================================ // SIGNALING CLIENT // ============================================ /** * WebRTC Signaling Client * Connects to signaling server for peer discovery and connection setup */ export class SignalingClient extends EventEmitter { constructor(options = {}) { super(); this.serverUrl = options.serverUrl || 'ws://localhost:8765'; this.nodeId = options.nodeId || `node-${randomBytes(8).toString('hex')}`; this.capabilities = options.capabilities || []; this.ws = null; this.peerId = null; this.connected = false; this.rooms = new Set(); this.pendingConnections = new Map(); this.peerConnections = new Map(); } /** * Connect to signaling server */ async connect() { return new Promise(async (resolve, reject) => { try { let WebSocket; if (typeof globalThis.WebSocket !== 'undefined') { WebSocket = globalThis.WebSocket; } else { const ws = await import('ws'); WebSocket = ws.default || ws.WebSocket; } this.ws = new WebSocket(this.serverUrl); const timeout = setTimeout(() => { reject(new Error('Connection timeout')); }, 10000); this.ws.onopen = () => { clearTimeout(timeout); this.connected = true; this.emit('connected'); }; this.ws.onmessage = (event) => { const message = JSON.parse(event.data); this.handleMessage(message); if (message.type === 'registered') { resolve(this); } }; this.ws.onclose = () => { this.connected = false; this.emit('disconnected'); }; this.ws.onerror = (error) => { clearTimeout(timeout); reject(error); }; } catch (error) { reject(error); } }); } /** * Handle incoming message */ handleMessage(message) { switch (message.type) { case 'welcome': this.peerId = message.peerId; // Register with capabilities this.send({ type: 'register', nodeId: this.nodeId, capabilities: this.capabilities, }); break; case 'registered': this.emit('registered', message); break; case 'room-joined': this.rooms.add(message.roomId); this.emit('room-joined', message); break; case 'peer-joined': this.emit('peer-joined', message); break; case 'peer-left': this.emit('peer-left', message); break; case 'offer': this.emit('offer', message); break; case 'answer': this.emit('answer', message); break; case 'ice-candidate': this.emit('ice-candidate', message); break; case 'discover-result': this.emit('discover-result', message); break; case 'broadcast': this.emit('broadcast', message); break; case 'pong': this.emit('pong', message); break; default: this.emit('message', message); } } /** * Send message to server */ send(message) { if (this.connected && this.ws?.readyState === 1) { this.ws.send(JSON.stringify(message)); return true; } return false; } /** * Join a room */ joinRoom(roomId) { return this.send({ type: 'join-room', roomId }); } /** * Leave a room */ leaveRoom(roomId) { this.rooms.delete(roomId); return this.send({ type: 'leave-room', roomId }); } /** * Send WebRTC offer to peer */ sendOffer(targetPeerId, offer, connectionId) { return this.send({ type: 'offer', to: targetPeerId, offer, connectionId, }); } /** * Send WebRTC answer to peer */ sendAnswer(targetPeerId, answer, connectionId) { return this.send({ type: 'answer', to: targetPeerId, answer, connectionId, }); } /** * Send ICE candidate to peer */ sendIceCandidate(targetPeerId, candidate, connectionId) { return this.send({ type: 'ice-candidate', to: targetPeerId, candidate, connectionId, }); } /** * Discover peers with capabilities */ discover(capabilities = [], limit = 10) { return this.send({ type: 'discover', capabilities, limit, }); } /** * Broadcast to room */ broadcast(roomId, data) { return this.send({ type: 'broadcast', roomId, data, }); } /** * Ping server */ ping() { return this.send({ type: 'ping' }); } /** * Close connection */ close() { if (this.ws) { this.ws.close(); } } } // ============================================ // EXPORTS // ============================================ export default SignalingServer;