This commit is contained in:
afrokick 2019-04-01 13:59:21 +03:00
parent 660b2f7fbb
commit 1e09fcfb64
24 changed files with 762 additions and 1302 deletions

View File

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

View File

@ -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" ]

View File

@ -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
config/index.js Normal file
View File

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

74
config/schema.js Normal file
View File

@ -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: ''
}
}
});

View File

@ -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
};

View File

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

View File

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

View File

@ -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",
"dependencies": {
"body-parser": "^1.18.3",
"express": "^4.16.3",
"optimist": "~0.6.1",
"ws": "6.0.0",
"cors": "~2.8.4"
},
"devDependencies": {
"expect.js": "*",
"sinon": "*",
"mocha": "*"
},
"engines": {
"node": ">=0.8"
},
"scripts": {
"test": "mocha test",
"start": "bin/peerjs --port ${PORT:=9000}"
"start": "node ./src/index.js"
},
"dependencies": {
"body-parser": "^1.18.3",
"convict": "^4.4.1",
"cors": "~2.8.4",
"express": "^4.16.3",
"log4js": "^4.1.0",
"ws": "6.0.0"
},
"devDependencies": {
"mocha": "^6.0.2",
"chai": "^4.2.0",
"semistandard": "^13.0.1",
"sinon": "^7.3.1"
},
"engines": {
"node": "^10"
}
}

18
src/api/index.js Normal file
View File

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

View File

@ -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
src/api/v1/calls/index.js Normal file
View File

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

View File

@ -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
src/enums.js Normal file
View File

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

124
src/index.js Normal file
View File

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

View File

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

View File

@ -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
src/models/client.js Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

581
yarn.lock
View File

@ -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"