var process = require('process') var fs = require('fs') var path = require('path') // Handle SIGINT process.on('SIGINT', () => { console.info("SIGINT Received, exiting...") process.exit(0) }) // Handle SIGTERM process.on('SIGTERM', () => { console.info("SIGTERM Received, exiting...") process.exit(0) }) const parser = require('ua-parser-js'); const { uniqueNamesGenerator, animals, colors } = require('unique-names-generator'); class SnapdropServer { constructor(port) { const WebSocket = require('ws'); this._wss = new WebSocket.Server({ port: port }); this._wss.on('connection', (socket, request) => this._onConnection(new Peer(socket, request))); this._wss.on('headers', (headers, response) => this._onHeaders(headers, response)); this._rooms = {}; this._userIdIndex = {}; this._fingerprintIndex = {}; this._pairingRecords = {}; this._pendingPairRequests = {}; this._canvasRooms = {}; this._pairingCodes = {}; this._radarPool = {}; this._activeScreenShares = {}; this._pairingCodeCleanup = setInterval(() => { const now = Date.now(); for (const code in this._pairingCodes) { if (now > this._pairingCodes[code].expiresAt) { delete this._pairingCodes[code]; } } }, 60000); this._radarPoolCleanup = setInterval(() => { const now = Date.now(); for (const deviceId in this._radarPool) { if (now - this._radarPool[deviceId].lastBroadcast > 30000) { delete this._radarPool[deviceId]; } } }, 10000); this._loadPairingRecords(); if (this._wss._server) { this._wss._server.on('request', (req, res) => { if (req.url === '/health') { const totalPeers = Object.values(this._rooms).reduce((sum, room) => sum + Object.keys(room).length, 0); res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ status: 'healthy', peers: totalPeers, pairingCodes: Object.keys(this._pairingCodes).length, radarDevices: Object.keys(this._radarPool).length, activeScreenShares: Object.keys(this._activeScreenShares).length, canvasRooms: Object.keys(this._canvasRooms).length, uptime: process.uptime() })); } }); } console.log('Snapdrop is running on port', port); } _loadPairingRecords() { const dataFile = path.join(__dirname, 'data', 'pairing_records.json'); try { if (fs.existsSync(dataFile)) { this._pairingRecords = JSON.parse(fs.readFileSync(dataFile, 'utf8')); } } catch (e) { console.error('Failed to load pairing records:', e.message); } } _savePairingRecords() { const dataDir = path.join(__dirname, 'data'); if (!fs.existsSync(dataDir)) fs.mkdirSync(dataDir, { recursive: true }); const dataFile = path.join(dataDir, 'pairing_records.json'); try { fs.writeFileSync(dataFile, JSON.stringify(this._pairingRecords, null, 2)); } catch (e) { console.error('Failed to save pairing records:', e.message); } } _onConnection(peer) { this._joinRoom(peer); peer.socket.on('message', message => this._onMessage(peer, message)); peer.socket.on('error', console.error); this._keepAlive(peer); this._send(peer, { type: 'registered', id: peer.id, fingerprint: peer.fingerprint || '', userId: peer.userId || '' }); this._send(peer, { type: 'display-name', message: { displayName: peer.name.displayName, deviceName: peer.name.deviceName } }); } _onHeaders(headers, response) { if (response.headers.cookie && response.headers.cookie.indexOf('peerid=') > -1) return; response.peerId = Peer.uuid(); headers.push('Set-Cookie: peerid=' + response.peerId + "; SameSite=Strict; Secure"); } _onMessage(sender, message) { try { message = JSON.parse(message); } catch (e) { return; } switch (message.type) { case 'disconnect': this._leaveRoom(sender); break; case 'pong': sender.lastBeat = Date.now(); break; case 'register': this._handleRegister(sender, message); break; case 'discoverMyDevices': this._handleDiscoverMyDevices(sender, message); break; case 'transportNegotiate': this._handleTransportNegotiate(sender, message); break; case 'wsRelay': this._handleWsRelay(sender, message); break; case 'pair-request': case 'pairRequest': this._handlePairRequest(sender, message); break; case 'pair-accept': case 'pairAccept': this._handlePairAccept(sender, message); break; case 'pair-reject': case 'pairReject': this._handlePairReject(sender, message); break; case 'heartbeat': sender.lastBeat = Date.now(); this._send(sender, { type: 'heartbeat_ack', timestamp: Date.now() }); break; case 'discover': this._handleDiscover(sender, message); break; case 'ping': sender.lastBeat = Date.now(); this._send(sender, { type: 'pong', timestamp: Date.now() }); break; case 'textMessage': this._handleTextMessage(sender, message); break; case 'fileMeta': this._handleFileMeta(sender, message); break; case 'canvas-stroke': this._handleCanvasStroke(sender, message); break; case 'canvas-cursor': this._handleCanvasCursor(sender, message); break; case 'canvas-join': this._handleCanvasJoin(sender, message); break; case 'canvas-leave': this._handleCanvasLeave(sender, message); break; case 'canvas-snapshot': this._handleCanvasSnapshot(sender, message); break; case 'pairing-code-create': this._handlePairingCodeCreate(sender, message); break; case 'pairing-code-join': this._handlePairingCodeJoin(sender, message); break; case 'radar-broadcast': this._handleRadarBroadcast(sender, message); break; case 'radar-scan': this._handleRadarScan(sender, message); break; case 'screen-share-request': this._handleScreenShareRequest(sender, message); break; case 'screen-share-accept': this._handleScreenShareAccept(sender, message); break; case 'screen-share-reject': this._handleScreenShareReject(sender, message); break; case 'screen-share-stop': this._handleScreenShareStop(sender, message); break; } const handledTypes = ['disconnect', 'pong', 'register', 'discoverMyDevices', 'discover', 'transportNegotiate', 'wsRelay', 'pair-request', 'pairRequest', 'pair-accept', 'pairAccept', 'pair-reject', 'pairReject', 'heartbeat', 'ping', 'textMessage', 'fileMeta', 'canvas-stroke', 'canvas-cursor', 'canvas-join', 'canvas-leave', 'canvas-snapshot', 'pairing-code-create', 'pairing-code-join', 'radar-broadcast', 'radar-scan', 'screen-share-request', 'screen-share-accept', 'screen-share-reject', 'screen-share-stop']; if (message.to && !handledTypes.includes(message.type)) { let recipientId = message.to; let recipient = this._resolveTarget(sender, recipientId); if (recipient) { delete message.to; message.sender = sender.id; message.from = sender.fingerprint || sender.id; message.fromPeerId = sender.id; this._send(recipient, message); } else { console.log('Message routing failed: target not found for', recipientId, 'from', sender.id); } return; } } _handleRegister(sender, message) { const data = message.payload || message.data || {}; if (data.userId) { const oldUserId = sender.userId; sender.userId = data.userId; if (oldUserId && this._userIdIndex[oldUserId]) { this._userIdIndex[oldUserId].delete(sender.id); if (this._userIdIndex[oldUserId].size === 0) { delete this._userIdIndex[oldUserId]; } } if (!this._userIdIndex[sender.userId]) { this._userIdIndex[sender.userId] = new Set(); } this._userIdIndex[sender.userId].add(sender.id); } if (data.fingerprint) { const oldFingerprint = sender.fingerprint; sender.fingerprint = data.fingerprint; if (oldFingerprint && this._fingerprintIndex[oldFingerprint] === sender.id) { delete this._fingerprintIndex[oldFingerprint]; } const existingPeerId = this._fingerprintIndex[sender.fingerprint]; if (existingPeerId && existingPeerId !== sender.id) { const existingPeer = this._findPeer(existingPeerId); if (existingPeer) { console.log('Replacing old connection for fingerprint:', sender.fingerprint, 'old peer:', existingPeerId, 'new peer:', sender.id); this._leaveRoom(existingPeer); } } this._fingerprintIndex[sender.fingerprint] = sender.id; console.log('Register fingerprint mapping:', sender.fingerprint, '->', sender.id); } if (data.ipCity) sender.ipCity = data.ipCity; if (data.ipRange) sender.ipRange = data.ipRange; if (data.deviceModel) sender.deviceModel = data.deviceModel; if (data.platform) sender.platform = data.platform; if (data.alias) sender.name.displayName = data.alias; if (data.deviceType) sender.name.type = data.deviceType; this._send(sender, { type: 'registered', id: sender.id, fingerprint: sender.fingerprint || '', userId: sender.userId }); } _handleDiscover(sender, message) { const allPeers = []; for (const roomId in this._rooms) { for (const peerId in this._rooms[roomId]) { const peer = this._rooms[roomId][peerId]; if (peer.id !== sender.id) { allPeers.push(peer.getInfo()); } } } this._send(sender, { type: 'discover_response', devices: allPeers }); } _handleDiscoverMyDevices(sender, message) { const payload = message.payload || message.data || {}; const userId = payload.userId || message.userId || sender.userId; if (!userId) { this._send(sender, { type: 'myDevicesResponse', devices: [], error: 'userId required' }); return; } const seenFingerprints = new Map(); for (const roomId in this._rooms) { for (const peerId in this._rooms[roomId]) { const peer = this._rooms[roomId][peerId]; if (peer.id === sender.id) continue; if (peer.userId === userId) { const fp = peer.fingerprint || peer.id; if (!seenFingerprints.has(fp)) { seenFingerprints.set(fp, { id: peer.id, fingerprint: peer.fingerprint || '', alias: peer.name.displayName, deviceName: peer.name.deviceName, deviceModel: peer.deviceModel || '', platform: peer.platform || '', ip: peer.ip, ipCity: peer.ipCity || '', isOnline: true, rtcSupported: peer.rtcSupported }); } } } } this._send(sender, { type: 'myDevicesResponse', devices: Array.from(seenFingerprints.values()) }); } _handleTransportNegotiate(sender, message) { const targetId = message.to; if (!targetId) return; let targetPeer = this._findPeer(targetId); if (!targetPeer && this._fingerprintIndex[targetId]) { targetPeer = this._findPeer(this._fingerprintIndex[targetId]); } if (!targetPeer) { this._send(sender, { type: 'transportNegotiateResponse', success: false, error: 'Target device not found' }); return; } this._send(targetPeer, { type: 'transportNegotiate', from: sender.fingerprint || sender.id, transport: message.transport, payload: message.payload || {} }); } _handlePairRequest(sender, message) { const targetId = message.to; if (!targetId) return; let targetPeer = this._findPeer(targetId); if (!targetPeer && this._fingerprintIndex[targetId]) { targetPeer = this._findPeer(this._fingerprintIndex[targetId]); } if (!targetPeer) { this._send(sender, { type: 'pair-response', from: sender.id, success: false, error: 'Target device not found' }); return; } const requestId = Date.now().toString(36) + Math.random().toString(36).substr(2); this._pendingPairRequests[requestId] = { id: requestId, fromId: sender.id, toId: targetId, fingerprint: message.fingerprint || message.payload?.fingerprint || '', createdAt: Date.now(), expiresAt: Date.now() + 300000 }; this._send(targetPeer, { type: 'pair-request', from: sender.fingerprint || sender.id, requestId: requestId, fingerprint: this._pendingPairRequests[requestId].fingerprint, payload: message.payload || {} }); } _handlePairAccept(sender, message) { const requestId = message.requestId || message.payload?.requestId; if (!requestId || !this._pendingPairRequests[requestId]) { this._send(sender, { type: 'pair-response', success: false, error: 'Request not found or expired' }); return; } const req = this._pendingPairRequests[requestId]; delete this._pendingPairRequests[requestId]; const pairKey1 = req.fromId + ':' + sender.id; const pairKey2 = sender.id + ':' + req.fromId; this._pairingRecords[pairKey1] = { deviceId: req.fromId, peerDeviceId: sender.id, peerFingerprint: req.fingerprint, pairedAt: Date.now(), isTrusted: true }; this._pairingRecords[pairKey2] = { deviceId: sender.id, peerDeviceId: req.fromId, peerFingerprint: req.fingerprint, pairedAt: Date.now(), isTrusted: true }; this._savePairingRecords(); const fromPeer = this._findPeer(req.fromId); if (fromPeer) { this._send(fromPeer, { type: 'pair-response', from: sender.fingerprint || sender.id, success: true, requestId: requestId }); } this._send(sender, { type: 'pair-response', from: fromPeer ? (fromPeer.fingerprint || fromPeer.id) : req.fromId, success: true, requestId: requestId }); } _handlePairReject(sender, message) { const requestId = message.requestId || message.payload?.requestId; if (!requestId || !this._pendingPairRequests[requestId]) return; const req = this._pendingPairRequests[requestId]; delete this._pendingPairRequests[requestId]; const fromPeer = this._findPeer(req.fromId); if (fromPeer) { this._send(fromPeer, { type: 'pair-response', from: sender.fingerprint || sender.id, success: false, requestId: requestId, error: 'Rejected' }); } } _findPeer(peerId) { for (const roomId in this._rooms) { if (this._rooms[roomId][peerId]) { return this._rooms[roomId][peerId]; } } return null; } _resolveTarget(sender, targetId) { if (this._rooms[sender.ip] && this._rooms[sender.ip][targetId]) { return this._rooms[sender.ip][targetId]; } let recipient = this._findPeer(targetId); if (recipient) return recipient; if (this._fingerprintIndex[targetId]) { const mappedPeerId = this._fingerprintIndex[targetId]; console.log('Fingerprint mapping:', targetId, '->', mappedPeerId); recipient = this._findPeer(mappedPeerId); if (recipient) return recipient; } if (sender.userId) { const sameUserPeerIds = this._userIdIndex[sender.userId]; if (sameUserPeerIds) { for (const peerId of sameUserPeerIds) { if (peerId !== sender.id) { recipient = this._findPeer(peerId); if (recipient) return recipient; } } } } if (this._userIdIndex[targetId]) { for (const peerId of this._userIdIndex[targetId]) { recipient = this._findPeer(peerId); if (recipient) return recipient; } } return null; } _handleTextMessage(sender, message) { const targetId = message.to; if (!targetId) return; const recipient = this._resolveTarget(sender, targetId); if (recipient) { this._send(recipient, { type: 'textMessage', from: sender.fingerprint || sender.id, fromPeerId: sender.id, to: recipient.id, message: message.message || message.payload || {}, timestamp: message.timestamp || Date.now() }); } else { console.log('textMessage routing failed: target not found for', targetId, 'from', sender.id); } } _handleFileMeta(sender, message) { const targetId = message.to; if (!targetId) return; const recipient = this._resolveTarget(sender, targetId); if (recipient) { this._send(recipient, { type: 'fileMeta', from: sender.fingerprint || sender.id, fromPeerId: sender.id, to: recipient.id, file: message.file || message.payload || {}, timestamp: message.timestamp || Date.now() }); } else { console.log('fileMeta routing failed: target not found for', targetId, 'from', sender.id); } } _handleWsRelay(sender, message) { const targetId = message.to; if (!targetId) return; let targetPeer = null; for (const roomId in this._rooms) { if (this._rooms[roomId][targetId]) { targetPeer = this._rooms[roomId][targetId]; break; } } if (!targetPeer && this._fingerprintIndex[targetId]) { const mappedPeerId = this._fingerprintIndex[targetId]; for (const roomId in this._rooms) { if (this._rooms[roomId][mappedPeerId]) { targetPeer = this._rooms[roomId][mappedPeerId]; break; } } } if (!targetPeer && sender.userId) { const sameUserPeerIds = this._userIdIndex[sender.userId]; if (sameUserPeerIds) { for (const peerId of sameUserPeerIds) { if (peerId !== sender.id) { for (const roomId in this._rooms) { if (this._rooms[roomId][peerId]) { targetPeer = this._rooms[roomId][peerId]; break; } } if (targetPeer) break; } } } } if (!targetPeer) { this._send(sender, { type: 'wsRelay', from: sender.fingerprint || sender.id, payload: { relayType: 'error', data: { error: 'Target peer not found', targetId: targetId } } }); return; } const relayType = message.relayType || (message.payload && message.payload.relayType) || 'data'; const payload = message.payload || {}; if (!payload.relayType) { payload.relayType = relayType; } this._send(targetPeer, { type: 'wsRelay', from: sender.fingerprint || sender.id, payload: payload }); } _handleCanvasJoin(sender, message) { const payload = message.payload || {}; const canvasId = payload.canvasId; if (!canvasId) return; if (!this._canvasRooms[canvasId]) { this._canvasRooms[canvasId] = new Set(); } this._canvasRooms[canvasId].add(sender.id); sender.canvasRoomId = canvasId; const members = Array.from(this._canvasRooms[canvasId]); for (const peerId of this._canvasRooms[canvasId]) { if (peerId === sender.id) continue; const peer = this._findPeer(peerId); if (peer) { this._send(peer, { type: 'canvas-join', from: sender.fingerprint || sender.id, payload: { canvasId: canvasId, members: members } }); } } this._send(sender, { type: 'canvas-join', from: 'server', payload: { canvasId: canvasId, members: members } }); console.log('Canvas join:', sender.id, 'joined canvas', canvasId, 'members:', members.length); } _handleCanvasLeave(sender, message) { const payload = message.payload || {}; const canvasId = payload.canvasId || sender.canvasRoomId; if (!canvasId || !this._canvasRooms[canvasId]) return; this._canvasRooms[canvasId].delete(sender.id); sender.canvasRoomId = null; for (const peerId of this._canvasRooms[canvasId]) { const peer = this._findPeer(peerId); if (peer) { this._send(peer, { type: 'canvas-leave', from: sender.fingerprint || sender.id, payload: { canvasId: canvasId } }); } } if (this._canvasRooms[canvasId].size === 0) { delete this._canvasRooms[canvasId]; } console.log('Canvas leave:', sender.id, 'left canvas', canvasId); } _handleCanvasStroke(sender, message) { const payload = message.payload || {}; const canvasId = payload.canvasId || sender.canvasRoomId; if (!canvasId || !this._canvasRooms[canvasId]) return; for (const peerId of this._canvasRooms[canvasId]) { if (peerId === sender.id) continue; const peer = this._findPeer(peerId); if (peer) { this._send(peer, { type: 'canvas-stroke', from: sender.fingerprint || sender.id, payload: payload }); } } } _handleCanvasCursor(sender, message) { const payload = message.payload || {}; const canvasId = payload.canvasId || sender.canvasRoomId; if (!canvasId || !this._canvasRooms[canvasId]) return; for (const peerId of this._canvasRooms[canvasId]) { if (peerId === sender.id) continue; const peer = this._findPeer(peerId); if (peer) { this._send(peer, { type: 'canvas-cursor', from: sender.fingerprint || sender.id, payload: payload }); } } } _handleCanvasSnapshot(sender, message) { const payload = message.payload || {}; const canvasId = payload.canvasId || sender.canvasRoomId; const action = payload.action || 'request'; if (!canvasId || !this._canvasRooms[canvasId]) return; if (action === 'request') { const members = Array.from(this._canvasRooms[canvasId]); const otherMember = members.find(id => id !== sender.id); if (otherMember) { const peer = this._findPeer(otherMember); if (peer) { this._send(peer, { type: 'canvas-snapshot', from: sender.fingerprint || sender.id, payload: { canvasId: canvasId, action: 'request' } }); } } } else if (action === 'response') { const targetId = message.to; if (targetId) { const targetPeer = this._findPeer(targetId); if (targetPeer) { this._send(targetPeer, { type: 'canvas-snapshot', from: sender.fingerprint || sender.id, payload: payload }); } } } } _handlePairingCodeCreate(sender, message) { const payload = message.payload || message.data || {}; let code = ''; for (let i = 0; i < 4; i++) { code += Math.floor(Math.random() * 10).toString(); } for (const existingCode in this._pairingCodes) { if (this._pairingCodes[existingCode].creatorId === sender.id) { delete this._pairingCodes[existingCode]; } } this._pairingCodes[code] = { creatorId: sender.id, creatorFingerprint: sender.fingerprint || '', creatorAlias: payload.alias || sender.name.displayName, creatorDeviceModel: payload.deviceModel || sender.deviceModel || '', creatorDeviceType: payload.deviceType || sender.name.type || '', createdAt: Date.now(), expiresAt: Date.now() + 300000, used: false }; this._send(sender, { type: 'pairing-code-created', pairingCode: code, expiresAt: this._pairingCodes[code].expiresAt }); console.log('Pairing code created:', code, 'by', sender.id, '(' + (sender.fingerprint || '') + ')'); } _handlePairingCodeJoin(sender, message) { const payload = message.payload || message.data || {}; const code = (payload.pairingCode || '').trim(); if (!code || !/^\d{4}$/.test(code)) { this._send(sender, { type: 'pairing-matched', success: false, error: '请输入4位数字配对码' }); return; } const entry = this._pairingCodes[code]; if (!entry) { this._send(sender, { type: 'pairing-matched', success: false, error: '配对码不存在或已过期' }); return; } if (Date.now() > entry.expiresAt) { delete this._pairingCodes[code]; this._send(sender, { type: 'pairing-matched', success: false, error: '配对码已过期' }); return; } if (entry.used) { this._send(sender, { type: 'pairing-matched', success: false, error: '配对码已被使用' }); return; } if (entry.creatorId === sender.id) { this._send(sender, { type: 'pairing-matched', success: false, error: '不能使用自己创建的配对码' }); return; } entry.used = true; const creatorPeer = this._findPeer(entry.creatorId); this._send(sender, { type: 'pairing-matched', success: true, pairingCode: code, peer: { id: entry.creatorId, fingerprint: entry.creatorFingerprint, alias: entry.creatorAlias, deviceModel: entry.creatorDeviceModel, deviceType: entry.creatorDeviceType } }); if (creatorPeer) { this._send(creatorPeer, { type: 'pairing-matched', success: true, pairingCode: code, peer: { id: sender.id, fingerprint: sender.fingerprint || '', alias: sender.name.displayName, deviceModel: sender.deviceModel || '', deviceType: sender.name.type || '' } }); } delete this._pairingCodes[code]; console.log('Pairing matched via code:', code, 'creator:', entry.creatorId, 'joiner:', sender.id); } _handleRadarBroadcast(sender, message) { const payload = message.payload || message.data || {}; this._radarPool[sender.id] = { deviceId: sender.id, fingerprint: sender.fingerprint || '', alias: payload.alias || sender.name.displayName, deviceModel: payload.deviceModel || sender.deviceModel || '', deviceType: payload.deviceType || sender.name.type || '', ipRange: payload.ipRange || sender.ipRange || '', ipCity: payload.ipCity || sender.ipCity || '', ip: sender.ip, userId: sender.userId || '', lastBroadcast: Date.now() }; this._send(sender, { type: 'radar-broadcast', success: true }); } _handleRadarScan(sender, message) { const payload = message.payload || message.data || {}; const myIpRange = payload.ipRange || sender.ipRange || ''; const myIpCity = payload.ipCity || sender.ipCity || ''; const devices = []; for (const deviceId in this._radarPool) { const entry = this._radarPool[deviceId]; if (deviceId === sender.id) continue; if (Date.now() - entry.lastBroadcast > 30000) continue; let matchType = 'remote'; if (sender.userId && entry.userId && sender.userId === entry.userId) { matchType = 'my-device'; } else if (myIpRange && entry.ipRange && myIpRange === entry.ipRange) { matchType = 'same-network'; } else if (myIpCity && entry.ipCity && myIpCity === entry.ipCity) { matchType = 'same-city'; } devices.push({ id: entry.deviceId, fingerprint: entry.fingerprint, alias: entry.alias, deviceModel: entry.deviceModel, deviceType: entry.deviceType, ipCity: entry.ipCity, matchType: matchType }); } devices.sort((a, b) => { const order = { 'my-device': 0, 'same-network': 1, 'same-city': 2, 'remote': 3 }; return (order[a.matchType] || 4) - (order[b.matchType] || 4); }); this._send(sender, { type: 'radar-devices', devices: devices }); } _handleScreenShareRequest(sender, message) { const targetId = message.to; if (!targetId) return; const targetPeer = this._resolveTarget(sender, targetId); if (!targetPeer) { this._send(sender, { type: 'screen-share-response', from: targetId, success: false, error: 'Target device not found or offline' }); return; } const payload = message.payload || message.data || {}; const direction = payload.direction || 'view'; this._send(targetPeer, { type: 'screen-share-request', from: sender.fingerprint || sender.id, fromPeerId: sender.id, direction: direction, requestId: Date.now().toString(36) + Math.random().toString(36).substr(2), timestamp: Date.now() }); console.log('Screen share request:', sender.id, '->', targetId, 'direction:', direction); } _handleScreenShareAccept(sender, message) { const targetId = message.to; if (!targetId) return; const targetPeer = this._resolveTarget(sender, targetId); if (!targetPeer) { this._send(sender, { type: 'screen-share-response', success: false, error: 'Target device not found' }); return; } const payload = message.payload || message.data || {}; const shareKey = [sender.id, targetId].sort().join(':'); this._activeScreenShares[shareKey] = { sharer: payload.direction === 'share' ? sender.id : targetId, viewer: payload.direction === 'share' ? targetId : sender.id, direction: payload.direction || 'view', startedAt: Date.now() }; this._send(targetPeer, { type: 'screen-share-accept', from: sender.fingerprint || sender.id, fromPeerId: sender.id, direction: payload.direction || 'view', timestamp: Date.now() }); console.log('Screen share accepted:', sender.id, '<->', targetId); } _handleScreenShareReject(sender, message) { const targetId = message.to; if (!targetId) return; const targetPeer = this._resolveTarget(sender, targetId); if (targetPeer) { this._send(targetPeer, { type: 'screen-share-reject', from: sender.fingerprint || sender.id, fromPeerId: sender.id, timestamp: Date.now() }); } console.log('Screen share rejected:', sender.id, 'rejected request from', targetId); } _handleScreenShareStop(sender, message) { const targetId = message.to; if (!targetId) return; const shareKey = [sender.id, targetId].sort().join(':'); delete this._activeScreenShares[shareKey]; const targetPeer = this._resolveTarget(sender, targetId); if (targetPeer) { this._send(targetPeer, { type: 'screen-share-stop', from: sender.fingerprint || sender.id, fromPeerId: sender.id, timestamp: Date.now() }); } console.log('Screen share stopped:', sender.id, '<->', targetId); } _joinRoom(peer) { if (!this._rooms[peer.ip]) { this._rooms[peer.ip] = {}; } for (const otherPeerId in this._rooms[peer.ip]) { const otherPeer = this._rooms[peer.ip][otherPeerId]; this._send(otherPeer, { type: 'peer-joined', peer: peer.getInfo() }); } const otherPeers = []; for (const otherPeerId in this._rooms[peer.ip]) { otherPeers.push(this._rooms[peer.ip][otherPeerId].getInfo()); } this._send(peer, { type: 'peers', peers: otherPeers }); this._rooms[peer.ip][peer.id] = peer; if (peer.userId) { if (!this._userIdIndex[peer.userId]) { this._userIdIndex[peer.userId] = new Set(); } this._userIdIndex[peer.userId].add(peer.id); } } _leaveRoom(peer) { if (!this._rooms[peer.ip] || !this._rooms[peer.ip][peer.id]) return; this._cancelKeepAlive(this._rooms[peer.ip][peer.id]); if (peer.canvasRoomId && this._canvasRooms[peer.canvasRoomId]) { this._canvasRooms[peer.canvasRoomId].delete(peer.id); for (const peerId of this._canvasRooms[peer.canvasRoomId]) { const otherPeer = this._findPeer(peerId); if (otherPeer) { this._send(otherPeer, { type: 'canvas-leave', from: peer.fingerprint || peer.id, payload: { canvasId: peer.canvasRoomId } }); } } if (this._canvasRooms[peer.canvasRoomId].size === 0) { delete this._canvasRooms[peer.canvasRoomId]; } peer.canvasRoomId = null; } if (peer.fingerprint && this._fingerprintIndex[peer.fingerprint] === peer.id) { delete this._fingerprintIndex[peer.fingerprint]; } delete this._rooms[peer.ip][peer.id]; if (peer.userId && this._userIdIndex[peer.userId]) { this._userIdIndex[peer.userId].delete(peer.id); if (this._userIdIndex[peer.userId].size === 0) { delete this._userIdIndex[peer.userId]; } } if (this._radarPool[peer.id]) { delete this._radarPool[peer.id]; } for (const shareKey in this._activeScreenShares) { const share = this._activeScreenShares[shareKey]; if (share.sharer === peer.id || share.viewer === peer.id) { const otherPeerId = share.sharer === peer.id ? share.viewer : share.sharer; const otherPeer = this._findPeer(otherPeerId); if (otherPeer) { this._send(otherPeer, { type: 'screen-share-stop', from: peer.fingerprint || peer.id, fromPeerId: peer.id, reason: 'peer-disconnected', timestamp: Date.now() }); } delete this._activeScreenShares[shareKey]; } } for (const code in this._pairingCodes) { if (this._pairingCodes[code].creatorId === peer.id) { delete this._pairingCodes[code]; } } peer.socket.terminate(); if (!Object.keys(this._rooms[peer.ip]).length) { delete this._rooms[peer.ip]; } else { for (const otherPeerId in this._rooms[peer.ip]) { const otherPeer = this._rooms[peer.ip][otherPeerId]; this._send(otherPeer, { type: 'peer-left', peerId: peer.id }); } } } _send(peer, message) { if (!peer) return; if (this._wss.readyState !== this._wss.OPEN) return; message = JSON.stringify(message); peer.socket.send(message, error => ''); } _keepAlive(peer) { this._cancelKeepAlive(peer); var timeout = 30000; if (!peer.lastBeat) { peer.lastBeat = Date.now(); } if (Date.now() - peer.lastBeat > 2 * timeout) { this._leaveRoom(peer); return; } this._send(peer, { type: 'ping' }); peer.timerId = setTimeout(() => this._keepAlive(peer), timeout); } _cancelKeepAlive(peer) { if (peer && peer.timerId) { clearTimeout(peer.timerId); } } } class Peer { constructor(socket, request) { this.socket = socket; this._setIP(request); this._setPeerId(request); this.rtcSupported = request.url.indexOf('webrtc') > -1; this._setName(request); this.userId = ''; this.fingerprint = ''; this.ipCity = ''; this.ipRange = ''; this.deviceModel = ''; this.platform = ''; this.canvasRoomId = null; this.timerId = 0; this.lastBeat = Date.now(); } _setIP(request) { if (request.headers['x-forwarded-for']) { this.ip = request.headers['x-forwarded-for'].split(/\s*,\s*/)[0]; } else { this.ip = request.connection.remoteAddress; } if (this.ip == '::1' || this.ip == '::ffff:127.0.0.1') { this.ip = '127.0.0.1'; } } _setPeerId(request) { if (request.peerId) { this.id = request.peerId; } else { this.id = request.headers.cookie.replace('peerid=', ''); } } toString() { return `` } _setName(req) { let ua = parser(req.headers['user-agent']); let deviceName = ''; if (ua.os && ua.os.name) { deviceName = ua.os.name.replace('Mac OS', 'Mac') + ' '; } if (ua.device.model) { deviceName += ua.device.model; } else { deviceName += ua.browser.name; } if(!deviceName) deviceName = 'Unknown Device'; const displayName = uniqueNamesGenerator({ length: 2, separator: ' ', dictionaries: [colors, animals], style: 'capital', seed: this.id.hashCode() }) this.name = { model: ua.device.model, os: ua.os.name, browser: ua.browser.name, type: ua.device.type, deviceName, displayName }; } getInfo() { return { id: this.id, name: this.name, rtcSupported: this.rtcSupported, userId: this.userId, ipCity: this.ipCity, deviceModel: this.deviceModel, platform: this.platform } } static uuid() { let uuid = '', ii; for (ii = 0; ii < 32; ii += 1) { switch (ii) { case 8: case 20: uuid += '-'; uuid += (Math.random() * 16 | 0).toString(16); break; case 12: uuid += '-'; uuid += '4'; break; case 16: uuid += '-'; uuid += (Math.random() * 4 | 8).toString(16); break; default: uuid += (Math.random() * 16 | 0).toString(16); } } return uuid; }; } Object.defineProperty(String.prototype, 'hashCode', { value: function() { var hash = 0, i, chr; for (i = 0; i < this.length; i++) { chr = this.charCodeAt(i); hash = ((hash << 5) - hash) + chr; hash |= 0; } return hash; } }); const server = new SnapdropServer(process.env.PORT || 3000);