var process = require('process') var net = require('net') // 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) }) // Handle APP ERRORS process.on('uncaughtException', (error, origin) => { console.log('----- Uncaught exception -----') console.log(error) console.log('----- Exception origin -----') console.log(origin) }) process.on('unhandledRejection', (reason, promise) => { console.log('----- Unhandled Rejection at -----') console.log(promise) console.log('----- Reason -----') console.log(reason) }) const express = require('express'); const http = require('http'); const app = express(); const port = process.env.PORT || 3000; app.use(express.static('public')); app.use(function(req, res) { res.redirect('/'); }); app.get('/', (req, res) => { res.sendFile('index.html'); }); const server = http.createServer(app); server.listen(port); const parser = require('ua-parser-js'); const { uniqueNamesGenerator, animals, colors } = require('unique-names-generator'); class SnapdropServer { constructor() { const WebSocket = require('ws'); this._wss = new WebSocket.Server({ server }); this._wss.on('connection', (socket, request) => this._onConnection(new Peer(socket, request))); this._wss.on('headers', (headers, response) => this._onHeaders(headers, response)); this._rooms = {}; console.log('Snapdrop is running on port', port); } _onConnection(peer) { this._joinRoom(peer); peer.socket.on('message', message => this._onMessage(peer, message)); peer.socket.on('error', console.error); this._keepAlive(peer); // send displayName this._send(peer, { type: 'display-name', message: { displayName: peer.name.displayName, deviceName: peer.name.deviceName } }); } _onHeaders(headers, request) { if (request.headers.cookie && request.headers.cookie.indexOf('peerid=') > -1) return; request.peerId = Peer.uuid(); headers.push('Set-Cookie: peerid=' + request.peerId + "; SameSite=Strict; Secure"); } _onMessage(sender, message) { // Try to parse message try { message = JSON.parse(message); } catch (e) { return; // TODO: handle malformed JSON } switch (message.type) { case 'disconnect': this._leaveRoom(sender); break; case 'pong': sender.lastBeat = Date.now(); break; } // relay message to recipient if (message.to && this._rooms[sender.ip]) { const recipientId = message.to; // TODO: sanitize const recipient = this._rooms[sender.ip][recipientId]; delete message.to; // add sender id message.sender = sender.id; this._send(recipient, message); return; } } _joinRoom(peer) { // if room doesn't exist, create it if (!this._rooms[peer.ip]) { this._rooms[peer.ip] = {}; } // notify all other peers for (const otherPeerId in this._rooms[peer.ip]) { if (otherPeerId === peer.id) continue; const otherPeer = this._rooms[peer.ip][otherPeerId]; this._send(otherPeer, { type: 'peer-joined', peer: peer.getInfo() }); } // notify peer about the other peers const otherPeers = []; for (const otherPeerId in this._rooms[peer.ip]) { if (otherPeerId === peer.id) continue; otherPeers.push(this._rooms[peer.ip][otherPeerId].getInfo()); } this._send(peer, { type: 'peers', peers: otherPeers }); // add peer to room this._rooms[peer.ip][peer.id] = peer; } _leaveRoom(peer) { if (!this._rooms[peer.ip] || !this._rooms[peer.ip][peer.id]) return; this._cancelKeepAlive(this._rooms[peer.ip][peer.id]); // delete the peer delete this._rooms[peer.ip][peer.id]; peer.socket.terminate(); //if room is empty, delete the room if (!Object.keys(this._rooms[peer.ip]).length) { delete this._rooms[peer.ip]; } else { // notify all other peers 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 = 500; 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) { // set socket this.socket = socket; // set remote ip this._setIP(request); // set peer id this._setPeerId(request) // is WebRTC supported ? this.rtcSupported = request.url.indexOf('webrtc') > -1; // set name this._setName(request); // for keepalive this.timerId = 0; this.lastBeat = Date.now(); console.debug(this.name.displayName) } _setIP(request) { if (request.headers['cf-connecting-ip']) { this.ip = request.headers['cf-connecting-ip'].split(/\s*,\s*/)[0]; } else if (request.headers['x-forwarded-for']) { this.ip = request.headers['x-forwarded-for'].split(/\s*,\s*/)[0]; } else { this.ip = request.connection.remoteAddress; } // IPv4 and IPv6 use different values to refer to localhost // put all peers on the same network as the server into the same room as well if (this.ip === '::1' || this.ip === '::ffff:127.0.0.1' || this.ip === '::1' || this.ipIsPrivate(this.ip)) { this.ip = '127.0.0.1'; } console.debug(this.ip) } ipIsPrivate(ip) { if (ip.substring(0,7) === "::ffff:") ip = ip.substring(7); if (net.isIPv4(ip)) { // 10.0.0.0 - 10.255.255.255 || 172.16.0.0 - 172.31.255.255 || 192.168.0.0 - 192.168.255.255 return /^(10)\.(.*)\.(.*)\.(.*)$/.test(ip) || /^(172)\.(1[6-9]|2[0-9]|3[0-1])\.(.*)\.(.*)$/.test(ip) || /^(192)\.(168)\.(.*)\.(.*)$/.test(ip) } // else: ip is IPv6 const firstWord = ip.split(":").find(el => !!el); //get first not empty word // The original IPv6 Site Local addresses (fec0::/10) are deprecated. Range: fec0 - feff if (/^fe[c-f][0-f]$/.test(firstWord)) return true; // These days Unique Local Addresses (ULA) are used in place of Site Local. // Range: fc00 - fcff else if (/^fc[0-f]{2}$/.test(firstWord)) return true; // Range: fd00 - fcff else if (/^fd[0-f]{2}$/.test(firstWord)) return true; // Link local addresses (prefixed with fe80) are not routable else if (firstWord === "fe80") return true; // Discard Prefix else if (firstWord === "100") return true; // Any other IP address is not Unique Local Address (ULA) return false; } _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 } } // return uuid of form xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx 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; // Convert to 32bit integer } return hash; } }); new SnapdropServer();