afrokick 6 years ago
parent
commit
1e09fcfb64

+ 1 - 1
.travis.yml

@@ -1,3 +1,3 @@
 language: node_js
 node_js:
-  - 8.11.3
+  - 10.15.3

+ 2 - 3
Dockerfile

@@ -2,10 +2,9 @@ FROM node:alpine
 RUN mkdir /peer-server
 WORKDIR /peer-server
 COPY package.json .
-COPY bin ./bin
-COPY lib ./lib
+COPY src ./src
 COPY app.json .
 RUN npm install
 EXPOSE 9000
-ENTRYPOINT ["node", "bin/peerjs"]
+ENTRYPOINT ["node", "index.js"]
 CMD [ "--port", "9000" ]

+ 0 - 98
bin/peerjs

@@ -1,98 +0,0 @@
-#!/usr/bin/env node
-
-var path = require('path')
-  , pkg = require('../package.json')
-  , fs = require('fs')
-  , version = pkg.version
-  , PeerServer = require('../lib').PeerServer
-  , util = require('../lib/util')
-  , opts = require('optimist')
-    .usage('Usage: $0')
-    .options({
-      debug: {
-        demand: false,
-        alias: 'd',
-        description: 'debug',
-        default: false
-      },
-      timeout: {
-        demand: false,
-        alias: 't',
-        description: 'timeout (milliseconds)',
-        default: 5000
-      },
-      ip_limit: {
-        demand: false,
-        alias: 'i',
-        description: 'IP limit',
-        default: 5000
-      },
-      concurrent_limit: {
-        demand: false,
-        alias: 'c',
-        description: 'concurrent limit',
-        default: 5000
-      },
-      key: {
-        demand: false,
-        alias: 'k',
-        description: 'connection key',
-        default: 'peerjs'
-      },
-      sslkey: {
-        demand: false,
-        description: 'path to SSL key'
-      },
-      sslcert: {
-        demand: false,
-        description: 'path to SSL certificate'
-      },
-      port: {
-        demand: true,
-        alias: 'p',
-        description: 'port'
-      },
-      path: {
-        demand: false,
-        description: 'custom path',
-        default: '/'
-      },
-      allow_discovery: {
-        demand: false,
-        description: 'allow discovery of peers'
-      }
-    })
-    .boolean('allow_discovery')
-    .argv;
-
-process.on('uncaughtException', function(e) {
-  console.error('Error: ' + e);
-});
-
-if (opts.sslkey || opts.sslcert) {
-  if (opts.sslkey && opts.sslcert) {
-    opts.ssl = {
-      key: fs.readFileSync(path.resolve(opts.sslkey)),
-      cert: fs.readFileSync(path.resolve(opts.sslcert))
-    }
-
-    delete opts.sslkey;
-    delete opts.sslcert;
-  } else {
-    util.prettyError('Warning: PeerServer will not run because either ' +
-      'the key or the certificate has not been provided.');
-    process.exit(1);
-  }
-}
-
-
-var userPath = opts.path;
-var server = PeerServer(opts, function(server) {
-  var host = server.address().address;
-  var port = server.address().port;
-
-  console.log(
-    'Started PeerServer on %s, port: %s, path: %s (v. %s)',
-    host, port, userPath || '/', version
-  );
-});

+ 5 - 0
config/index.js

@@ -0,0 +1,5 @@
+const config = require('./schema');
+
+config.validate({ allowed: 'strict' });
+
+module.exports = config;

+ 74 - 0
config/schema.js

@@ -0,0 +1,74 @@
+const convict = require('convict');
+
+module.exports = convict({
+  debug: {
+    doc: 'Enable debug mode',
+    format: Boolean,
+    default: false
+  },
+  env: {
+    doc: 'The application environment.',
+    format: ['prod', 'dev', 'test'],
+    default: 'dev',
+    env: 'NODE_ENV'
+  },
+  port: {
+    doc: 'The port to bind.',
+    format: 'port',
+    default: 9000,
+    env: 'PORT',
+    arg: 'port'
+  },
+  timeout: {
+    doc: '',
+    format: 'duration',
+    default: 5000
+  },
+  key: {
+    doc: 'The key to check incoming clients',
+    format: String,
+    default: 'peerjs'
+  },
+  path: {
+    doc: '',
+    format: String,
+    default: '/'
+  },
+  ip_limit: {
+    doc: 'Max connections per ip',
+    format: 'duration',
+    default: 100
+  },
+  concurrent_limit: {
+    doc: 'Max connections',
+    format: 'duration',
+    default: 5000
+  },
+  allow_discovery: {
+    doc: 'Allow discovery of peers',
+    format: Boolean,
+    default: false
+  },
+  proxied: {
+    doc: 'Set true if server running behind proxy',
+    format: Boolean,
+    default: false
+  },
+  cleanup_out_msgs: {
+    doc: '',
+    format: 'duration',
+    default: 5000
+  },
+  ssl: {
+    key_path: {
+      doc: 'The path to the private key file',
+      format: String,
+      default: ''
+    },
+    cert_path: {
+      doc: 'The path to the cert file',
+      format: String,
+      default: ''
+    }
+  }
+});

+ 0 - 99
lib/index.js

@@ -1,99 +0,0 @@
-var express = require('express');
-var proto = require('./server');
-var util = require('./util');
-var http = require('http');
-var https = require('https');
-
-function ExpressPeerServer(server, options) {
-  var app = express();
-
-  util.extend(app, proto);
-
-  options = app._options = util.extend({
-    debug: false,
-    timeout: 5000,
-    key: 'peerjs',
-    ip_limit: 5000,
-    concurrent_limit: 5000,
-    allow_discovery: false,
-    proxied: false
-  }, options);
-
-  // Connected clients
-  app._clients = {};
-
-  // Messages waiting for another peer.
-  app._outstanding = {};
-
-  // Mark concurrent users per ip
-  app._ips = {};
-
-  if (options.proxied) {
-    app.set('trust proxy', options.proxied);
-  }
-
-  app.on('mount', function() {
-    if (!server) {
-      throw new Error('Server is not passed to constructor - '+
-        'can\'t start PeerServer');
-    }
-
-    // Initialize HTTP routes. This is only used for the first few milliseconds
-    // before a socket is opened for a Peer.
-    app._initializeHTTP();
-    app._setCleanupIntervals();
-    app._initializeWSS(server);
-  });
-
-  return app;
-}
-
-function PeerServer(options, callback) {
-  var app = express();
-
-  options = options || {};
-  var path = options.path || '/';
-  var port = options.port || 80;
-
-  delete options.path;
-
-  if (path[0] !== '/') {
-    path = '/' + path;
-  }
-
-  if (path[path.length - 1] !== '/') {
-    path += '/';
-  }
-
-  var server;
-  if (options.ssl) {
-    if (options.ssl.certificate) {
-      // Preserve compatibility with 0.2.7 API
-      options.ssl.cert = options.ssl.certificate;
-      delete options.ssl.certificate;
-    }
-
-    server = https.createServer(options.ssl, app);
-    delete options.ssl;
-  } else {
-    server = http.createServer(app);
-  }
-
-  var peerjs = ExpressPeerServer(server, options);
-  app.use(path, peerjs);
-
-  if (callback) {
-    server.listen(port, function() {
-      callback(server);
-    });
-  } else {
-    server.listen(port);
-  }
-
-  return peerjs;
-}
-
-exports = module.exports = {
-  ExpressPeerServer: ExpressPeerServer,
-  PeerServer: PeerServer
-};

+ 0 - 422
lib/server.js

@@ -1,422 +0,0 @@
-var util = require("./util");
-var bodyParser = require("body-parser");
-var WebSocketServer = require("ws").Server;
-var url = require("url");
-var cors = require("cors");
-
-var app = (exports = module.exports = {});
-
-/** Initialize WebSocket server. */
-app._initializeWSS = function(server) {
-  var self = this;
-
-  if (this.mountpath instanceof Array) {
-    throw new Error("This app can only be mounted on a single path");
-  }
-
-  var path = this.mountpath;
-  var path = path + (path[path.length - 1] != "/" ? "/" : "") + "peerjs";
-
-  // Create WebSocket server as well.
-  this._wss = new WebSocketServer({ path: path, server: server });
-
-  this._wss.on("connection", function(socket, req) {
-    var query = url.parse(req.url, true).query;
-    var id = query.id;
-    var token = query.token;
-    var key = query.key;
-    var ip = req.socket.remoteAddress;
-
-    if (!id || !token || !key) {
-      socket.send(
-        JSON.stringify({
-          type: "ERROR",
-          payload: { msg: "No id, token, or key supplied to websocket server" }
-        })
-      );
-      socket.close();
-      return;
-    }
-
-    if (!self._clients[key] || !self._clients[key][id]) {
-      self._checkKey(key, ip, function(err) {
-        if (!err) {
-          if (!self._clients[key][id]) {
-            self._clients[key][id] = { token: token, ip: ip };
-            self._ips[ip]++;
-            socket.send(JSON.stringify({ type: "OPEN" }));
-          }
-          self._configureWS(socket, key, id, token);
-        } else {
-          socket.send(JSON.stringify({ type: "ERROR", payload: { msg: err } }));
-        }
-      });
-    } else {
-      self._configureWS(socket, key, id, token);
-    }
-  });
-
-  this._wss.on("error", function (err) {
-    // handle error
-  })
-};
-
-app._configureWS = function(socket, key, id, token) {
-  var self = this;
-  var client = this._clients[key][id];
-
-  if (token === client.token) {
-    // res 'close' event will delete client.res for us
-    client.socket = socket;
-    // Client already exists
-    if (client.res) {
-      client.res.end();
-    }
-  } else {
-    // ID-taken, invalid token
-    socket.send(
-      JSON.stringify({ type: "ID-TAKEN", payload: { msg: "ID is taken" } })
-    );
-    socket.close();
-    return;
-  }
-
-  this._processOutstanding(key, id);
-
-  // Cleanup after a socket closes.
-  socket.on("close", function() {
-    self._log("Socket closed:", id);
-    if (client.socket == socket) {
-      self._removePeer(key, id);
-    }
-  });
-
-  // Handle messages from peers.
-  socket.on("message", function(data) {
-    try {
-      var message = JSON.parse(data);
-
-      if (
-        ["LEAVE", "CANDIDATE", "OFFER", "ANSWER"].indexOf(message.type) !== -1
-      ) {
-        self._handleTransmission(key, {
-          type: message.type,
-          src: id,
-          dst: message.dst,
-          payload: message.payload
-        });
-      } else if (message.type === 'HEARTBEAT') {
-        // Ignore - nothing needs doing here.
-      } else {
-        util.prettyError("Message unrecognized");
-      }
-    } catch (e) {
-      self._log("Invalid message", data);
-      throw e;
-    }
-  });
-
-  // We're going to emit here, because for XHR we don't *know* when someone
-  // disconnects.
-  this.emit("connection", id);
-};
-
-app._checkAllowsDiscovery = function(key, cb) {
-  cb(this._options.allow_discovery);
-};
-
-app._checkKey = function(key, ip, cb) {
-  if (key == this._options.key) {
-    if (!this._clients[key]) {
-      this._clients[key] = {};
-    }
-    if (!this._outstanding[key]) {
-      this._outstanding[key] = {};
-    }
-    if (!this._ips[ip]) {
-      this._ips[ip] = 0;
-    }
-    // Check concurrent limit
-    if (
-      Object.keys(this._clients[key]).length >= this._options.concurrent_limit
-    ) {
-      cb("Server has reached its concurrent user limit");
-      return;
-    }
-    if (this._ips[ip] >= this._options.ip_limit) {
-      cb(ip + " has reached its concurrent user limit");
-      return;
-    }
-    cb(null);
-  } else {
-    cb("Invalid key provided");
-  }
-};
-
-/** Initialize HTTP server routes. */
-app._initializeHTTP = function() {
-  var self = this;
-
-  this.use(cors());
-
-  this.get("/", function(req, res, next) {
-    res.send(require("../app.json"));
-  });
-
-  // Retrieve guaranteed random ID.
-  this.get("/:key/id", function(req, res, next) {
-    res.contentType = "text/html";
-    res.send(self._generateClientId(req.params.key));
-  });
-
-  // Server sets up HTTP streaming when you get post an ID.
-  this.post("/:key/:id/:token/id", function(req, res, next) {
-    var id = req.params.id;
-    var token = req.params.token;
-    var key = req.params.key;
-    var ip = req.connection.remoteAddress;
-
-    if (!self._clients[key] || !self._clients[key][id]) {
-      self._checkKey(key, ip, function(err) {
-        if (!err && !self._clients[key][id]) {
-          self._clients[key][id] = { token: token, ip: ip };
-          self._ips[ip]++;
-          self._startStreaming(res, key, id, token, true);
-        } else {
-          res.send(JSON.stringify({ type: "HTTP-ERROR" }));
-        }
-      });
-    } else {
-      self._startStreaming(res, key, id, token);
-    }
-  });
-
-  // Get a list of all peers for a key, enabled by the `allowDiscovery` flag.
-  this.get("/:key/peers", function(req, res, next) {
-    var key = req.params.key;
-    if (self._clients[key]) {
-      self._checkAllowsDiscovery(key, function(isAllowed) {
-        if (isAllowed) {
-          res.send(Object.keys(self._clients[key]));
-        } else {
-          res.sendStatus(401);
-        }
-      });
-    } else {
-      res.sendStatus(404);
-    }
-  });
-
-  var handle = function(req, res, next) {
-    var key = req.params.key;
-    var id = req.params.id;
-
-    var client;
-    if (!self._clients[key] || !(client = self._clients[key][id])) {
-      if (req.params.retry) {
-        res.sendStatus(401);
-        return;
-      } else {
-        // Retry this request
-        req.params.retry = true;
-        setTimeout(handle, 25, req, res);
-        return;
-      }
-    }
-
-    // Auth the req
-    if (client.token && req.params.token !== client.token) {
-      res.sendStatus(401);
-      return;
-    } else {
-      self._handleTransmission(key, {
-        type: req.body.type,
-        src: id,
-        dst: req.body.dst,
-        payload: req.body.payload
-      });
-      res.sendStatus(200);
-    }
-  };
-
-  var jsonParser = bodyParser.json();
-
-  this.post("/:key/:id/:token/offer", jsonParser, handle);
-
-  this.post("/:key/:id/:token/candidate", jsonParser, handle);
-
-  this.post("/:key/:id/:token/answer", jsonParser, handle);
-
-  this.post("/:key/:id/:token/leave", jsonParser, handle);
-};
-
-/** Saves a streaming response and takes care of timeouts and headers. */
-app._startStreaming = function(res, key, id, token, open) {
-  var self = this;
-
-  res.writeHead(200, { "Content-Type": "application/octet-stream" });
-
-  var pad = "00";
-  for (var i = 0; i < 10; i++) {
-    pad += pad;
-  }
-  res.write(pad + "\n");
-
-  if (open) {
-    res.write(JSON.stringify({ type: "OPEN" }) + "\n");
-  }
-
-  var client = this._clients[key][id];
-
-  if (token === client.token) {
-    // Client already exists
-    res.on("close", function() {
-      if (client.res === res) {
-        if (!client.socket) {
-          // No new request yet, peer dead
-          self._removePeer(key, id);
-          return;
-        }
-        delete client.res;
-      }
-    });
-    client.res = res;
-    this._processOutstanding(key, id);
-  } else {
-    // ID-taken, invalid token
-    res.end(JSON.stringify({ type: "HTTP-ERROR" }));
-  }
-};
-
-app._pruneOutstanding = function() {
-  var keys = Object.keys(this._outstanding);
-  for (var k = 0, kk = keys.length; k < kk; k += 1) {
-    var key = keys[k];
-    var dsts = Object.keys(this._outstanding[key]);
-    for (var i = 0, ii = dsts.length; i < ii; i += 1) {
-      var offers = this._outstanding[key][dsts[i]];
-      var seen = {};
-      for (var j = 0, jj = offers.length; j < jj; j += 1) {
-        var message = offers[j];
-        if (!seen[message.src]) {
-          this._handleTransmission(key, {
-            type: "EXPIRE",
-            src: message.dst,
-            dst: message.src
-          });
-          seen[message.src] = true;
-        }
-      }
-    }
-    this._outstanding[key] = {};
-  }
-};
-
-/** Cleanup */
-app._setCleanupIntervals = function() {
-  var self = this;
-
-  // Clean up ips every 10 minutes
-  setInterval(function() {
-    var keys = Object.keys(self._ips);
-    for (var i = 0, ii = keys.length; i < ii; i += 1) {
-      var key = keys[i];
-      if (self._ips[key] === 0) {
-        delete self._ips[key];
-      }
-    }
-  }, 600000);
-
-  // Clean up outstanding messages every 5 seconds
-  setInterval(function() {
-    self._pruneOutstanding();
-  }, 5000);
-};
-
-/** Process outstanding peer offers. */
-app._processOutstanding = function(key, id) {
-  var offers = this._outstanding[key][id];
-  if (!offers) {
-    return;
-  }
-  for (var j = 0, jj = offers.length; j < jj; j += 1) {
-    this._handleTransmission(key, offers[j]);
-  }
-  delete this._outstanding[key][id];
-};
-
-app._removePeer = function(key, id) {
-  if (this._clients[key] && this._clients[key][id]) {
-    this._ips[this._clients[key][id].ip]--;
-    delete this._clients[key][id];
-    this.emit("disconnect", id);
-  }
-};
-
-/** Handles passing on a message. */
-app._handleTransmission = function(key, message) {
-  var type = message.type;
-  var src = message.src;
-  var dst = message.dst;
-  var data = JSON.stringify(message);
-
-  var destination = this._clients[key][dst];
-
-  // User is connected!
-  if (destination) {
-    try {
-      this._log(type, "from", src, "to", dst);
-      if (destination.socket) {
-        destination.socket.send(data);
-      } else if (destination.res) {
-        data += "\n";
-        destination.res.write(data);
-      } else {
-        // Neither socket no res available. Peer dead?
-        throw "Peer dead";
-      }
-    } catch (e) {
-      // This happens when a peer disconnects without closing connections and
-      // the associated WebSocket has not closed.
-      // Tell other side to stop trying.
-      this._removePeer(key, dst);
-      this._handleTransmission(key, {
-        type: "LEAVE",
-        src: dst,
-        dst: src
-      });
-    }
-  } else {
-    // Wait for this client to connect/reconnect (XHR) for important
-    // messages.
-    if (type !== "LEAVE" && type !== "EXPIRE" && dst) {
-      var self = this;
-      if (!this._outstanding[key][dst]) {
-        this._outstanding[key][dst] = [];
-      }
-      this._outstanding[key][dst].push(message);
-    } else if (type === "LEAVE" && !dst) {
-      this._removePeer(key, src);
-    } else {
-      // Unavailable destination specified with message LEAVE or EXPIRE
-      // Ignore
-    }
-  }
-};
-
-app._generateClientId = function(key) {
-  var clientId = util.randomId();
-  if (!this._clients[key]) {
-    return clientId;
-  }
-  while (!!this._clients[key][clientId]) {
-    clientId = util.randomId();
-  }
-  return clientId;
-};
-
-app._log = function() {
-  if (this._options.debug) {
-    console.log.apply(console, arguments);
-  }
-};

+ 0 - 31
lib/util.js

@@ -1,31 +0,0 @@
-var util = {
-  debug: false,
-  inherits: function(ctor, superCtor) {
-    ctor.super_ = superCtor;
-    ctor.prototype = Object.create(superCtor.prototype, {
-      constructor: {
-        value: ctor,
-        enumerable: false,
-        writable: true,
-        configurable: true
-      }
-    });
-  },
-  extend: function(dest, source) {
-    source = source || {};
-    for(var key in source) {
-      if(source.hasOwnProperty(key)) {
-        dest[key] = source[key];
-      }
-    }
-    return dest;
-  },
-  randomId: function () {
-    return (Math.random().toString(36) + '0000000000000000000').substr(2, 16);
-  },
-  prettyError: function (msg) {
-    console.log('ERROR PeerServer: ', msg);
-  }
-};
-
-module.exports = util;

+ 14 - 15
package.json

@@ -2,33 +2,32 @@
   "name": "peer",
   "version": "0.2.9",
   "description": "PeerJS server component",
-  "main": "lib/index.js",
-  "bin": {
-    "peerjs": "./bin/peerjs"
-  },
+  "main": "src/index.js",
   "repository": {
     "type": "git",
     "url": "git://github.com/peers/peerjs-server.git"
   },
   "author": "Michelle Bu, Eric Zhang",
   "license": "MIT",
+  "scripts": {
+    "test": "mocha test",
+    "start": "node ./src/index.js"
+  },
   "dependencies": {
     "body-parser": "^1.18.3",
+    "convict": "^4.4.1",
+    "cors": "~2.8.4",
     "express": "^4.16.3",
-    "optimist": "~0.6.1",
-    "ws": "6.0.0",
-    "cors": "~2.8.4"
+    "log4js": "^4.1.0",
+    "ws": "6.0.0"
   },
   "devDependencies": {
-    "expect.js": "*",
-    "sinon": "*",
-    "mocha": "*"
+    "mocha": "^6.0.2",
+    "chai": "^4.2.0",
+    "semistandard": "^13.0.1",
+    "sinon": "^7.3.1"
   },
   "engines": {
-    "node": ">=0.8"
-  },
-  "scripts": {
-    "test": "mocha test",
-    "start": "bin/peerjs --port ${PORT:=9000}"
+    "node": "^10"
   }
 }

+ 18 - 0
src/api/index.js

@@ -0,0 +1,18 @@
+const express = require('express');
+const cors = require('cors');
+const bodyParser = require('body-parser');
+const authMiddleware = require('./middleware/auth');
+const publicContent = require('../../app.json');
+
+const app = module.exports = express.Router();
+
+const jsonParser = bodyParser.json();
+
+app.use(cors());
+
+app.get('/', (req, res, next) => {
+  res.send(publicContent);
+});
+
+app.use('/:key', authMiddleware, require('./v1/public'));
+app.use('/:key/:id/:token', authMiddleware, jsonParser, require('./v1/calls'));

+ 23 - 0
src/api/middleware/auth/index.js

@@ -0,0 +1,23 @@
+const realm = require('../../../services/realm');
+
+module.exports = (req, res, next) => {
+  const { id, token } = req.params;
+
+  const sendAuthError = () => res.sendStatus(401);
+
+  if (!id) {
+    return next();
+  }
+
+  const client = realm.getRealmByKey(id);
+
+  if (!realm) {
+    return sendAuthError();
+  }
+
+  if (client.getToken() && token !== client.getToken()) {
+    return sendAuthError();
+  }
+
+  next();
+};

+ 42 - 0
src/api/v1/calls/index.js

@@ -0,0 +1,42 @@
+const express = require('express');
+// const realm = require('../../../realm');
+
+const app = module.exports = express.Router();
+
+// const handle = (req, res, next) => {
+//   var id = req.params.id;
+
+//   let client;
+//   if (!(client = realm.getClientById(id))) {
+//     if (req.params.retry) {
+//       res.sendStatus(401);
+//       return;
+//     } else {
+//       // Retry this request
+//       req.params.retry = true;
+//       setTimeout(handle, 25, req, res);
+//       return;
+//     }
+//   }
+
+//   // Auth the req
+//   if (client.token && req.params.token !== client.token) {
+//     res.sendStatus(401);
+//   } else {
+//     self._handleTransmission(key, {
+//       type: req.body.type,
+//       src: id,
+//       dst: req.body.dst,
+//       payload: req.body.payload
+//     });
+//     res.sendStatus(200);
+//   }
+// };
+
+// app.post('/:key/:id/:token/offer', jsonParser, handle);
+
+// app.post('/:key/:id/:token/candidate', jsonParser, handle);
+
+// app.post('/:key/:id/:token/answer', jsonParser, handle);
+
+// app.post('/:key/:id/:token/leave', jsonParser, handle);

+ 65 - 0
src/api/v1/public/index.js

@@ -0,0 +1,65 @@
+const express = require('express');
+const realm = require('../../../services/realm');
+const config = require('../../../../config');
+
+const app = module.exports = express.Router();
+
+const randomId = () => {
+  return (Math.random().toString(36) + '0000000000000000000').substr(2, 16);
+};
+
+const generateClientId = (key) => {
+  let clientId = randomId();
+
+  const realm = realmsCache.getRealmByKey(key);
+  if (!realm) {
+    return clientId;
+  }
+
+  while (realm.getClientById(clientId)) {
+    clientId = randomId();
+  }
+
+  return clientId;
+};
+
+// Retrieve guaranteed random ID.
+app.get('/id', (req, res, next) => {
+  const { key } = req.params;
+
+  res.contentType = 'text/html';
+  res.send(generateClientId(key));
+});
+
+// Get a list of all peers for a key, enabled by the `allowDiscovery` flag.
+app.get('/peers', (req, res, next) => {
+  if (config.get('allow_discovery')) {
+    const clientsIds = realm.getClientsIds();
+
+    return res.send(clientsIds);
+  }
+
+  res.sendStatus(401);
+});
+
+// Server sets up HTTP streaming when you get post an ID.
+// app.post('/:id/:token/id', (req, res, next) => {
+//   var id = req.params.id;
+//   var token = req.params.token;
+//   var key = req.params.key;
+//   var ip = req.connection.remoteAddress;
+
+//   if (!self._clients[key] || !self._clients[key][id]) {
+//     self._checkKey(key, ip, function (err) {
+//       if (!err && !self._clients[key][id]) {
+//         self._clients[key][id] = { token: token, ip: ip };
+//         self._ips[ip]++;
+//         self._startStreaming(res, key, id, token, true);
+//       } else {
+//         res.send(JSON.stringify({ type: 'HTTP-ERROR' }));
+//       }
+//     });
+//   } else {
+//     self._startStreaming(res, key, id, token);
+//   }
+// });

+ 10 - 0
src/enums.js

@@ -0,0 +1,10 @@
+module.exports.Errors = {};
+
+module.exports.MessageType = {
+  LEAVE: 'LEAVE',
+  CANDIDATE: 'CANDIDATE',
+  OFFER: 'OFFER',
+  ANSWER: 'ANSWER',
+  EXPIRE: 'EXPIRE',
+  HEARTBEAT: 'HEARTBEAT'
+};

+ 124 - 0
src/index.js

@@ -0,0 +1,124 @@
+const express = require('express');
+const http = require('http');
+const https = require('https');
+const fs = require('fs');
+
+const config = require('../config');
+const WebSocketServer = require('./services/webSocketServer');
+const logger = require('./services/logger');
+const api = require('./api');
+const messageHandler = require('./messageHandler');
+const realm = require('./services/realm');
+const MessageType = require('./enums');
+
+// parse config
+let path = config.get('path');
+const port = config.get('port');
+
+if (path[0] !== '/') {
+  path = '/' + path;
+}
+
+if (path[path.length - 1] !== '/') {
+  path += '/';
+}
+
+const app = express();
+
+if (config.get('proxied')) {
+  app.set('trust proxy', config.get('proxied'));
+}
+
+app.on('mount', () => {
+  if (!server) {
+    throw new Error('Server is not passed to constructor - ' +
+        'can\'t start PeerServer');
+  }
+
+  // TODO
+  app._setCleanupIntervals();
+
+  const wss = new WebSocketServer(server, app.mountpath);
+
+  wss.on('connection', client => {
+    const messages = realm.getMessageQueueById(client.getId());
+
+    messages.forEach(message => messageHandler(client, message));
+
+    realm.clearMessageQueue(client.getId());
+
+    logger.info(`client ${client.getId()} was connected`);
+  });
+
+  wss.on('message', (client, message) => {
+    messageHandler(client, message);
+  });
+
+  wss.on('close', client => {
+    logger.info(`client ${client.getId()} was disconnected`);
+  });
+
+  wss.on('error', error => {
+    logger.error(error);
+  });
+
+  app._wss = wss;
+});
+
+let server;
+
+if (config.get('ssl.key_path') && config.get('ssl.cert_path')) {
+  const keyPath = config.get('ssl.key_path');
+  const certPath = config.get('ssl.cert_path');
+
+  const opts = {
+    key: fs.readFileSync(path.resolve(keyPath)),
+    cert: fs.readFileSync(path.resolve(certPath))
+  };
+
+  server = https.createServer(opts, app);
+} else {
+  server = http.createServer(app);
+}
+
+app.use(path, api);
+
+server.listen(port, () => {
+  const host = server.address().address;
+  const port = server.address().port;
+
+  logger.info(
+    'Started PeerServer on %s, port: %s',
+    host, port
+  );
+});
+
+const pruneOutstanding = () => {
+  const destinationClientsIds = realm.messageQueue.keys();
+
+  for (const destinationClientId of destinationClientsIds) {
+    const messages = realm.getMessageQueueById(destinationClientId);
+
+    const seen = {};
+
+    for (const message of messages) {
+      if (!seen[message.src]) {
+        messageHandler(null, {
+          type: MessageType.EXPIRE,
+          src: message.dst,
+          dst: message.src
+        });
+        seen[message.src] = true;
+      }
+    }
+  }
+
+  realm.messageQueue.clear();
+
+  logger.debug(`message queue was cleared`);
+};
+
+// Clean up outstanding messages
+setInterval(() => {
+  pruneOutstanding();
+}, config.get('cleanup_out_msgs'));

+ 55 - 0
src/messageHandler/handlers/transmission/index.js

@@ -0,0 +1,55 @@
+const realm = require('../../../services/realm');
+const logger = require('../../../services/logger');
+const MessageType = require('../../../enums');
+
+const handler = (client, message) => {
+  const type = message.type;
+  const srcId = message.src;
+  const dstId = message.dst;
+
+  const destinationClient = realm.getClientById(dstId);
+
+  // User is connected!
+  if (destinationClient) {
+    try {
+      logger.debug(type, 'from', srcId, 'to', dstId);
+
+      if (destinationClient.socket) {
+        const data = JSON.stringify(message);
+
+        destinationClient.socket.send(data);
+      } else {
+        // Neither socket no res available. Peer dead?
+        throw new Error('Peer dead');
+      }
+    } catch (e) {
+      // This happens when a peer disconnects without closing connections and
+      // the associated WebSocket has not closed.
+      // Tell other side to stop trying.
+      if (destinationClient.socket) {
+        destinationClient.socket.close();
+      } else {
+        realm.removeClientById(destinationClient.getId());
+      }
+
+      handler(client, {
+        type: MessageType.LEAVE,
+        src: dstId,
+        dst: srcId
+      });
+    }
+  } else {
+    // Wait for this client to connect/reconnect (XHR) for important
+    // messages.
+    if (type !== MessageType.LEAVE && type !== MessageType.EXPIRE && dstId) {
+      realm.addMessageToQueue(dstId, message);
+    } else if (type === MessageType.LEAVE && !dstId) {
+      realm.removeClientById(srcId);
+    } else {
+      // Unavailable destination specified with message LEAVE or EXPIRE
+      // Ignore
+    }
+  }
+};
+
+module.exports = handler;

+ 40 - 0
src/messageHandler/index.js

@@ -0,0 +1,40 @@
+const logger = require('../services/logger');
+const MessageType = require('../enums');
+const transmissionHandler = require('./handlers/transmission');
+
+const handlers = {};
+
+const registerHandler = (messageType, handler) => {
+  handlers[messageType] = handler;
+};
+
+module.exports = (client, message) => {
+  const { type } = message;
+
+  const handler = handlers[type];
+
+  if (!handler) {
+    return logger.error('Message unrecognized');
+  }
+
+  handler(client, message);
+};
+
+const handleTransmission = (client, message) => {
+  transmissionHandler(client, {
+    type: message.type,
+    src: client.getId(),
+    dst: message.dst,
+    payload: message.payload
+  });
+};
+
+const handleHeartbeat = (client, message) => {
+
+};
+
+registerHandler(MessageType.HEARTBEAT, handleHeartbeat);
+registerHandler(MessageType.OFFER, handleTransmission);
+registerHandler(MessageType.ANSWER, handleTransmission);
+registerHandler(MessageType.CANDIDATE, handleTransmission);
+registerHandler(MessageType.LEAVE, handleTransmission);

+ 30 - 0
src/models/client.js

@@ -0,0 +1,30 @@
+class Client {
+  constructor ({ id, token, ip }) {
+    this.id = id;
+    this.token = token;
+    this.ip = ip;
+    this.socket = null;
+  }
+
+  getId () {
+    return this.id;
+  }
+
+  getToken () {
+    return this.token;
+  }
+
+  getIp () {
+    return this.ip;
+  }
+
+  setSocket (socket) {
+    this.socket = socket;
+  }
+
+  send (data) {
+    this.socket.send(JSON.stringify(data));
+  }
+}
+
+module.exports = Client;

+ 3 - 0
src/services/errors/index.js

@@ -0,0 +1,3 @@
+module.exports = {
+  INVALID_KEY: 'Invalid key provided'
+};

+ 6 - 0
src/services/logger/index.js

@@ -0,0 +1,6 @@
+const log4js = require('log4js');
+
+const logger = log4js.getLogger();
+logger.level = 'ALL';
+
+module.exports = logger;

+ 44 - 0
src/services/realm/index.js

@@ -0,0 +1,44 @@
+class Realm {
+  constructor () {
+    this.clients = new Map();
+    this.messageQueue = new Map();
+  }
+
+  getClientsIds () {
+    return [...this.clients.keys()];
+  }
+
+  getClientById (clientId) {
+    return this.clients.get(clientId);
+  }
+
+  setClient (client, id) {
+    this.clients.set(id, client);
+  }
+
+  removeClientById (id) {
+    const client = this.getClientById(id);
+
+    if (!client) return false;
+
+    this.clients.delete(id);
+  }
+
+  getMessageQueueById (id) {
+    return this.messageQueue.get(id);
+  }
+
+  addMessageToQueue (id, message) {
+    if (!this.getMessageQueueById(id)) {
+      this.messageQueue.set(id, []);
+    }
+
+    this.getMessageQueueById(id).push(message);
+  }
+
+  clearMessageQueue (id) {
+    this.messageQueue.delete(id);
+  }
+}
+
+module.exports = new Realm();

+ 154 - 0
src/services/webSocketServer/index.js

@@ -0,0 +1,154 @@
+const WSS = require('ws').Server;
+const url = require('url');
+const EventEmitter = require('events');
+const logger = require('../logger');
+
+const config = require('../../../config');
+const realm = require('../realm');
+const Client = require('../../models/client');
+
+class WebSocketServer extends EventEmitter {
+  constructor (server, mountpath) {
+    super();
+    this.setMaxListeners(0);
+
+    this._ips = {};
+
+    if (mountpath instanceof Array) {
+      throw new Error('This app can only be mounted on a single path');
+    }
+
+    let path = mountpath;
+    path = path + (path[path.length - 1] !== '/' ? '/' : '') + 'peerjs';
+
+    this._wss = new WSS({ path, server });
+
+    this._wss.on('connection', this._onSocketConnection);
+    this._wss.on('error', this._onSocketError);
+  }
+
+  _onSocketConnection (socket, req) {
+    const { query = {} } = url.parse(req.url, true);
+
+    const { id, token, key } = query;
+
+    if (!id || !token || !key) {
+      return this._sendErrorAndClose(socket, 'No id, token, or key supplied to websocket server');
+    }
+
+    if (key !== config.get('key')) {
+      return this._sendErrorAndClose(socket, 'Invalid key provided');
+    }
+
+    const client = realm.getClientById(id);
+
+    if (client) {
+      if (token !== client.getToken()) {
+        // ID-taken, invalid token
+        socket.send(JSON.stringify({
+          type: 'ID-TAKEN',
+          payload: { msg: 'ID is taken' }
+        }));
+
+        return socket.close();
+      }
+
+      return this._configureWS(socket, client);
+    }
+
+    this._registerClient({ socket, id, token });
+  }
+
+  _onSocketError (error) {
+    // handle error
+    this.emit('error', error);
+  }
+
+  _registerClient ({ socket, id, token }) {
+    const ip = socket.remoteAddress;
+
+    if (!this._ips[ip]) {
+      this._ips[ip] = 0;
+    }
+
+    // Check concurrent limit
+    const clientsCount = realm.getClientsIds().length;
+
+    if (clientsCount >= config.get('concurrent_limit')) {
+      return this._sendErrorAndClose(socket, 'Server has reached its concurrent user limit');
+    }
+
+    const connectionsPerIP = this._ips[ip];
+
+    if (connectionsPerIP >= config.get('ip_limit')) {
+      return this._sendErrorAndClose(socket, `${ip} has reached its concurrent user limit`);
+    }
+
+    const oldClient = realm.getClientById(id);
+
+    if (oldClient) {
+      return this._sendErrorAndClose(socket, `${id} already registered`);
+    }
+
+    const newClient = new Client({ id, token, ip });
+    realm.setClient(newClient, id);
+    socket.send(JSON.stringify({ type: 'OPEN' }));
+    this._ips[ip]++;
+    this._configureWS(socket, newClient);
+  }
+
+  _configureWS (socket, client) {
+    if (client.socket && socket !== client.socket) {
+      // TODO remove old ip, add new ip
+    }
+
+    client.setSocket(socket);
+
+    // Cleanup after a socket closes.
+    socket.on('close', () => {
+      logger.info('Socket closed:', client.getId());
+
+      const ip = socket.remoteAddress;
+
+      if (this._ips[ip]) {
+        this._ips[ip]--;
+
+        if (this._ips[ip] === 0) {
+          delete this._ips[ip];
+        }
+      }
+
+      if (client.socket === socket) {
+        realm.removeClientById(client.getId());
+        this.emit('close', client);
+      }
+    });
+
+    // Handle messages from peers.
+    socket.on('message', (data) => {
+      try {
+        const message = JSON.parse(data);
+
+        this.emit('message', client, message);
+      } catch (e) {
+        logger.error('Invalid message', data);
+        throw e;
+      }
+    });
+
+    this.emit('connection', client);
+  }
+
+  _sendErrorAndClose (socket, msg) {
+    socket.send(
+      JSON.stringify({
+        type: 'ERROR',
+        payload: { msg }
+      })
+    );
+
+    socket.close();
+  }
+}
+
+module.exports = WebSocketServer;

+ 47 - 47
test/server.js

@@ -1,56 +1,56 @@
-var ExpressPeerServer = require('../').ExpressPeerServer;
-var expect = require('expect.js');
-var sinon = require('sinon');
+const ExpressPeerServer = require('../').ExpressPeerServer;
+const { expect } = require('chai');
+const sinon = require('sinon');
 
-describe('ExpressPeerServer', function() {
-  describe('method', function() {
+describe('ExpressPeerServer', function () {
+  describe('method', function () {
     var p;
 
-    before(function() {
-      p = ExpressPeerServer(undefined, {port: 8000});
+    before(function () {
+      p = ExpressPeerServer(undefined, { port: 8000 });
     });
 
-    describe('#_checkKey', function() {
-      it('should reject keys that are not the default', function(done) {
-        p._checkKey('bad key', null, function(response) {
+    describe('#_checkKey', function () {
+      it('should reject keys that are not the default', function (done) {
+        p._checkKey('bad key', null, function (response) {
           expect(response).to.be('Invalid key provided');
           done();
         });
       });
 
-      it('should accept valid key/ip pairs', function(done) {
-        p._checkKey('peerjs', 'myip', function(response) {
+      it('should accept valid key/ip pairs', function (done) {
+        p._checkKey('peerjs', 'myip', function (response) {
           expect(response).to.be(null);
           done();
         });
       });
 
-      it('should reject ips that are at their limit', function(done) {
+      it('should reject ips that are at their limit', function (done) {
         p._options.ip_limit = 0;
-        p._checkKey('peerjs', 'myip', function(response) {
+        p._checkKey('peerjs', 'myip', function (response) {
           expect(response).to.be('myip has reached its concurrent user limit');
           done();
         });
       });
 
-      it('should reject when the server is at its limit', function(done) {
+      it('should reject when the server is at its limit', function (done) {
         p._options.concurrent_limit = 0;
-        p._checkKey('peerjs', 'myip', function(response) {
+        p._checkKey('peerjs', 'myip', function (response) {
           expect(response).to.be('Server has reached its concurrent user limit');
           done();
         });
       });
     });
 
-    describe('#_removePeer', function() {
-      before(function() {
-        var fake = {ip: '0.0.0.0'};
+    describe('#_removePeer', function () {
+      before(function () {
+        var fake = { ip: '0.0.0.0' };
         p._ips[fake.ip] = 1;
         p._clients['peerjs'] = {};
         p._clients['peerjs']['test'] = fake;
       });
 
-      it('should decrement the number of ips being used and remove the connection', function() {
+      it('should decrement the number of ips being used and remove the connection', function () {
         expect(p._ips['0.0.0.0']).to.be(1);
         p._removePeer('peerjs', 'test');
         expect(p._ips['0.0.0.0']).to.be(0);
@@ -58,94 +58,94 @@ describe('ExpressPeerServer', function() {
       });
     });
 
-    describe('#_handleTransmission', function() {
+    describe('#_handleTransmission', function () {
       var KEY = 'peerjs';
       var ID = 'test';
 
-      before(function() {
+      before(function () {
         p._clients[KEY] = {};
       });
 
-      it('should send to the socket when appropriate', function() {
+      it('should send to the socket when appropriate', function () {
         var send = sinon.spy();
         var write = sinon.spy();
-        var message = {dst: ID};
+        var message = { dst: ID };
         p._clients[KEY][ID] = {
           socket: {
-          send: send
+            send: send
           },
           res: {
-          write: write
+            write: write
           }
-        }
+        };
         p._handleTransmission(KEY, message);
         expect(send.calledWith(JSON.stringify(message))).to.be(true);
         expect(write.calledWith(JSON.stringify(message))).to.be(false);
       });
 
-      it('should write to the response with a newline when appropriate', function() {
+      it('should write to the response with a newline when appropriate', function () {
         var write = sinon.spy();
-        var message = {dst: ID};
+        var message = { dst: ID };
         p._clients[KEY][ID] = {
           res: {
-          write: write
+            write: write
           }
-        }
+        };
         p._handleTransmission(KEY, message);
         expect(write.calledWith(JSON.stringify(message) + '\n')).to.be(true);
       });
 
       // no destination.
-      it('should push to outstanding messages if the destination is not found', function() {
-        var message = {dst: ID};
+      it('should push to outstanding messages if the destination is not found', function () {
+        var message = { dst: ID };
         p._outstanding[KEY] = {};
         p._clients[KEY] = {};
         p._handleTransmission(KEY, message);
         expect(p._outstanding[KEY][ID][0]).to.be(message);
       });
 
-      it('should not push to outstanding messages if the message is a LEAVE or EXPIRE', function() {
-        var message = {dst: ID, type: 'LEAVE'};
+      it('should not push to outstanding messages if the message is a LEAVE or EXPIRE', function () {
+        var message = { dst: ID, type: 'LEAVE' };
         p._outstanding[KEY] = {};
         p._clients[KEY] = {};
         p._handleTransmission(KEY, message);
         expect(p._outstanding[KEY][ID]).to.be(undefined);
 
-        message = {dst: ID, type: 'EXPIRE'};
+        message = { dst: ID, type: 'EXPIRE' };
         p._handleTransmission(KEY, message);
         expect(p._outstanding[KEY][ID]).to.be(undefined);
       });
 
-      it('should remove the peer if there is no dst in the message', function() {
-        var message = {type: 'LEAVE'};
+      it('should remove the peer if there is no dst in the message', function () {
+        var message = { type: 'LEAVE' };
         p._removePeer = sinon.spy();
         p._outstanding[KEY] = {};
         p._handleTransmission(KEY, message);
         expect(p._removePeer.calledWith(KEY, undefined)).to.be(true);
       });
 
-      it('should remove the peer and send a LEAVE message if the socket appears to be closed', function() {
+      it('should remove the peer and send a LEAVE message if the socket appears to be closed', function () {
         var send = sinon.stub().throws();
-        var message = {dst: ID};
-        var leaveMessage = {type: 'LEAVE', dst: undefined, src: ID};
+        var message = { dst: ID };
+        var leaveMessage = { type: 'LEAVE', dst: undefined, src: ID };
         var oldHandleTransmission = p._handleTransmission;
-        p._removePeer = function() {
+        p._removePeer = function () {
           // Hacks!
           p._handleTransmission = sinon.spy();
         };
         p._clients[KEY][ID] = {
           socket: {
-          send: send
+            send: send
           }
-        }
+        };
         p._handleTransmission(KEY, message);
         expect(p._handleTransmission.calledWith(KEY, leaveMessage)).to.be(true);
       });
     });
 
-    describe('#_generateClientId', function() {
-      it('should generate a 16-character ID', function() {
-      expect(p._generateClientId('anykey').length).to.be(16);
+    describe('#_generateClientId', function () {
+      it('should generate a 16-character ID', function () {
+        expect(p._generateClientId('anykey').length).to.be(16);
       });
     });
   });

+ 0 - 581
yarn.lock

@@ -1,581 +0,0 @@
-# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
-# yarn lockfile v1
-
-
-"@sinonjs/commons@^1.0.1":
-  version "1.0.2"
-  resolved "https://registry.yarnpkg.com/@sinonjs/commons/-/commons-1.0.2.tgz#3e0ac737781627b8844257fadc3d803997d0526e"
-  dependencies:
-    type-detect "4.0.8"
-
-"@sinonjs/formatio@^2.0.0":
-  version "2.0.0"
-  resolved "https://registry.yarnpkg.com/@sinonjs/formatio/-/formatio-2.0.0.tgz#84db7e9eb5531df18a8c5e0bfb6e449e55e654b2"
-  dependencies:
-    samsam "1.3.0"
-
-"@sinonjs/samsam@^2.0.0":
-  version "2.0.0"
-  resolved "https://registry.yarnpkg.com/@sinonjs/samsam/-/samsam-2.0.0.tgz#9163742ac35c12d3602dece74317643b35db6a80"
-
-accepts@~1.3.5:
-  version "1.3.5"
-  resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.5.tgz#eb777df6011723a3b14e8a72c0805c8e86746bd2"
-  dependencies:
-    mime-types "~2.1.18"
-    negotiator "0.6.1"
-
-array-flatten@1.1.1:
-  version "1.1.1"
-  resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2"
-
-async-limiter@~1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/async-limiter/-/async-limiter-1.0.0.tgz#78faed8c3d074ab81f22b4e985d79e8738f720f8"
-
-balanced-match@^1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767"
-
-body-parser@1.18.2:
-  version "1.18.2"
-  resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.18.2.tgz#87678a19d84b47d859b83199bd59bce222b10454"
-  dependencies:
-    bytes "3.0.0"
-    content-type "~1.0.4"
-    debug "2.6.9"
-    depd "~1.1.1"
-    http-errors "~1.6.2"
-    iconv-lite "0.4.19"
-    on-finished "~2.3.0"
-    qs "6.5.1"
-    raw-body "2.3.2"
-    type-is "~1.6.15"
-
-body-parser@^1.18.3:
-  version "1.18.3"
-  resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.18.3.tgz#5b292198ffdd553b3a0f20ded0592b956955c8b4"
-  dependencies:
-    bytes "3.0.0"
-    content-type "~1.0.4"
-    debug "2.6.9"
-    depd "~1.1.2"
-    http-errors "~1.6.3"
-    iconv-lite "0.4.23"
-    on-finished "~2.3.0"
-    qs "6.5.2"
-    raw-body "2.3.3"
-    type-is "~1.6.16"
-
-brace-expansion@^1.1.7:
-  version "1.1.11"
-  resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd"
-  dependencies:
-    balanced-match "^1.0.0"
-    concat-map "0.0.1"
-
-browser-stdout@1.3.1:
-  version "1.3.1"
-  resolved "https://registry.yarnpkg.com/browser-stdout/-/browser-stdout-1.3.1.tgz#baa559ee14ced73452229bad7326467c61fabd60"
-
-bytes@3.0.0:
-  version "3.0.0"
-  resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.0.0.tgz#d32815404d689699f85a4ea4fa8755dd13a96048"
-
-commander@2.15.1:
-  version "2.15.1"
-  resolved "https://registry.yarnpkg.com/commander/-/commander-2.15.1.tgz#df46e867d0fc2aec66a34662b406a9ccafff5b0f"
-
-concat-map@0.0.1:
-  version "0.0.1"
-  resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
-
-content-disposition@0.5.2:
-  version "0.5.2"
-  resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.2.tgz#0cf68bb9ddf5f2be7961c3a85178cb85dba78cb4"
-
-content-type@~1.0.4:
-  version "1.0.4"
-  resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b"
-
-cookie-signature@1.0.6:
-  version "1.0.6"
-  resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c"
-
-cookie@0.3.1:
-  version "0.3.1"
-  resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.3.1.tgz#e7e0a1f9ef43b4c8ba925c5c5a96e806d16873bb"
-
-cors@~2.8.4:
-  version "2.8.4"
-  resolved "https://registry.yarnpkg.com/cors/-/cors-2.8.4.tgz#2bd381f2eb201020105cd50ea59da63090694686"
-  dependencies:
-    object-assign "^4"
-    vary "^1"
-
-debug@2.6.9:
-  version "2.6.9"
-  resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f"
-  dependencies:
-    ms "2.0.0"
-
-debug@3.1.0:
-  version "3.1.0"
-  resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261"
-  dependencies:
-    ms "2.0.0"
-
-depd@1.1.1:
-  version "1.1.1"
-  resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.1.tgz#5783b4e1c459f06fa5ca27f991f3d06e7a310359"
-
-depd@~1.1.1, depd@~1.1.2:
-  version "1.1.2"
-  resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9"
-
-destroy@~1.0.4:
-  version "1.0.4"
-  resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.0.4.tgz#978857442c44749e4206613e37946205826abd80"
-
-diff@3.5.0, diff@^3.5.0:
-  version "3.5.0"
-  resolved "https://registry.yarnpkg.com/diff/-/diff-3.5.0.tgz#800c0dd1e0a8bfbc95835c202ad220fe317e5a12"
-
-ee-first@1.1.1:
-  version "1.1.1"
-  resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d"
-
-encodeurl@~1.0.2:
-  version "1.0.2"
-  resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59"
-
-escape-html@~1.0.3:
-  version "1.0.3"
-  resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988"
-
-escape-string-regexp@1.0.5:
-  version "1.0.5"
-  resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4"
-
-etag@~1.8.1:
-  version "1.8.1"
-  resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887"
-
-expect.js@*:
-  version "0.3.1"
-  resolved "https://registry.yarnpkg.com/expect.js/-/expect.js-0.3.1.tgz#b0a59a0d2eff5437544ebf0ceaa6015841d09b5b"
-
-express@^4.16.3:
-  version "4.16.3"
-  resolved "https://registry.yarnpkg.com/express/-/express-4.16.3.tgz#6af8a502350db3246ecc4becf6b5a34d22f7ed53"
-  dependencies:
-    accepts "~1.3.5"
-    array-flatten "1.1.1"
-    body-parser "1.18.2"
-    content-disposition "0.5.2"
-    content-type "~1.0.4"
-    cookie "0.3.1"
-    cookie-signature "1.0.6"
-    debug "2.6.9"
-    depd "~1.1.2"
-    encodeurl "~1.0.2"
-    escape-html "~1.0.3"
-    etag "~1.8.1"
-    finalhandler "1.1.1"
-    fresh "0.5.2"
-    merge-descriptors "1.0.1"
-    methods "~1.1.2"
-    on-finished "~2.3.0"
-    parseurl "~1.3.2"
-    path-to-regexp "0.1.7"
-    proxy-addr "~2.0.3"
-    qs "6.5.1"
-    range-parser "~1.2.0"
-    safe-buffer "5.1.1"
-    send "0.16.2"
-    serve-static "1.13.2"
-    setprototypeof "1.1.0"
-    statuses "~1.4.0"
-    type-is "~1.6.16"
-    utils-merge "1.0.1"
-    vary "~1.1.2"
-
-finalhandler@1.1.1:
-  version "1.1.1"
-  resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.1.1.tgz#eebf4ed840079c83f4249038c9d703008301b105"
-  dependencies:
-    debug "2.6.9"
-    encodeurl "~1.0.2"
-    escape-html "~1.0.3"
-    on-finished "~2.3.0"
-    parseurl "~1.3.2"
-    statuses "~1.4.0"
-    unpipe "~1.0.0"
-
-forwarded@~0.1.2:
-  version "0.1.2"
-  resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.1.2.tgz#98c23dab1175657b8c0573e8ceccd91b0ff18c84"
-
-fresh@0.5.2:
-  version "0.5.2"
-  resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7"
-
-fs.realpath@^1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f"
-
-glob@7.1.2:
-  version "7.1.2"
-  resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.2.tgz#c19c9df9a028702d678612384a6552404c636d15"
-  dependencies:
-    fs.realpath "^1.0.0"
-    inflight "^1.0.4"
-    inherits "2"
-    minimatch "^3.0.4"
-    once "^1.3.0"
-    path-is-absolute "^1.0.0"
-
-growl@1.10.5:
-  version "1.10.5"
-  resolved "https://registry.yarnpkg.com/growl/-/growl-1.10.5.tgz#f2735dc2283674fa67478b10181059355c369e5e"
-
-has-flag@^3.0.0:
-  version "3.0.0"
-  resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd"
-
-he@1.1.1:
-  version "1.1.1"
-  resolved "https://registry.yarnpkg.com/he/-/he-1.1.1.tgz#93410fd21b009735151f8868c2f271f3427e23fd"
-
-http-errors@1.6.2:
-  version "1.6.2"
-  resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.6.2.tgz#0a002cc85707192a7e7946ceedc11155f60ec736"
-  dependencies:
-    depd "1.1.1"
-    inherits "2.0.3"
-    setprototypeof "1.0.3"
-    statuses ">= 1.3.1 < 2"
-
-http-errors@1.6.3, http-errors@~1.6.2, http-errors@~1.6.3:
-  version "1.6.3"
-  resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.6.3.tgz#8b55680bb4be283a0b5bf4ea2e38580be1d9320d"
-  dependencies:
-    depd "~1.1.2"
-    inherits "2.0.3"
-    setprototypeof "1.1.0"
-    statuses ">= 1.4.0 < 2"
-
-iconv-lite@0.4.19:
-  version "0.4.19"
-  resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.19.tgz#f7468f60135f5e5dad3399c0a81be9a1603a082b"
-
-iconv-lite@0.4.23:
-  version "0.4.23"
-  resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.23.tgz#297871f63be507adcfbfca715d0cd0eed84e9a63"
-  dependencies:
-    safer-buffer ">= 2.1.2 < 3"
-
-inflight@^1.0.4:
-  version "1.0.6"
-  resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9"
-  dependencies:
-    once "^1.3.0"
-    wrappy "1"
-
-inherits@2, inherits@2.0.3:
-  version "2.0.3"
-  resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de"
-
-ipaddr.js@1.8.0:
-  version "1.8.0"
-  resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.8.0.tgz#eaa33d6ddd7ace8f7f6fe0c9ca0440e706738b1e"
-
-isarray@0.0.1:
-  version "0.0.1"
-  resolved "https://registry.yarnpkg.com/isarray/-/isarray-0.0.1.tgz#8a18acfca9a8f4177e09abfc6038939b05d1eedf"
-
-just-extend@^1.1.27:
-  version "1.1.27"
-  resolved "https://registry.yarnpkg.com/just-extend/-/just-extend-1.1.27.tgz#ec6e79410ff914e472652abfa0e603c03d60e905"
-
-lodash.get@^4.4.2:
-  version "4.4.2"
-  resolved "https://registry.yarnpkg.com/lodash.get/-/lodash.get-4.4.2.tgz#2d177f652fa31e939b4438d5341499dfa3825e99"
-
-lolex@^2.3.2, lolex@^2.7.1:
-  version "2.7.1"
-  resolved "https://registry.yarnpkg.com/lolex/-/lolex-2.7.1.tgz#e40a8c4d1f14b536aa03e42a537c7adbaf0c20be"
-
-media-typer@0.3.0:
-  version "0.3.0"
-  resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748"
-
-merge-descriptors@1.0.1:
-  version "1.0.1"
-  resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61"
-
-methods@~1.1.2:
-  version "1.1.2"
-  resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee"
-
-mime-db@~1.35.0:
-  version "1.35.0"
-  resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.35.0.tgz#0569d657466491283709663ad379a99b90d9ab47"
-
-mime-types@~2.1.18:
-  version "2.1.19"
-  resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.19.tgz#71e464537a7ef81c15f2db9d97e913fc0ff606f0"
-  dependencies:
-    mime-db "~1.35.0"
-
-mime@1.4.1:
-  version "1.4.1"
-  resolved "https://registry.yarnpkg.com/mime/-/mime-1.4.1.tgz#121f9ebc49e3766f311a76e1fa1c8003c4b03aa6"
-
-minimatch@3.0.4, minimatch@^3.0.4:
-  version "3.0.4"
-  resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083"
-  dependencies:
-    brace-expansion "^1.1.7"
-
-minimist@0.0.8:
-  version "0.0.8"
-  resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d"
-
-minimist@~0.0.1:
-  version "0.0.10"
-  resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.10.tgz#de3f98543dbf96082be48ad1a0c7cda836301dcf"
-
-mkdirp@0.5.1:
-  version "0.5.1"
-  resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903"
-  dependencies:
-    minimist "0.0.8"
-
-mocha@*:
-  version "5.2.0"
-  resolved "https://registry.yarnpkg.com/mocha/-/mocha-5.2.0.tgz#6d8ae508f59167f940f2b5b3c4a612ae50c90ae6"
-  dependencies:
-    browser-stdout "1.3.1"
-    commander "2.15.1"
-    debug "3.1.0"
-    diff "3.5.0"
-    escape-string-regexp "1.0.5"
-    glob "7.1.2"
-    growl "1.10.5"
-    he "1.1.1"
-    minimatch "3.0.4"
-    mkdirp "0.5.1"
-    supports-color "5.4.0"
-
-ms@2.0.0:
-  version "2.0.0"
-  resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"
-
-negotiator@0.6.1:
-  version "0.6.1"
-  resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.1.tgz#2b327184e8992101177b28563fb5e7102acd0ca9"
-
-nise@^1.4.2:
-  version "1.4.2"
-  resolved "https://registry.yarnpkg.com/nise/-/nise-1.4.2.tgz#a9a3800e3994994af9e452333d549d60f72b8e8c"
-  dependencies:
-    "@sinonjs/formatio" "^2.0.0"
-    just-extend "^1.1.27"
-    lolex "^2.3.2"
-    path-to-regexp "^1.7.0"
-    text-encoding "^0.6.4"
-
-object-assign@^4:
-  version "4.1.1"
-  resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863"
-
-on-finished@~2.3.0:
-  version "2.3.0"
-  resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947"
-  dependencies:
-    ee-first "1.1.1"
-
-once@^1.3.0:
-  version "1.4.0"
-  resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1"
-  dependencies:
-    wrappy "1"
-
-optimist@~0.6.1:
-  version "0.6.1"
-  resolved "https://registry.yarnpkg.com/optimist/-/optimist-0.6.1.tgz#da3ea74686fa21a19a111c326e90eb15a0196686"
-  dependencies:
-    minimist "~0.0.1"
-    wordwrap "~0.0.2"
-
-parseurl@~1.3.2:
-  version "1.3.2"
-  resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.2.tgz#fc289d4ed8993119460c156253262cdc8de65bf3"
-
-path-is-absolute@^1.0.0:
-  version "1.0.1"
-  resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f"
-
-path-to-regexp@0.1.7:
-  version "0.1.7"
-  resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c"
-
-path-to-regexp@^1.7.0:
-  version "1.7.0"
-  resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-1.7.0.tgz#59fde0f435badacba103a84e9d3bc64e96b9937d"
-  dependencies:
-    isarray "0.0.1"
-
-proxy-addr@~2.0.3:
-  version "2.0.4"
-  resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.4.tgz#ecfc733bf22ff8c6f407fa275327b9ab67e48b93"
-  dependencies:
-    forwarded "~0.1.2"
-    ipaddr.js "1.8.0"
-
-qs@6.5.1:
-  version "6.5.1"
-  resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.1.tgz#349cdf6eef89ec45c12d7d5eb3fc0c870343a6d8"
-
-qs@6.5.2:
-  version "6.5.2"
-  resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36"
-
-range-parser@~1.2.0:
-  version "1.2.0"
-  resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.0.tgz#f49be6b487894ddc40dcc94a322f611092e00d5e"
-
-raw-body@2.3.2:
-  version "2.3.2"
-  resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.3.2.tgz#bcd60c77d3eb93cde0050295c3f379389bc88f89"
-  dependencies:
-    bytes "3.0.0"
-    http-errors "1.6.2"
-    iconv-lite "0.4.19"
-    unpipe "1.0.0"
-
-raw-body@2.3.3:
-  version "2.3.3"
-  resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.3.3.tgz#1b324ece6b5706e153855bc1148c65bb7f6ea0c3"
-  dependencies:
-    bytes "3.0.0"
-    http-errors "1.6.3"
-    iconv-lite "0.4.23"
-    unpipe "1.0.0"
-
-safe-buffer@5.1.1:
-  version "5.1.1"
-  resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.1.tgz#893312af69b2123def71f57889001671eeb2c853"
-
-"safer-buffer@>= 2.1.2 < 3":
-  version "2.1.2"
-  resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a"
-
-samsam@1.3.0:
-  version "1.3.0"
-  resolved "https://registry.yarnpkg.com/samsam/-/samsam-1.3.0.tgz#8d1d9350e25622da30de3e44ba692b5221ab7c50"
-
-send@0.16.2:
-  version "0.16.2"
-  resolved "https://registry.yarnpkg.com/send/-/send-0.16.2.tgz#6ecca1e0f8c156d141597559848df64730a6bbc1"
-  dependencies:
-    debug "2.6.9"
-    depd "~1.1.2"
-    destroy "~1.0.4"
-    encodeurl "~1.0.2"
-    escape-html "~1.0.3"
-    etag "~1.8.1"
-    fresh "0.5.2"
-    http-errors "~1.6.2"
-    mime "1.4.1"
-    ms "2.0.0"
-    on-finished "~2.3.0"
-    range-parser "~1.2.0"
-    statuses "~1.4.0"
-
-serve-static@1.13.2:
-  version "1.13.2"
-  resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.13.2.tgz#095e8472fd5b46237db50ce486a43f4b86c6cec1"
-  dependencies:
-    encodeurl "~1.0.2"
-    escape-html "~1.0.3"
-    parseurl "~1.3.2"
-    send "0.16.2"
-
-setprototypeof@1.0.3:
-  version "1.0.3"
-  resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.0.3.tgz#66567e37043eeb4f04d91bd658c0cbefb55b8e04"
-
-setprototypeof@1.1.0:
-  version "1.1.0"
-  resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.0.tgz#d0bd85536887b6fe7c0d818cb962d9d91c54e656"
-
-sinon@*:
-  version "6.1.5"
-  resolved "https://registry.yarnpkg.com/sinon/-/sinon-6.1.5.tgz#41451502d43cd5ffb9d051fbf507952400e81d09"
-  dependencies:
-    "@sinonjs/commons" "^1.0.1"
-    "@sinonjs/formatio" "^2.0.0"
-    "@sinonjs/samsam" "^2.0.0"
-    diff "^3.5.0"
-    lodash.get "^4.4.2"
-    lolex "^2.7.1"
-    nise "^1.4.2"
-    supports-color "^5.4.0"
-    type-detect "^4.0.8"
-
-"statuses@>= 1.3.1 < 2", "statuses@>= 1.4.0 < 2":
-  version "1.5.0"
-  resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c"
-
-statuses@~1.4.0:
-  version "1.4.0"
-  resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.4.0.tgz#bb73d446da2796106efcc1b601a253d6c46bd087"
-
-supports-color@5.4.0, supports-color@^5.4.0:
-  version "5.4.0"
-  resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.4.0.tgz#1c6b337402c2137605efe19f10fec390f6faab54"
-  dependencies:
-    has-flag "^3.0.0"
-
-text-encoding@^0.6.4:
-  version "0.6.4"
-  resolved "https://registry.yarnpkg.com/text-encoding/-/text-encoding-0.6.4.tgz#e399a982257a276dae428bb92845cb71bdc26d19"
-
-type-detect@4.0.8, type-detect@^4.0.8:
-  version "4.0.8"
-  resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-4.0.8.tgz#7646fb5f18871cfbb7749e69bd39a6388eb7450c"
-
-type-is@~1.6.15, type-is@~1.6.16:
-  version "1.6.16"
-  resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.16.tgz#f89ce341541c672b25ee7ae3c73dee3b2be50194"
-  dependencies:
-    media-typer "0.3.0"
-    mime-types "~2.1.18"
-
-unpipe@1.0.0, unpipe@~1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec"
-
-utils-merge@1.0.1:
-  version "1.0.1"
-  resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713"
-
-vary@^1, vary@~1.1.2:
-  version "1.1.2"
-  resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc"
-
-wordwrap@~0.0.2:
-  version "0.0.3"
-  resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-0.0.3.tgz#a3d5da6cd5c0bc0008d37234bbaf1bed63059107"
-
-wrappy@1:
-  version "1.0.2"
-  resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
-
-ws@6.0.0:
-  version "6.0.0"
-  resolved "https://registry.yarnpkg.com/ws/-/ws-6.0.0.tgz#eaa494aded00ac4289d455bac8d84c7c651cef35"
-  dependencies:
-    async-limiter "~1.0.0"