snapdrop

A Progressive Web App for local file sharing https://github.com/RobinLinus/snapdrop snapdrop.net
git clone http://git.hanabi.in/repos/snapdrop.git
Log | Files | Refs | README | LICENSE

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);