import {WebSocketServer} from "ws"; import crypto from "crypto" import Peer from "./peer.js"; import {hasher, randomizer} from "./helper.js"; export default class PairDropWsServer { constructor(server, conf) { this._conf = conf this._rooms = {}; // { roomId: peers[] } this._roomSecrets = {}; // { pairKey: roomSecret } this._keepAliveTimers = {}; this._wss = new WebSocketServer({ server }); this._wss.on('connection', (socket, request) => this._onConnection(new Peer(socket, request, conf))); } _onConnection(peer) { peer.socket.on('message', message => this._onMessage(peer, message)); peer.socket.onerror = e => console.error(e); this._keepAlive(peer); this._send(peer, { type: 'ws-config', wsConfig: { rtcConfig: this._conf.rtcConfig, wsFallback: this._conf.wsFallback } }); // send displayName this._send(peer, { type: 'display-name', displayName: peer.name.displayName, deviceName: peer.name.deviceName, peerId: peer.id, peerIdHash: hasher.hashCodeSalted(peer.id) }); } _onMessage(sender, message) { // Try to parse message try { message = JSON.parse(message); } catch (e) { console.warn("WS: Received JSON is malformed"); return; } switch (message.type) { case 'disconnect': this._onDisconnect(sender); break; case 'pong': this._setKeepAliveTimerToNow(sender); break; case 'join-ip-room': this._joinIpRoom(sender); break; case 'room-secrets': this._onRoomSecrets(sender, message); break; case 'room-secrets-deleted': this._onRoomSecretsDeleted(sender, message); break; case 'pair-device-initiate': this._onPairDeviceInitiate(sender); break; case 'pair-device-join': this._onPairDeviceJoin(sender, message); break; case 'pair-device-cancel': this._onPairDeviceCancel(sender); break; case 'regenerate-room-secret': this._onRegenerateRoomSecret(sender, message); break; case 'create-public-room': this._onCreatePublicRoom(sender); break; case 'join-public-room': this._onJoinPublicRoom(sender, message); break; case 'leave-public-room': this._onLeavePublicRoom(sender); break; case 'signal': this._signalAndRelay(sender, message); break; case 'request': case 'header': case 'partition': case 'partition-received': case 'progress': case 'files-transfer-response': case 'file-transfer-complete': case 'message-transfer-complete': case 'text': case 'display-name-changed': case 'ws-chunk': // relay ws-fallback if (this._conf.wsFallback) { this._signalAndRelay(sender, message); } else { console.log("Websocket fallback is not activated on this instance.") } } } _signalAndRelay(sender, message) { const room = message.roomType === 'ip' ? sender.ip : message.roomId; // relay message to recipient if (message.to && Peer.isValidUuid(message.to) && this._rooms[room]) { const recipient = this._rooms[room][message.to]; delete message.to; // add sender message.sender = { id: sender.id, rtcSupported: sender.rtcSupported }; this._send(recipient, message); } } _onDisconnect(sender) { this._disconnect(sender); } _disconnect(sender) { this._removePairKey(sender.pairKey); sender.pairKey = null; this._cancelKeepAlive(sender); delete this._keepAliveTimers[sender.id]; this._leaveIpRoom(sender, true); this._leaveAllSecretRooms(sender, true); this._leavePublicRoom(sender, true); sender.socket.terminate(); } _onRoomSecrets(sender, message) { if (!message.roomSecrets) return; const roomSecrets = message.roomSecrets.filter(roomSecret => { return /^[\x00-\x7F]{64,256}$/.test(roomSecret); }) if (!roomSecrets) return; this._joinSecretRooms(sender, roomSecrets); } _onRoomSecretsDeleted(sender, message) { for (let i = 0; i 5 * timeout) { // Disconnect peer if unresponsive for 10s this._disconnect(peer); return; } this._send(peer, { type: 'ping' }); this._keepAliveTimers[peer.id].timer = setTimeout(() => this._keepAlive(peer), timeout); } _cancelKeepAlive(peer) { if (this._keepAliveTimers[peer.id]?.timer) { clearTimeout(this._keepAliveTimers[peer.id].timer); } } _setKeepAliveTimerToNow(peer) { if (this._keepAliveTimers[peer.id]?.lastBeat) { this._keepAliveTimers[peer.id].lastBeat = Date.now(); } } }