index.js (8022B)
1 var process = require('process') 2 // Handle SIGINT 3 process.on('SIGINT', () => { 4 console.info("SIGINT Received, exiting...") 5 process.exit(0) 6 }) 7 8 // Handle SIGTERM 9 process.on('SIGTERM', () => { 10 console.info("SIGTERM Received, exiting...") 11 process.exit(0) 12 }) 13 14 const parser = require('ua-parser-js'); 15 const { uniqueNamesGenerator, animals, colors } = require('unique-names-generator'); 16 17 class SnapdropServer { 18 19 constructor(port) { 20 const WebSocket = require('ws'); 21 this._wss = new WebSocket.Server({ port: port }); 22 this._wss.on('connection', (socket, request) => this._onConnection(new Peer(socket, request))); 23 this._wss.on('headers', (headers, response) => this._onHeaders(headers, response)); 24 25 this._rooms = {}; 26 27 console.log('Snapdrop is running on port', port); 28 } 29 30 _onConnection(peer) { 31 this._joinRoom(peer); 32 peer.socket.on('message', message => this._onMessage(peer, message)); 33 this._keepAlive(peer); 34 35 // send displayName 36 this._send(peer, { 37 type: 'display-name', 38 message: { 39 displayName: peer.name.displayName, 40 deviceName: peer.name.deviceName 41 } 42 }); 43 } 44 45 _onHeaders(headers, response) { 46 if (response.headers.cookie && response.headers.cookie.indexOf('peerid=') > -1) return; 47 response.peerId = Peer.uuid(); 48 headers.push('Set-Cookie: peerid=' + response.peerId + "; SameSite=Strict; Secure"); 49 } 50 51 _onMessage(sender, message) { 52 // Try to parse message 53 try { 54 message = JSON.parse(message); 55 } catch (e) { 56 return; // TODO: handle malformed JSON 57 } 58 59 switch (message.type) { 60 case 'disconnect': 61 this._leaveRoom(sender); 62 break; 63 case 'pong': 64 sender.lastBeat = Date.now(); 65 break; 66 } 67 68 // relay message to recipient 69 if (message.to && this._rooms[sender.ip]) { 70 const recipientId = message.to; // TODO: sanitize 71 const recipient = this._rooms[sender.ip][recipientId]; 72 delete message.to; 73 // add sender id 74 message.sender = sender.id; 75 this._send(recipient, message); 76 return; 77 } 78 } 79 80 _joinRoom(peer) { 81 // if room doesn't exist, create it 82 if (!this._rooms[peer.ip]) { 83 this._rooms[peer.ip] = {}; 84 } 85 86 // notify all other peers 87 for (const otherPeerId in this._rooms[peer.ip]) { 88 const otherPeer = this._rooms[peer.ip][otherPeerId]; 89 this._send(otherPeer, { 90 type: 'peer-joined', 91 peer: peer.getInfo() 92 }); 93 } 94 95 // notify peer about the other peers 96 const otherPeers = []; 97 for (const otherPeerId in this._rooms[peer.ip]) { 98 otherPeers.push(this._rooms[peer.ip][otherPeerId].getInfo()); 99 } 100 101 this._send(peer, { 102 type: 'peers', 103 peers: otherPeers 104 }); 105 106 // add peer to room 107 this._rooms[peer.ip][peer.id] = peer; 108 } 109 110 _leaveRoom(peer) { 111 if (!this._rooms[peer.ip] || !this._rooms[peer.ip][peer.id]) return; 112 this._cancelKeepAlive(this._rooms[peer.ip][peer.id]); 113 114 // delete the peer 115 delete this._rooms[peer.ip][peer.id]; 116 117 peer.socket.terminate(); 118 //if room is empty, delete the room 119 if (!Object.keys(this._rooms[peer.ip]).length) { 120 delete this._rooms[peer.ip]; 121 } else { 122 // notify all other peers 123 for (const otherPeerId in this._rooms[peer.ip]) { 124 const otherPeer = this._rooms[peer.ip][otherPeerId]; 125 this._send(otherPeer, { type: 'peer-left', peerId: peer.id }); 126 } 127 } 128 } 129 130 _send(peer, message) { 131 if (!peer) return; 132 if (this._wss.readyState !== this._wss.OPEN) return; 133 message = JSON.stringify(message); 134 peer.socket.send(message, error => ''); 135 } 136 137 _keepAlive(peer) { 138 this._cancelKeepAlive(peer); 139 var timeout = 30000; 140 if (!peer.lastBeat) { 141 peer.lastBeat = Date.now(); 142 } 143 if (Date.now() - peer.lastBeat > 2 * timeout) { 144 this._leaveRoom(peer); 145 return; 146 } 147 148 this._send(peer, { type: 'ping' }); 149 150 peer.timerId = setTimeout(() => this._keepAlive(peer), timeout); 151 } 152 153 _cancelKeepAlive(peer) { 154 if (peer && peer.timerId) { 155 clearTimeout(peer.timerId); 156 } 157 } 158 } 159 160 161 162 class Peer { 163 164 constructor(socket, request) { 165 // set socket 166 this.socket = socket; 167 168 169 // set remote ip 170 this._setIP(request); 171 172 // set peer id 173 this._setPeerId(request) 174 // is WebRTC supported ? 175 this.rtcSupported = request.url.indexOf('webrtc') > -1; 176 // set name 177 this._setName(request); 178 // for keepalive 179 this.timerId = 0; 180 this.lastBeat = Date.now(); 181 } 182 183 _setIP(request) { 184 if (request.headers['x-forwarded-for']) { 185 this.ip = request.headers['x-forwarded-for'].split(/\s*,\s*/)[0]; 186 } else { 187 this.ip = request.connection.remoteAddress; 188 } 189 // IPv4 and IPv6 use different values to refer to localhost 190 if (this.ip == '::1' || this.ip == '::ffff:127.0.0.1') { 191 this.ip = '127.0.0.1'; 192 } 193 } 194 195 _setPeerId(request) { 196 if (request.peerId) { 197 this.id = request.peerId; 198 } else { 199 this.id = request.headers.cookie.replace('peerid=', ''); 200 } 201 } 202 203 toString() { 204 return `<Peer id=${this.id} ip=${this.ip} rtcSupported=${this.rtcSupported}>` 205 } 206 207 _setName(req) { 208 let ua = parser(req.headers['user-agent']); 209 210 211 let deviceName = ''; 212 213 if (ua.os && ua.os.name) { 214 deviceName = ua.os.name.replace('Mac OS', 'Mac') + ' '; 215 } 216 217 if (ua.device.model) { 218 deviceName += ua.device.model; 219 } else { 220 deviceName += ua.browser.name; 221 } 222 223 if(!deviceName) 224 deviceName = 'Unknown Device'; 225 226 const displayName = uniqueNamesGenerator({ 227 length: 2, 228 separator: ' ', 229 dictionaries: [colors, animals], 230 style: 'capital', 231 seed: this.id.hashCode() 232 }) 233 234 this.name = { 235 model: ua.device.model, 236 os: ua.os.name, 237 browser: ua.browser.name, 238 type: ua.device.type, 239 deviceName, 240 displayName 241 }; 242 } 243 244 getInfo() { 245 return { 246 id: this.id, 247 name: this.name, 248 rtcSupported: this.rtcSupported 249 } 250 } 251 252 // return uuid of form xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx 253 static uuid() { 254 let uuid = '', 255 ii; 256 for (ii = 0; ii < 32; ii += 1) { 257 switch (ii) { 258 case 8: 259 case 20: 260 uuid += '-'; 261 uuid += (Math.random() * 16 | 0).toString(16); 262 break; 263 case 12: 264 uuid += '-'; 265 uuid += '4'; 266 break; 267 case 16: 268 uuid += '-'; 269 uuid += (Math.random() * 4 | 8).toString(16); 270 break; 271 default: 272 uuid += (Math.random() * 16 | 0).toString(16); 273 } 274 } 275 return uuid; 276 }; 277 } 278 279 Object.defineProperty(String.prototype, 'hashCode', { 280 value: function() { 281 var hash = 0, i, chr; 282 for (i = 0; i < this.length; i++) { 283 chr = this.charCodeAt(i); 284 hash = ((hash << 5) - hash) + chr; 285 hash |= 0; // Convert to 32bit integer 286 } 287 return hash; 288 } 289 }); 290 291 const server = new SnapdropServer(process.env.PORT || 3000);