diff --git a/README.md b/README.md index 5b46d5d..9c829f0 100644 --- a/README.md +++ b/README.md @@ -43,6 +43,16 @@ You will be assigned a URL similar to `heavy-puma-9.sub.example.com:1234`. If your server is acting as a reverse proxy (i.e. nginx) and is able to listen on port 80, then you do not need the `:1234` part of the hostname for the `lt` client. +## REST API + +### POST /api/tunnels + +Create a new tunnel. A LocalTunnel client posts to this enpoint to request a new tunnel with a specific name or a randomly assigned name. + +### GET /api/status + +General server information. + ## Deploy You can deploy your own localtunnel server using the prebuilt docker image. diff --git a/lib/BindingAgent.js b/lib/BindingAgent.js deleted file mode 100644 index c0d7ad9..0000000 --- a/lib/BindingAgent.js +++ /dev/null @@ -1,23 +0,0 @@ -import http from 'http'; -import util from 'util'; -import assert from 'assert'; - -// binding agent will return a given options.socket as the socket for the agent -// this is useful if you already have a socket established and want the request -// to use that socket instead of making a new one -function BindingAgent(options) { - options = options || {}; - http.Agent.call(this, options); - - this.socket = options.socket; - assert(this.socket, 'socket is required for BindingAgent'); - this.createConnection = create_connection; -} - -util.inherits(BindingAgent, http.Agent); - -function create_connection(port, host, options) { - return this.socket; -} - -export default BindingAgent; diff --git a/lib/Client.js b/lib/Client.js new file mode 100644 index 0000000..aae3df0 --- /dev/null +++ b/lib/Client.js @@ -0,0 +1,69 @@ +import http from 'http'; + +import TunnelAgent from './TunnelAgent'; + +// A client encapsulates req/res handling using an agent +// +// If an agent is destroyed, the request handling will error +// The caller is responsible for handling a failed request +class Client { + constructor(options) { + this.agent = options.agent; + } + + handleRequest(req, res) { + const opt = { + path: req.url, + agent: this.agent, + method: req.method, + headers: req.headers + }; + + const clientReq = http.request(opt, (clientRes) => { + // write response code and headers + res.writeHead(clientRes.statusCode, clientRes.headers); + clientRes.pipe(res); + }); + + // this can happen when underlying agent produces an error + // in our case we 504 gateway error this? + // if we have already sent headers? + clientReq.once('error', (err) => { + + }); + + req.pipe(clientReq); + } + + handleUpgrade(req, socket) { + this.agent.createConnection({}, (err, conn) => { + // any errors getting a connection mean we cannot service this request + if (err) { + socket.end(); + return; + } + + // socket met have disconnected while we waiting for a socket + if (!socket.readable || !socket.writable) { + socket.end(); + return; + } + + // websocket requests are special in that we simply re-create the header info + // then directly pipe the socket data + // avoids having to rebuild the request and handle upgrades via the http client + const arr = [`${req.method} ${req.url} HTTP/${req.httpVersion}`]; + for (let i=0 ; i < (req.rawHeaders.length-1) ; i+=2) { + arr.push(`${req.rawHeaders[i]}: ${req.rawHeaders[i+1]}`); + } + + arr.push(''); + arr.push(''); + + conn.pipe(socket).pipe(conn); + conn.write(arr.join('\r\n')); + }); + } +} + +export default Client; \ No newline at end of file diff --git a/lib/Client.test.js b/lib/Client.test.js new file mode 100644 index 0000000..8f53be1 --- /dev/null +++ b/lib/Client.test.js @@ -0,0 +1,158 @@ +import assert from 'assert'; +import http from 'http'; +import { Duplex } from 'stream'; +import EventEmitter from 'events'; +import WebSocket from 'ws'; +import net from 'net'; + +import Client from './Client'; + +class DummySocket extends Duplex { + constructor(options) { + super(options); + } + + _write(chunk, encoding, callback) { + callback(); + } + + _read(size) { + this.push('HTTP/1.1 304 Not Modified\r\nX-Powered-By: dummy\r\n\r\n\r\n'); + this.push(null); + } +} + +class DummyWebsocket extends Duplex { + constructor(options) { + super(options); + this.sentHeader = false; + } + + _write(chunk, encoding, callback) { + const str = chunk.toString(); + // if chunk contains `GET / HTTP/1.1` -> queue headers + // otherwise echo back received data + if (str.indexOf('GET / HTTP/1.1') === 0) { + const arr = [ + 'HTTP/1.1 101 Switching Protocols', + 'Upgrade: websocket', + 'Connection: Upgrade', + ]; + this.push(arr.join('\r\n')); + this.push('\r\n\r\n'); + } + else { + this.push(str); + } + callback(); + } + + _read(size) { + // nothing to implement + } +} + +class DummyAgent extends http.Agent { + constructor() { + super(); + } + + createConnection(options, cb) { + cb(null, new DummySocket()); + } +} + +describe('Client', () => { + it('should handle request', async () => { + const agent = new DummyAgent(); + const client = new Client({ agent }); + + const server = http.createServer((req, res) => { + client.handleRequest(req, res); + }); + + await new Promise(resolve => server.listen(resolve)); + + const address = server.address(); + const opt = { + host: 'localhost', + port: address.port, + path: '/', + }; + + const res = await new Promise((resolve) => { + const req = http.get(opt, (res) => { + resolve(res); + }); + req.end(); + }); + assert.equal(res.headers['x-powered-by'], 'dummy'); + server.close(); + }); + + it('should handle upgrade', async () => { + // need a websocket server and a socket for it + class DummyWebsocketAgent extends http.Agent { + constructor() { + super(); + } + + createConnection(options, cb) { + cb(null, new DummyWebsocket()); + } + } + + const agent = new DummyWebsocketAgent(); + const client = new Client({ agent }); + + const server = http.createServer(); + server.on('upgrade', (req, socket, head) => { + client.handleUpgrade(req, socket); + }); + + await new Promise(resolve => server.listen(resolve)); + + const address = server.address(); + + const netClient = await new Promise((resolve) => { + const newClient = net.createConnection({ port: address.port }, () => { + resolve(newClient); + }); + }); + + const out = [ + 'GET / HTTP/1.1', + 'Connection: Upgrade', + 'Upgrade: websocket' + ]; + + netClient.write(out.join('\r\n') + '\r\n\r\n'); + + { + const data = await new Promise((resolve) => { + netClient.once('data', (chunk) => { + resolve(chunk.toString()); + }); + }); + const exp = [ + 'HTTP/1.1 101 Switching Protocols', + 'Upgrade: websocket', + 'Connection: Upgrade', + ]; + assert.equal(exp.join('\r\n') + '\r\n\r\n', data); + } + + { + netClient.write('foobar'); + const data = await new Promise((resolve) => { + netClient.once('data', (chunk) => { + resolve(chunk.toString()); + }); + }); + assert.equal('foobar', data); + } + + netClient.destroy(); + server.close(); + }); +}); diff --git a/lib/ClientManager.js b/lib/ClientManager.js index a4199f9..b41270f 100644 --- a/lib/ClientManager.js +++ b/lib/ClientManager.js @@ -1,36 +1,31 @@ -import Proxy from './Proxy'; +import { hri } from 'human-readable-ids'; +import Debug from 'debug'; -// maybe remove? -import on_finished from 'on-finished'; -import http from 'http'; -import pump from 'pump'; -import { hri } from "human-readable-ids"; - -import BindingAgent from './BindingAgent'; - -const NoOp = () => {}; +import Client from './Client'; +import TunnelAgent from './TunnelAgent'; // Manage sets of clients // // A client is a "user session" established to service a remote localtunnel client class ClientManager { constructor(opt) { - this.opt = opt; - - this.reqId = 0; + this.opt = opt || {}; // id -> client instance - this.clients = Object.create(null); + this.clients = new Map(); // statistics this.stats = { tunnels: 0 }; + + this.debug = Debug('lt:ClientManager'); } // create a new tunnel with `id` // if the id is already used, a random id is assigned - async newClient (id) { + // if the tunnel could not be created, throws an error + async newClient(id) { const clients = this.clients; const stats = this.stats; @@ -39,161 +34,68 @@ class ClientManager { id = hri.random(); } - const popt = { - id: id, - max_tcp_sockets: this.opt.max_tcp_sockets - }; + const maxSockets = this.opt.max_tcp_sockets; + const agent = new TunnelAgent({ + maxSockets: 10, + }); - const client = Proxy(popt); + agent.on('online', () => { + this.debug('client online %s', id); + }); + + agent.on('offline', () => { + // TODO(roman): grace period for re-connecting + // this period is short as the client is expected to maintain connections actively + // if they client does not reconnect on a dropped connection they need to re-establish + this.debug('client offline %s', id); + this.removeClient(id); + }); + + // TODO(roman): an agent error removes the client, the user needs to re-connect? + // how does a user realize they need to re-connect vs some random client being assigned same port? + agent.once('error', (err) => { + this.removeClient(id); + }); + + const client = new Client({ agent }); // add to clients map immediately // avoiding races with other clients requesting same id clients[id] = client; - client.on('end', () => { - --stats.tunnels; - delete clients[id]; - }); + // try/catch used here to remove client id + try { + const info = await agent.listen(); + ++stats.tunnels; + return { + id: id, + port: info.port, + max_conn_count: maxSockets, + }; + } + catch (err) { + this.removeClient(id); + // rethrow error for upstream to handle + throw err; + } + } - return new Promise((resolve, reject) => { - // each local client has a tcp server to link with the remove localtunnel client - // this starts the server and waits until it is listening - client.start((err, info) => { - if (err) { - // clear the reserved client id - delete clients[id]; - reject(err); - return; - } - - ++stats.tunnels; - info.id = id; - resolve(info); - }); - }); + removeClient(id) { + const client = this.clients[id]; + if (!client) { + return; + } + --this.stats.tunnels; + delete this.clients[id]; + client.agent.destroy(); } hasClient(id) { return this.clients[id]; } - // handle http request - handleRequest(clientId, req, res) { - const client = this.clients[clientId]; - if (!client) { - return; - } - - const reqId = this.reqId; - this.reqId = this.reqId + 1; - - let endRes = () => { - endRes = NoOp; - res.end(); - }; - - on_finished(res, () => { - endRes = NoOp; - }); - - client.nextSocket((clientSocket) => { - // response ended before we even got a socket to respond on - if (endRes === NoOp) { - return; - } - - // happens when client upstream is disconnected (or disconnects) - // and the proxy iterates the waiting list and clears the callbacks - // we gracefully inform the user and kill their conn - // without this, the browser will leave some connections open - // and try to use them again for new requests - // TODO(roman) we could instead have a timeout above - // if no socket becomes available within some time, - // we just tell the user no resource available to service request - if (!clientSocket) { - endRes(); - return; - } - - const agent = new BindingAgent({ - socket: clientSocket, - }); - - const opt = { - path: req.url, - agent: agent, - method: req.method, - headers: req.headers - }; - - return new Promise((resolve) => { - // what if error making this request? - const clientReq = http.request(opt, (clientRes) => { - // write response code and headers - res.writeHead(clientRes.statusCode, clientRes.headers); - - // when this pump is done, we end our response - pump(clientRes, res, (err) => { - endRes(); - resolve(); - }); - }); - - // we don't care about when this ends, only if there is error - pump(req, clientReq, (err) => { - if (err) { - endRes(); - resolve(); - } - }); - }); - }); - } - - // handle http upgrade - handleUpgrade(clientId, req, sock) { - const client = this.clients[clientId]; - if (!client) { - return; - } - - client.nextSocket(async (clientSocket) => { - if (!sock.readable || !sock.writable) { - sock.end(); - return; - } - - // happens when client upstream is disconnected (or disconnects) - // and the proxy iterates the waiting list and clears the callbacks - // we gracefully inform the user and kill their conn - // without this, the browser will leave some connections open - // and try to use them again for new requests - // TODO(roman) we could instead have a timeout above - // if no socket becomes available within some time, - // we just tell the user no resource available to service request - if (!clientSocket) { - sock.end(); - return; - } - - // websocket requests are special in that we simply re-create the header info - // then directly pipe the socket data - // avoids having to rebuild the request and handle upgrades via the http client - const arr = [`${req.method} ${req.url} HTTP/${req.httpVersion}`]; - for (let i=0 ; i < (req.rawHeaders.length-1) ; i+=2) { - arr.push(`${req.rawHeaders[i]}: ${req.rawHeaders[i+1]}`); - } - - arr.push(''); - arr.push(''); - - clientSocket.pipe(sock).pipe(clientSocket); - clientSocket.write(arr.join('\r\n')); - - await new Promise((resolve) => { - sock.once('end', resolve); - }); - }); + getClient(id) { + return this.clients[id]; } } diff --git a/lib/ClientManager.test.js b/lib/ClientManager.test.js new file mode 100644 index 0000000..63eb60e --- /dev/null +++ b/lib/ClientManager.test.js @@ -0,0 +1,51 @@ +import assert from 'assert'; +import net from 'net'; + +import ClientManager from './ClientManager'; + +describe('ClientManager', () => { + it('should construct with no tunnels', () => { + const manager = new ClientManager(); + assert.equal(manager.stats.tunnels, 0); + }); + + it('should create a new client with random id', async () => { + const manager = new ClientManager(); + const client = await manager.newClient(); + assert(manager.hasClient(client.id)); + manager.removeClient(client.id); + }); + + it('should create a new client with id', async () => { + const manager = new ClientManager(); + const client = await manager.newClient('foobar'); + assert(manager.hasClient('foobar')); + manager.removeClient('foobar'); + }); + + it('should create a new client with random id if previous exists', async () => { + const manager = new ClientManager(); + const clientA = await manager.newClient('foobar'); + const clientB = await manager.newClient('foobar'); + assert(clientA.id, 'foobar'); + assert(manager.hasClient(clientB.id)); + assert(clientB.id != clientA.id); + manager.removeClient(clientB.id); + manager.removeClient('foobar'); + }); + + it('should remove client once it goes offline', async () => { + const manager = new ClientManager(); + const client = await manager.newClient('foobar'); + + const socket = await new Promise((resolve) => { + const netClient = net.createConnection({ port: client.port }, () => { + resolve(netClient); + }); + }); + const closePromise = new Promise(resolve => socket.once('close', resolve)); + socket.end(); + await closePromise; + assert(!manager.hasClient('foobar')); + }); +}); diff --git a/lib/Proxy.js b/lib/Proxy.js deleted file mode 100644 index ccd0ebf..0000000 --- a/lib/Proxy.js +++ /dev/null @@ -1,193 +0,0 @@ -import net from 'net'; -import EventEmitter from 'events'; -import log from 'book'; -import Debug from 'debug'; - -const debug = Debug('localtunnel:server'); - -const Proxy = function(opt) { - if (!(this instanceof Proxy)) { - return new Proxy(opt); - } - - const self = this; - - self.sockets = []; - self.waiting = []; - self.id = opt.id; - - self.activeSockets = 0; - - // default max is 10 - self.max_tcp_sockets = opt.max_tcp_sockets || 10; - - // new tcp server to service requests for this client - self.server = net.createServer(); - - // track initial user connection setup - self.conn_timeout = undefined; - - self.debug = Debug(`localtunnel:server:${self.id}`); -}; - -Proxy.prototype.__proto__ = EventEmitter.prototype; - -Proxy.prototype.start = function(cb) { - const self = this; - const server = self.server; - - if (self.started) { - cb(new Error('already started')); - return; - } - self.started = true; - - server.on('close', self._cleanup.bind(self)); - server.on('connection', self._handle_socket.bind(self)); - - server.on('error', function(err) { - // where do these errors come from? - // other side creates a connection and then is killed? - if (err.code == 'ECONNRESET' || err.code == 'ETIMEDOUT') { - return; - } - - log.error(err); - }); - - server.listen(function() { - const port = server.address().port; - self.debug('tcp server listening on port: %d', port); - - cb(null, { - // port for lt client tcp connections - port: port, - // maximum number of tcp connections allowed by lt client - max_conn_count: self.max_tcp_sockets - }); - }); - - self._maybe_destroy(); -}; - -Proxy.prototype._maybe_destroy = function() { - const self = this; - - clearTimeout(self.conn_timeout); - - // After last socket is gone, we give opportunity to connect again quickly - self.conn_timeout = setTimeout(function() { - // sometimes the server is already closed but the event has not fired? - try { - clearTimeout(self.conn_timeout); - self.server.close(); - } - catch (err) { - self._cleanup(); - } - }, 1000); -} - -// new socket connection from client for tunneling requests to client -Proxy.prototype._handle_socket = function(socket) { - const self = this; - - // no more socket connections allowed - if (self.activeSockets >= self.max_tcp_sockets) { - return socket.end(); - } - - self.activeSockets = self.activeSockets + 1; - - self.debug('new connection from: %s:%s', socket.address().address, socket.address().port); - - // a single connection is enough to keep client id slot open - clearTimeout(self.conn_timeout); - - socket.once('close', function(had_error) { - self.activeSockets = self.activeSockets - 1; - self.debug('closed socket (error: %s)', had_error); - - // what if socket was servicing a request at this time? - // then it will be put back in available after right? - // we need a list of sockets servicing requests? - - // remove this socket - const idx = self.sockets.indexOf(socket); - if (idx >= 0) { - self.sockets.splice(idx, 1); - } - - // need to track total sockets, not just active available - self.debug('remaining client sockets: %s', self.sockets.length); - - // no more sockets for this ident - if (self.sockets.length === 0) { - self.debug('all sockets disconnected'); - self._maybe_destroy(); - } - }); - - // close will be emitted after this - socket.on('error', function(err) { - // we don't log here to avoid logging crap for misbehaving clients - socket.destroy(); - }); - - self.sockets.push(socket); - self._process_waiting(); -}; - -Proxy.prototype._process_waiting = function() { - const self = this; - const fn = self.waiting.shift(); - if (fn) { - self.debug('handling queued request'); - self.nextSocket(fn); - } -}; - -Proxy.prototype._cleanup = function() { - const self = this; - self.debug('closed tcp socket for client(%s)', self.id); - - clearTimeout(self.conn_timeout); - - // clear waiting by ending responses, (requests?) - self.waiting.forEach(handler => handler(null)); - - self.emit('end'); -}; - -Proxy.prototype.nextSocket = async function(fn) { - const self = this; - - // socket is a tcp connection back to the user hosting the site - const sock = self.sockets.shift(); - if (!sock) { - self.debug('no more clients, queue callback'); - self.waiting.push(fn); - return; - } - - self.debug('processing request'); - await fn(sock); - - if (!sock.destroyed) { - self.debug('retuning socket'); - self.sockets.push(sock); - } - - // no sockets left to process waiting requests - if (self.sockets.length === 0) { - return; - } - - self._process_waiting(); -}; - -Proxy.prototype._done = function() { - const self = this; -}; - -export default Proxy; diff --git a/lib/TunnelAgent.js b/lib/TunnelAgent.js new file mode 100644 index 0000000..215b1f8 --- /dev/null +++ b/lib/TunnelAgent.js @@ -0,0 +1,171 @@ +import { Agent } from 'http'; +import net from 'net'; +import assert from 'assert'; +import log from 'book'; +import Debug from 'debug'; + +const DEFAULT_MAX_SOCKETS = 10; + +// Implements an http.Agent interface to a pool of tunnel sockets +// A tunnel socket is a connection _from_ a client that will +// service http requests. This agent is usable wherever one can use an http.Agent +class TunnelAgent extends Agent { + constructor(options = {}) { + super({ + keepAlive: true, + // only allow keepalive to hold on to one socket + // this prevents it from holding on to all the sockets so they can be used for upgrades + maxFreeSockets: 1, + }); + + // sockets we can hand out via createConnection + this.availableSockets = []; + + // when a createConnection cannot return a socket, it goes into a queue + // once a socket is available it is handed out to the next callback + this.waitingCreateConn = []; + + this.debug = Debug('lt:TunnelAgent'); + + // track maximum allowed sockets + this.activeSockets = 0; + this.maxTcpSockets = options.maxTcpSockets || DEFAULT_MAX_SOCKETS; + + // new tcp server to service requests for this client + this.server = net.createServer(); + + // flag to avoid double starts + this.started = false; + } + + listen() { + const server = this.server; + if (this.started) { + throw new Error('already started'); + } + this.started = true; + + server.on('close', this._onClose.bind(this)); + server.on('connection', this._onConnection.bind(this)); + server.on('error', (err) => { + // where do these errors come from? + // other side creates a connection and then is killed? + if (err.code == 'ECONNRESET' || err.code == 'ETIMEDOUT') { + return; + } + log.error(err); + }); + + return new Promise((resolve) => { + server.listen(() => { + const port = server.address().port; + this.debug('tcp server listening on port: %d', port); + + resolve({ + // port for lt client tcp connections + port: port, + }); + }); + }); + } + + _onClose() { + this.debug('closed tcp socket'); + clearTimeout(this.connTimeout); + // we will not invoke these callbacks? + // TODO(roman): we could invoke these with errors...? + // this makes downstream have to handle this + this.waitingCreateConn = []; + this.emit('end'); + } + + // new socket connection from client for tunneling requests to client + _onConnection(socket) { + // no more socket connections allowed + if (this.activeSockets >= this.maxTcpSockets) { + this.debug('no more sockets allowed'); + socket.destroy(); + return false; + } + + // a new socket becomes available + if (this.activeSockets == 0) { + this.emit('online'); + } + + this.activeSockets += 1; + this.debug('new connection from: %s:%s', socket.address().address, socket.address().port); + + // a single connection is enough to keep client id slot open + clearTimeout(this.connTimeout); + + socket.once('close', (had_error) => { + this.debug('closed socket (error: %s)', had_error); + this.debug('removing socket'); + this.activeSockets -= 1; + // remove the socket from available list + const idx = this.availableSockets.indexOf(socket); + if (idx >= 0) { + this.availableSockets.splice(idx, 1); + } + // need to track total sockets, not just active available + this.debug('remaining client sockets: %s', this.availableSockets.length); + // no more sockets for this session + // the session will become inactive if client does not reconnect + if (this.availableSockets.length <= 0) { + this.debug('all sockets disconnected'); + this.emit('offline'); + } + }); + + // close will be emitted after this + socket.once('error', (err) => { + // we do not log these errors, sessions can drop from clients for many reasons + // these are not actionable errors for our server + socket.destroy(); + }); + + // make socket available for those waiting on sockets + this.availableSockets.push(socket); + + // flush anyone waiting on sockets + this._callWaitingCreateConn(); + } + + // invoke when a new socket is available and there may be waiting createConnection calls + _callWaitingCreateConn() { + const fn = this.waitingCreateConn.shift(); + if (!fn) { + return; + } + + this.debug('handling queued request'); + this.createConnection({}, fn); + } + + // fetch a socket from the available socket pool for the agent + // if no socket is available, queue + // cb(err, socket) + createConnection(options, cb) { + // socket is a tcp connection back to the user hosting the site + const sock = this.availableSockets.shift(); + + // no available sockets + // wait until we have one + if (!sock) { + this.waitingCreateConn.push(cb); + this.debug('waiting'); + return; + } + + this.debug('socket given'); + cb(null, sock); + } + + destroy() { + this.server.close(); + super.destroy(); + } +} + +export default TunnelAgent; diff --git a/lib/TunnelAgent.test.js b/lib/TunnelAgent.test.js new file mode 100644 index 0000000..62839c0 --- /dev/null +++ b/lib/TunnelAgent.test.js @@ -0,0 +1,176 @@ +import http from 'http'; +import net from 'net'; +import assert from 'assert'; + +import TunnelAgent from './TunnelAgent'; + +describe('TunnelAgent', () => { + it('should create an empty agent', async () => { + const agent = new TunnelAgent(); + assert.equal(agent.started, false); + + const info = await agent.listen(); + assert.ok(info.port > 0); + agent.destroy(); + }); + + it('should create a new server and accept connections', async () => { + const agent = new TunnelAgent(); + assert.equal(agent.started, false); + + const info = await agent.listen(); + const sock = net.createConnection({ port: info.port }); + + // in this test we wait for the socket to be connected + await new Promise(resolve => sock.once('connect', resolve)); + + const agentSock = await new Promise((resolve, reject) => { + agent.createConnection({}, (err, sock) => { + if (err) { + reject(err); + } + resolve(sock); + }); + }); + + agentSock.write('foo'); + await new Promise(resolve => sock.once('readable', resolve)); + + assert.equal('foo', sock.read().toString()); + agent.destroy(); + sock.destroy(); + }); + + it('should reject connections over the max', async () => { + const agent = new TunnelAgent({ + maxTcpSockets: 2, + }); + assert.equal(agent.started, false); + + const info = await agent.listen(); + const sock1 = net.createConnection({ port: info.port }); + const sock2 = net.createConnection({ port: info.port }); + + // two valid socket connections + const p1 = new Promise(resolve => sock1.once('connect', resolve)); + const p2 = new Promise(resolve => sock2.once('connect', resolve)); + await Promise.all([p1, p2]); + + const sock3 = net.createConnection({ port: info.port }); + const p3 = await new Promise(resolve => sock3.once('close', resolve)); + + agent.destroy(); + sock1.destroy(); + sock2.destroy(); + sock3.destroy(); + }); + + it('should queue createConnection requests', async () => { + const agent = new TunnelAgent(); + assert.equal(agent.started, false); + + const info = await agent.listen(); + + // create a promise for the next connection + let fulfilled = false; + const waitSockPromise = new Promise((resolve, reject) => { + agent.createConnection({}, (err, sock) => { + fulfilled = true; + if (err) { + reject(err); + } + resolve(sock); + }); + }); + + // check that the next socket is not yet available + await new Promise(resolve => setTimeout(resolve, 500)); + assert(!fulfilled); + + // connect, this will make a socket available + const sock = net.createConnection({ port: info.port }); + await new Promise(resolve => sock.once('connect', resolve)); + + const anotherAgentSock = await waitSockPromise; + agent.destroy(); + sock.destroy(); + }); + + it('should should emit a online event when a socket connects', async () => { + const agent = new TunnelAgent(); + const info = await agent.listen(); + + const onlinePromise = new Promise(resolve => agent.once('online', resolve)); + + const sock = net.createConnection({ port: info.port }); + await new Promise(resolve => sock.once('connect', resolve)); + + await onlinePromise; + agent.destroy(); + sock.destroy(); + }); + + it('should emit offline event when socket disconnects', async () => { + const agent = new TunnelAgent(); + const info = await agent.listen(); + + const offlinePromise = new Promise(resolve => agent.once('offline', resolve)); + + const sock = net.createConnection({ port: info.port }); + await new Promise(resolve => sock.once('connect', resolve)); + + sock.end(); + await offlinePromise; + agent.destroy(); + sock.destroy(); + }); + + it('should emit offline event only when last socket disconnects', async () => { + const agent = new TunnelAgent(); + const info = await agent.listen(); + + const offlinePromise = new Promise(resolve => agent.once('offline', resolve)); + + const sockA = net.createConnection({ port: info.port }); + await new Promise(resolve => sockA.once('connect', resolve)); + const sockB = net.createConnection({ port: info.port }); + await new Promise(resolve => sockB.once('connect', resolve)); + + sockA.end(); + + const timeout = new Promise(resolve => setTimeout(resolve, 500)); + await Promise.race([offlinePromise, timeout]); + + sockB.end(); + await offlinePromise; + + agent.destroy(); + }); + + it('should error an http request', async () => { + class ErrorAgent extends http.Agent { + constructor() { + super(); + } + + createConnection(options, cb) { + cb(new Error('foo')); + } + } + + const agent = new ErrorAgent(); + + const opt = { + host: 'localhost', + port: 1234, + path: '/', + agent: agent, + }; + + const err = await new Promise((resolve) => { + const req = http.get(opt, (res) => {}); + req.once('error', resolve); + }); + assert.equal(err.message, 'foo'); + }); +}); diff --git a/package.json b/package.json index aad8c63..1624eef 100644 --- a/package.json +++ b/package.json @@ -11,23 +11,21 @@ "dependencies": { "book": "1.3.3", "debug": "3.1.0", - "esm": "3.0.14", + "esm": "3.0.34", "human-readable-ids": "1.0.3", - "koa": "2.4.1", + "koa": "2.5.1", "localenv": "0.2.2", - "on-finished": "2.3.0", "optimist": "0.6.1", - "pump": "2.0.0", "tldjs": "2.3.1" }, "devDependencies": { - "localtunnel": "1.8.0", - "mocha": "2.5.3", + "mocha": "5.1.1", "node-dev": "3.1.3", - "ws": "0.8.0" + "supertest": "3.1.0", + "ws": "5.1.1" }, "scripts": { - "test": "mocha --ui qunit --reporter spec", + "test": "mocha --check-leaks --require esm './**/*.test.js'", "start": "./bin/server", "dev": "node-dev bin/server --port 3000" } diff --git a/server.js b/server.js index 86b2d57..c5c395a 100644 --- a/server.js +++ b/server.js @@ -14,6 +14,7 @@ export default function(opt) { const validHosts = (opt.domain) ? [opt.domain] : undefined; const myTldjs = tldjs.fromUserSettings({ validHosts }); + const landingPage = opt.landing || 'https://localtunnel.github.io/www/'; function GetClientIdFromHostname(hostname) { return myTldjs.getSubdomain(hostname); @@ -53,9 +54,9 @@ export default function(opt) { const isNewClientRequest = ctx.query['new'] !== undefined; if (isNewClientRequest) { - const req_id = hri.random(); - debug('making new client with id %s', req_id); - const info = await manager.newClient(req_id); + const reqId = hri.random(); + debug('making new client with id %s', reqId); + const info = await manager.newClient(reqId); const url = schema + '://' + info.id + '.' + ctx.request.host; info.url = url; @@ -64,7 +65,7 @@ export default function(opt) { } // no new client request, send to landing page - ctx.redirect('https://localtunnel.github.io/www/'); + ctx.redirect(landingPage); }); // anything after the / path is a request for a specific client name @@ -80,10 +81,10 @@ export default function(opt) { return; } - const req_id = parts[1]; + const reqId = parts[1]; // limit requested hostnames to 63 characters - if (! /^(?:[a-z0-9][a-z0-9\-]{4,63}[a-z0-9]|[a-z0-9]{4,63})$/.test(req_id)) { + if (! /^(?:[a-z0-9][a-z0-9\-]{4,63}[a-z0-9]|[a-z0-9]{4,63})$/.test(reqId)) { const msg = 'Invalid subdomain. Subdomains must be lowercase and between 4 and 63 alphanumeric characters.'; ctx.status = 403; ctx.body = { @@ -92,8 +93,8 @@ export default function(opt) { return; } - debug('making new client with id %s', req_id); - const info = await manager.newClient(req_id); + debug('making new client with id %s', reqId); + const info = await manager.newClient(reqId); const url = schema + '://' + info.id + '.' + ctx.request.host; info.url = url; @@ -104,6 +105,7 @@ export default function(opt) { const server = http.createServer(); const appCallback = app.callback(); + server.on('request', (req, res) => { // without a hostname, we won't know who the request is for const hostname = req.headers.host; @@ -119,13 +121,14 @@ export default function(opt) { return; } - if (manager.hasClient(clientId)) { - manager.handleRequest(clientId, req, res); + const client = manager.getClient(clientId); + if (!client) { + res.statusCode = 404; + res.end('404'); return; } - res.statusCode = 404; - res.end('404'); + client.handleRequest(req, res); }); server.on('upgrade', (req, socket, head) => { @@ -141,12 +144,13 @@ export default function(opt) { return; } - if (manager.hasClient(clientId)) { - manager.handleUpgrade(clientId, req, socket); + const client = manager.getClient(clientId); + if (!client) { + sock.destroy(); return; } - socket.destroy(); + client.handleUpgrade(req, socket); }); return server; diff --git a/server.test.js b/server.test.js new file mode 100644 index 0000000..3f72f2d --- /dev/null +++ b/server.test.js @@ -0,0 +1,85 @@ +import request from 'supertest'; +import assert from 'assert'; +import { Server as WebSocketServer } from 'ws'; +import WebSocket from 'ws'; +import net from 'net'; + +import createServer from './server'; + +describe('Server', () => { + it('server starts and stops', async () => { + const server = createServer(); + await new Promise(resolve => server.listen(resolve)); + await new Promise(resolve => server.close(resolve)); + }); + + it('should redirect root requests to landing page', async () => { + const server = createServer(); + const res = await request(server).get('/'); + assert.equal('https://localtunnel.github.io/www/', res.headers.location); + }); + + it('should support custom base domains', async () => { + const server = createServer({ + domain: 'domain.example.com', + }); + + const res = await request(server).get('/'); + assert.equal('https://localtunnel.github.io/www/', res.headers.location); + }); + + it('reject long domain name requests', async () => { + const server = createServer(); + const res = await request(server).get('/thisdomainisoutsidethesizeofwhatweallowwhichissixtythreecharacters'); + assert.equal(res.body.message, 'Invalid subdomain. Subdomains must be lowercase and between 4 and 63 alphanumeric characters.'); + }); + + it('should upgrade websocket requests', async () => { + const hostname = 'websocket-test'; + const server = createServer({ + domain: 'example.com', + }); + await new Promise(resolve => server.listen(resolve)); + + const res = await request(server).get('/websocket-test'); + const localTunnelPort = res.body.port; + + const wss = await new Promise((resolve) => { + const wsServer = new WebSocketServer({ port: 0 }, () => { + resolve(wsServer); + }); + }); + + const websocketServerPort = wss.address().port; + + const ltSocket = net.createConnection({ port: localTunnelPort }); + const wsSocket = net.createConnection({ port: websocketServerPort }); + ltSocket.pipe(wsSocket).pipe(ltSocket); + + wss.once('connection', (ws) => { + ws.once('message', (message) => { + ws.send(message); + }); + }); + + const ws = new WebSocket('http://localhost:' + server.address().port, { + headers: { + host: hostname + '.example.com', + } + }); + + ws.on('open', () => { + ws.send('something'); + }); + + await new Promise((resolve) => { + ws.once('message', (msg) => { + assert.equal(msg, 'something'); + resolve(); + }); + }); + + wss.close(); + await new Promise(resolve => server.close(resolve)); + }); +}); \ No newline at end of file diff --git a/test/basic.js b/test/basic.js deleted file mode 100644 index c50b7e2..0000000 --- a/test/basic.js +++ /dev/null @@ -1,166 +0,0 @@ -import http from 'http'; -import url from 'url'; -import assert from 'assert'; -import localtunnel from 'localtunnel'; - -import CreateServer from '../server'; - -const localtunnel_server = CreateServer(); - -process.on('uncaughtException', (err) => { - console.error(err); -}); - -process.on('unhandledRejection', (reason, promise) => { - console.error(reason); -}); - -suite('basic'); - -var lt_server_port; - -before('set up localtunnel server', function(done) { - var server = localtunnel_server.listen(function() { - lt_server_port = server.address().port; - done(); - }); -}); - -test('landing page', function(done) { - var opt = { - host: 'localhost', - port: lt_server_port, - headers: { - host: 'example.com' - }, - path: '/' - } - - var req = http.request(opt, function(res) { - res.setEncoding('utf8'); - assert.equal(res.headers.location, 'https://localtunnel.github.io/www/') - done(); - }); - - req.end(); -}); - -before('set up local http server', function(done) { - var server = http.createServer(); - server.on('request', function(req, res) { - res.write('foo'); - res.end(); - }); - server.listen(function() { - var port = server.address().port; - - test._fake_port = port; - done(); - }); -}); - -before('set up localtunnel client', function(done) { - var opt = { - host: 'http://localhost:' + lt_server_port, - }; - - localtunnel(test._fake_port, opt, function(err, tunnel) { - assert.ifError(err); - var url = tunnel.url; - assert.ok(new RegExp('^http:\/\/.*localhost:' + lt_server_port + '$').test(url)); - test._fake_url = url; - done(err); - }); -}); - -test('query localtunnel server w/ ident', function(done) { - var uri = test._fake_url; - var hostname = url.parse(uri).hostname; - - var opt = { - host: 'localhost', - port: lt_server_port, - headers: { - host: hostname + '.tld' - }, - path: '/' - } - - var req = http.request(opt, function(res) { - res.setEncoding('utf8'); - var body = ''; - - res.on('data', function(chunk) { - body += chunk; - }); - - res.on('end', function() { - assert.equal('foo', body); - - // TODO(shtylman) shutdown client - done(); - }); - }); - - req.end(); -}); - -test('request specific domain', function(done) { - var opt = { - host: 'http://localhost:' + lt_server_port, - subdomain: 'abcd' - }; - - localtunnel(test._fake_port, opt, function(err, tunnel) { - assert.ifError(err); - var url = tunnel.url; - assert.ok(new RegExp('^http:\/\/.*localhost:' + lt_server_port + '$').test(url)); - test._fake_url = url; - done(err); - }); -}); - -test('request domain with dash', function(done) { - var opt = { - host: 'http://localhost:' + lt_server_port, - subdomain: 'abcd-1234' - }; - - localtunnel(test._fake_port, opt, function(err, tunnel) { - assert.ifError(err); - var url = tunnel.url; - assert.ok(new RegExp('^http:\/\/.*localhost:' + lt_server_port + '$').test(url)); - test._fake_url = url; - done(err); - }); -}); - -test('request domain that is too long', function(done) { - var opt = { - host: 'http://localhost:' + lt_server_port, - subdomain: 'thisdomainisoutsidethesizeofwhatweallowwhichissixtythreecharacters' - }; - - localtunnel(test._fake_port, opt, function(err, tunnel) { - assert(err); - assert.equal(err.message, 'Invalid subdomain. Subdomains must be lowercase and between 4 and 63 alphanumeric characters.'); - done(); - }); -}); - -test('request uppercase domain', function(done) { - var opt = { - host: 'http://localhost:' + lt_server_port, - subdomain: 'ABCD' - }; - - localtunnel(test._fake_port, opt, function(err, tunnel) { - assert(err); - assert.equal(err.message, 'Invalid subdomain. Subdomains must be lowercase and between 4 and 63 alphanumeric characters.'); - done(); - }); -}); - -after('shutdown', function() { - localtunnel_server.close(); -}); diff --git a/test/domain.js b/test/domain.js deleted file mode 100644 index 6fa2b02..0000000 --- a/test/domain.js +++ /dev/null @@ -1,52 +0,0 @@ -import http from 'http'; -import url from 'url'; -import assert from 'assert'; -import localtunnel from 'localtunnel'; - -import CreateServer from '../server'; - -const localtunnel_server = CreateServer({ - domain: 'domain.example.com', -}); - -process.on('uncaughtException', (err) => { - console.error(err); -}); - -process.on('unhandledRejection', (reason, promise) => { - console.error(reason); -}); - -suite('domain'); - -var lt_server_port; - -before('set up localtunnel server', function(done) { - var server = localtunnel_server.listen(function() { - lt_server_port = server.address().port; - done(); - }); -}); - -test('landing page', function(done) { - var opt = { - host: 'localhost', - port: lt_server_port, - headers: { - host: 'domain.example.com' - }, - path: '/' - } - - var req = http.request(opt, function(res) { - res.setEncoding('utf8'); - assert.equal(res.headers.location, 'https://localtunnel.github.io/www/') - done(); - }); - - req.end(); -}); - -after('shutdown', function() { - localtunnel_server.close(); -}); diff --git a/test/mocha.opts b/test/mocha.opts deleted file mode 100644 index ce1c914..0000000 --- a/test/mocha.opts +++ /dev/null @@ -1,4 +0,0 @@ ---check-leaks ---reporter spec ---ui qunit ---require esm diff --git a/test/queue.js b/test/queue.js deleted file mode 100644 index b945586..0000000 --- a/test/queue.js +++ /dev/null @@ -1,105 +0,0 @@ -import http from 'http'; -import url from 'url'; -import assert from 'assert'; -import localtunnel from 'localtunnel'; - -import CreateServer from '../server'; - -suite('queue'); - -var localtunnel_server = CreateServer({ - max_tcp_sockets: 1 -}); - -var server; -var lt_server_port; - -before('set up localtunnel server', function(done) { - var lt_server = localtunnel_server.listen(function() { - lt_server_port = lt_server.address().port; - done(); - }); -}); - -before('set up local http server', function(done) { - server = http.createServer(); - server.on('request', function(req, res) { - // respond sometime later - setTimeout(function() { - res.setHeader('x-count', req.headers['x-count']); - res.end('foo'); - }, 500); - }); - - server.listen(function() { - var port = server.address().port; - - test._fake_port = port; - done(); - }); -}); - -before('set up localtunnel client', function(done) { - var opt = { - host: 'http://localhost:' + lt_server_port, - }; - - localtunnel(test._fake_port, opt, function(err, tunnel) { - assert.ifError(err); - var url = tunnel.url; - assert.ok(new RegExp('^http:\/\/.*localhost:' + lt_server_port + '$').test(url)); - test._fake_url = url; - done(err); - }); -}); - -test('query localtunnel server w/ ident', function(done) { - var uri = test._fake_url; - var hostname = url.parse(uri).hostname; - - var count = 0; - var opt = { - host: 'localhost', - port: lt_server_port, - agent: false, - headers: { - host: hostname + '.tld' - }, - path: '/' - } - - var num_requests = 2; - var responses = 0; - - function maybe_done() { - if (++responses >= num_requests) { - done(); - } - } - - function make_req() { - opt.headers['x-count'] = count++; - http.get(opt, function(res) { - res.setEncoding('utf8'); - var body = ''; - - res.on('data', function(chunk) { - body += chunk; - }); - - res.on('end', function() { - assert.equal('foo', body); - maybe_done(); - }); - }); - } - - for (var i=0 ; i= 1.3.1 < 2" - -http-signature@~0.11.0: - version "0.11.0" - resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-0.11.0.tgz#1796cf67a001ad5cd6849dca0991485f09089fe6" - dependencies: - asn1 "0.1.11" - assert-plus "^0.1.5" - ctype "0.5.3" + setprototypeof "1.1.0" + statuses ">= 1.4.0 < 2" human-readable-ids@1.0.3: version "1.0.3" @@ -451,14 +367,17 @@ indexof@~0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/indexof/-/indexof-0.0.1.tgz#82dc336d232b9062179d05ab3293a66059fd435d" -inherits@2, inherits@2.0.3, inherits@~2.0.1: +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, inherits@~2.0.3: version "2.0.3" resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" -invert-kv@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/invert-kv/-/invert-kv-1.0.0.tgz#104a8e4aaca6d3d8cd157a8ef8bfab2d7a3ffdb6" - is-arrayish@^0.2.1: version "0.2.1" resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" @@ -475,33 +394,14 @@ is-finite@^1.0.0: dependencies: number-is-nan "^1.0.0" -is-fullwidth-code-point@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz#ef9e31386f031a7f0d643af82fde50c457ef00cb" - dependencies: - number-is-nan "^1.0.0" - is-generator-function@^1.0.3: - version "1.0.6" - resolved "https://registry.yarnpkg.com/is-generator-function/-/is-generator-function-1.0.6.tgz#9e71653cd15fff341c79c4151460a131d31e9fc4" - -is-my-json-valid@^2.12.4: - version "2.16.1" - resolved "https://registry.yarnpkg.com/is-my-json-valid/-/is-my-json-valid-2.16.1.tgz#5a846777e2c2620d1e69104e5d3a03b1f6088f11" - dependencies: - generate-function "^2.0.0" - generate-object-property "^1.1.0" - jsonpointer "^4.0.0" - xtend "^4.0.0" + version "1.0.7" + resolved "https://registry.yarnpkg.com/is-generator-function/-/is-generator-function-1.0.7.tgz#d2132e529bb0000a7f80794d4bdf5cd5e5813522" is-object@~0.1.2: version "0.1.2" resolved "https://registry.yarnpkg.com/is-object/-/is-object-0.1.2.tgz#00efbc08816c33cfc4ac8251d132e10dc65098d7" -is-property@^1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/is-property/-/is-property-1.0.2.tgz#57fe1c4e48474edd65b09911f26b1cd4095dda84" - is-utf8@^0.2.0: version "0.2.1" resolved "https://registry.yarnpkg.com/is-utf8/-/is-utf8-0.2.1.tgz#4b0da1442104d1b336340e80797e865cf39f7d72" @@ -518,25 +418,6 @@ isexe@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" -isstream@~0.1.2: - version "0.1.2" - resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a" - -jade@0.26.3: - version "0.26.3" - resolved "https://registry.yarnpkg.com/jade/-/jade-0.26.3.tgz#8f10d7977d8d79f2f6ff862a81b0513ccb25686c" - dependencies: - commander "0.6.1" - mkdirp "0.3.0" - -json-stringify-safe@~5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb" - -jsonpointer@^4.0.0: - version "4.0.1" - resolved "https://registry.yarnpkg.com/jsonpointer/-/jsonpointer-4.0.1.tgz#4fd92cb34e0e9db3c89c8622ecf51f9b978c6cb9" - keygrip@~1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/keygrip/-/keygrip-1.0.2.tgz#ad3297c557069dea8bcfe7a4fa491b75c5ddeb91" @@ -566,9 +447,9 @@ koa-is-json@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/koa-is-json/-/koa-is-json-1.0.0.tgz#273c07edcdcb8df6a2c1ab7d59ee76491451ec14" -koa@2.4.1: - version "2.4.1" - resolved "https://registry.yarnpkg.com/koa/-/koa-2.4.1.tgz#d449cfb970a7e9da571f699eda40bb9e32eb1484" +koa@2.5.1: + version "2.5.1" + resolved "https://registry.yarnpkg.com/koa/-/koa-2.5.1.tgz#79f8b95f8d72d04fe9a58a8da5ebd6d341103f9c" dependencies: accepts "^1.2.2" content-disposition "~0.5.0" @@ -595,12 +476,6 @@ koa@2.4.1: type-is "^1.5.5" vary "^1.0.0" -lcid@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/lcid/-/lcid-1.0.0.tgz#308accafa0bc483a3867b4b6f2b9506251d1b835" - dependencies: - invert-kv "^1.0.0" - load-json-file@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-1.1.0.tgz#956905708d58b4bab4c2261b04f59f31c99374c0" @@ -617,15 +492,6 @@ localenv@0.2.2: dependencies: commander "2.5.0" -localtunnel@1.8.0: - version "1.8.0" - resolved "https://registry.yarnpkg.com/localtunnel/-/localtunnel-1.8.0.tgz#e8feba5fa015bfa2cc1c5daff0973eaf3adf3337" - dependencies: - debug "2.2.0" - openurl "1.1.0" - request "2.65.0" - yargs "3.29.0" - lodash._arraycopy@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/lodash._arraycopy/-/lodash._arraycopy-3.0.0.tgz#76e7b7c1f1fb92547374878a562ed06a3e50f6e1" @@ -699,10 +565,6 @@ lodash.toarray@^4.4.0: version "4.4.0" resolved "https://registry.yarnpkg.com/lodash.toarray/-/lodash.toarray-4.4.0.tgz#24c4bfcd6b2fba38bfd0594db1179d8e9b656561" -lodash@^4.14.0: - version "4.17.4" - resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.4.tgz#78203a4d1c328ae1d86dca6460e369b57f4055ae" - loud-rejection@^1.0.0: version "1.6.0" resolved "https://registry.yarnpkg.com/loud-rejection/-/loud-rejection-1.6.0.tgz#5b46f80147edee578870f086d04821cf998e551f" @@ -710,17 +572,13 @@ loud-rejection@^1.0.0: currently-unhandled "^0.4.1" signal-exit "^3.0.0" -lru-cache@2: - version "2.7.3" - resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-2.7.3.tgz#6d4524e8b955f95d4f5b58851ce21dd72fb4e952" - map-obj@^1.0.0, map-obj@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/map-obj/-/map-obj-1.0.1.tgz#d933ceb9205d82bdcf4886f6742bdc2b4dea146d" -marked-terminal@^1.6.2: - version "1.7.0" - resolved "https://registry.yarnpkg.com/marked-terminal/-/marked-terminal-1.7.0.tgz#c8c460881c772c7604b64367007ee5f77f125904" +marked-terminal@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/marked-terminal/-/marked-terminal-2.0.0.tgz#5eaf568be66f686541afa52a558280310a31de2d" dependencies: cardinal "^1.0.0" chalk "^1.1.3" @@ -728,9 +586,9 @@ marked-terminal@^1.6.2: lodash.assign "^4.2.0" node-emoji "^1.4.1" -marked@^0.3.6: - version "0.3.7" - resolved "https://registry.yarnpkg.com/marked/-/marked-0.3.7.tgz#80ef3bbf1bd00d1c9cfebe42ba1b8c85da258d0d" +marked@^0.3.12: + version "0.3.19" + resolved "https://registry.yarnpkg.com/marked/-/marked-0.3.19.tgz#5d47f709c4c9fc3c216b6d46127280f40b39d790" media-typer@0.3.0: version "0.3.0" @@ -751,22 +609,29 @@ meow@^3.3.0: redent "^1.0.0" trim-newlines "^1.0.0" -mime-db@~1.30.0: - version "1.30.0" - resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.30.0.tgz#74c643da2dd9d6a45399963465b26d5ca7d71f01" +methods@^1.1.1, methods@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" -mime-types@^2.0.7, mime-types@^2.1.11, mime-types@~2.1.15, mime-types@~2.1.16, mime-types@~2.1.7: - version "2.1.17" - resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.17.tgz#09d7a393f03e995a79f8af857b70a9e0ab16557a" - dependencies: - mime-db "~1.30.0" +mime-db@~1.33.0: + version "1.33.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.33.0.tgz#a3492050a5cb9b63450541e39d9788d2272783db" -minimatch@0.3: - version "0.3.0" - resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-0.3.0.tgz#275d8edaac4f1bb3326472089e7949c8394699dd" +mime-types@^2.0.7, mime-types@^2.1.12, mime-types@~2.1.18: + version "2.1.18" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.18.tgz#6f323f60a83d11146f831ff11fd66e2fe5503bb8" dependencies: - lru-cache "2" - sigmund "~1.0.0" + mime-db "~1.33.0" + +mime@^1.4.1: + version "1.6.0" + resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" + +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" @@ -780,47 +645,32 @@ minimist@~0.0.1: version "0.0.10" resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.10.tgz#de3f98543dbf96082be48ad1a0c7cda836301dcf" -mkdirp@0.3.0: - version "0.3.0" - resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.3.0.tgz#1bbf5ab1ba827af23575143490426455f481fe1e" - 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@2.5.3: - version "2.5.3" - resolved "https://registry.yarnpkg.com/mocha/-/mocha-2.5.3.tgz#161be5bdeb496771eb9b35745050b622b5aefc58" +mocha@5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/mocha/-/mocha-5.1.1.tgz#b774c75609dac05eb48f4d9ba1d827b97fde8a7b" dependencies: - commander "2.3.0" - debug "2.2.0" - diff "1.4.0" - escape-string-regexp "1.0.2" - glob "3.2.11" - growl "1.9.2" - jade "0.26.3" + browser-stdout "1.3.1" + commander "2.11.0" + debug "3.1.0" + diff "3.5.0" + escape-string-regexp "1.0.5" + glob "7.1.2" + growl "1.10.3" + he "1.1.1" + minimatch "3.0.4" mkdirp "0.5.1" - supports-color "1.2.0" - to-iso-string "0.0.2" - -ms@0.7.1: - version "0.7.1" - resolved "https://registry.yarnpkg.com/ms/-/ms-0.7.1.tgz#9cd13c03adbff25b65effde7ce864ee952017098" + supports-color "4.4.0" ms@2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" -nan@^2.0.5: - version "2.8.0" - resolved "https://registry.yarnpkg.com/nan/-/nan-2.8.0.tgz#ed715f3fe9de02b57a5e6252d90a96675e1f085a" - -nan@~2.4.0: - version "2.4.0" - resolved "https://registry.yarnpkg.com/nan/-/nan-2.4.0.tgz#fb3c59d45fe4effe215f0b890f8adf6eb32d2232" - negotiator@0.6.1: version "0.6.1" resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.1.tgz#2b327184e8992101177b28563fb5e7102acd0ca9" @@ -854,10 +704,6 @@ node-notifier@^4.0.2: shellwords "^0.1.0" which "^1.0.5" -node-uuid@~1.4.3: - version "1.4.8" - resolved "https://registry.yarnpkg.com/node-uuid/-/node-uuid-1.4.8.tgz#b040eb0923968afabf8d32fb1f17f1167fdab907" - normalize-package-data@^2.3.2, normalize-package-data@^2.3.4: version "2.4.0" resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-2.4.0.tgz#12f95a307d58352075a04907b84ac8be98ac012f" @@ -871,10 +717,6 @@ number-is-nan@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/number-is-nan/-/number-is-nan-1.0.1.tgz#097b602b53422a522c1afb8790318336941a011d" -oauth-sign@~0.8.0: - version "0.8.2" - resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.8.2.tgz#46a6ab7f0aead8deae9ec0565780b7d4efeb9d43" - object-assign@^4.0.1: version "4.1.1" resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" @@ -887,13 +729,13 @@ object-keys@~0.2.0: indexof "~0.0.1" is "~0.2.6" -on-finished@2.3.0, on-finished@^2.1.0: +on-finished@^2.1.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.1, once@^1.4.0: +once@^1.3.0: version "1.4.0" resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" dependencies: @@ -903,10 +745,6 @@ only@0.0.2: version "0.0.2" resolved "https://registry.yarnpkg.com/only/-/only-0.0.2.tgz#2afde84d03e50b9a8edc444e30610a70295edfb4" -openurl@1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/openurl/-/openurl-1.1.0.tgz#e2f2189d999c04823201f083f0f1a7cd8903187a" - optimist@0.6.1: version "0.6.1" resolved "https://registry.yarnpkg.com/optimist/-/optimist-0.6.1.tgz#da3ea74686fa21a19a111c326e90eb15a0196686" @@ -914,16 +752,6 @@ optimist@0.6.1: minimist "~0.0.1" wordwrap "~0.0.2" -options@>=0.0.5: - version "0.0.6" - resolved "https://registry.yarnpkg.com/options/-/options-0.0.6.tgz#ec22d312806bb53e731773e7cdaefcf1c643128f" - -os-locale@^1.4.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/os-locale/-/os-locale-1.4.0.tgz#20f9f17ae29ed345e8bde583b13d2009803c14d9" - dependencies: - lcid "^1.0.0" - parse-json@^2.2.0: version "2.2.0" resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-2.2.0.tgz#f480f40434ef80741f8469099f8dea18f55a4dc9" @@ -940,6 +768,10 @@ path-exists@^2.0.0: dependencies: pinkie-promise "^2.0.0" +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-parse@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.5.tgz#3c1adf871ea9cd6c9431b6ea2bd74a0ff055c4c1" @@ -966,24 +798,17 @@ pinkie@^2.0.0: version "2.0.4" resolved "https://registry.yarnpkg.com/pinkie/-/pinkie-2.0.4.tgz#72556b80cfa0d48a974e80e77248e80ed4f7f870" -process-nextick-args@~1.0.6: - version "1.0.7" - resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-1.0.7.tgz#150e20b756590ad3f91093f25a4f2ad8bff30ba3" - -pump@2.0.0: +process-nextick-args@~2.0.0: version "2.0.0" - resolved "https://registry.yarnpkg.com/pump/-/pump-2.0.0.tgz#7946da1c8d622b098e2ceb2d3476582470829c9d" - dependencies: - end-of-stream "^1.1.0" - once "^1.3.1" + resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.0.tgz#a37d732f4271b4ab1ad070d35508e8290788ffaa" punycode@^1.4.1: version "1.4.1" resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e" -qs@~5.2.0: - version "5.2.1" - resolved "https://registry.yarnpkg.com/qs/-/qs-5.2.1.tgz#801fee030e0b9450d6385adc48a4cc55b44aedfc" +qs@^6.5.1: + version "6.5.2" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36" read-pkg-up@^1.0.1: version "1.0.1" @@ -1000,15 +825,16 @@ read-pkg@^1.0.0: normalize-package-data "^2.3.2" path-type "^1.0.0" -readable-stream@~2.0.5: - version "2.0.6" - resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.0.6.tgz#8f90341e68a53ccc928788dacfcd11b36eb9b78e" +readable-stream@^2.0.5: + version "2.3.6" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.6.tgz#b11c27d88b8ff1fbe070643cf94b0c79ae1b0aaf" dependencies: core-util-is "~1.0.0" - inherits "~2.0.1" + inherits "~2.0.3" isarray "~1.0.0" - process-nextick-args "~1.0.6" - string_decoder "~0.10.x" + process-nextick-args "~2.0.0" + safe-buffer "~5.1.1" + string_decoder "~1.1.1" util-deprecate "~1.0.1" redent@^1.0.0: @@ -1030,101 +856,69 @@ repeating@^2.0.0: dependencies: is-finite "^1.0.0" -request@2.65.0: - version "2.65.0" - resolved "https://registry.yarnpkg.com/request/-/request-2.65.0.tgz#cc1a3bc72b96254734fc34296da322f9486ddeba" - dependencies: - aws-sign2 "~0.6.0" - bl "~1.0.0" - caseless "~0.11.0" - combined-stream "~1.0.5" - extend "~3.0.0" - forever-agent "~0.6.1" - form-data "~1.0.0-rc3" - har-validator "~2.0.2" - hawk "~3.1.0" - http-signature "~0.11.0" - isstream "~0.1.2" - json-stringify-safe "~5.0.1" - mime-types "~2.1.7" - node-uuid "~1.4.3" - oauth-sign "~0.8.0" - qs "~5.2.0" - stringstream "~0.0.4" - tough-cookie "~2.2.0" - tunnel-agent "~0.4.1" - resolve@^1.0.0: - version "1.5.0" - resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.5.0.tgz#1f09acce796c9a762579f31b2c1cc4c3cddf9f36" + version "1.7.1" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.7.1.tgz#aadd656374fd298aee895bc026b8297418677fd3" dependencies: path-parse "^1.0.5" -"semver@2 || 3 || 4 || 5", semver@^5.1.0: - version "5.4.1" - resolved "https://registry.yarnpkg.com/semver/-/semver-5.4.1.tgz#e059c09d8571f0540823733433505d3a2f00b18e" +safe-buffer@~5.1.0, safe-buffer@~5.1.1: + version "5.1.2" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" -setprototypeof@1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.0.3.tgz#66567e37043eeb4f04d91bd658c0cbefb55b8e04" +"semver@2 || 3 || 4 || 5", semver@^5.1.0: + version "5.5.0" + resolved "https://registry.yarnpkg.com/semver/-/semver-5.5.0.tgz#dc4bbc7a6ca9d916dee5d43516f0092b58f7b8ab" + +setprototypeof@1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.0.tgz#d0bd85536887b6fe7c0d818cb962d9d91c54e656" shellwords@^0.1.0: version "0.1.1" resolved "https://registry.yarnpkg.com/shellwords/-/shellwords-0.1.1.tgz#d6b9181c1a48d397324c84871efbcfc73fc0654b" -sigmund@~1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/sigmund/-/sigmund-1.0.1.tgz#3ff21f198cad2175f9f3b781853fd94d0d19b590" - signal-exit@^3.0.0: version "3.0.2" resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.2.tgz#b5fdc08f1287ea1178628e415e25132b73646c6d" -sntp@1.x.x: - version "1.0.9" - resolved "https://registry.yarnpkg.com/sntp/-/sntp-1.0.9.tgz#6541184cc90aeea6c6e7b35e2659082443c66198" +spdx-correct@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-3.0.0.tgz#05a5b4d7153a195bc92c3c425b69f3b2a9524c82" dependencies: - hoek "2.x.x" + spdx-expression-parse "^3.0.0" + spdx-license-ids "^3.0.0" -spdx-correct@~1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-1.0.2.tgz#4b3073d933ff51f3912f03ac5519498a4150db40" +spdx-exceptions@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/spdx-exceptions/-/spdx-exceptions-2.1.0.tgz#2c7ae61056c714a5b9b9b2b2af7d311ef5c78fe9" + +spdx-expression-parse@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/spdx-expression-parse/-/spdx-expression-parse-3.0.0.tgz#99e119b7a5da00e05491c9fa338b7904823b41d0" dependencies: - spdx-license-ids "^1.0.2" + spdx-exceptions "^2.1.0" + spdx-license-ids "^3.0.0" -spdx-expression-parse@~1.0.0: - version "1.0.4" - resolved "https://registry.yarnpkg.com/spdx-expression-parse/-/spdx-expression-parse-1.0.4.tgz#9bdf2f20e1f40ed447fbe273266191fced51626c" - -spdx-license-ids@^1.0.2: - version "1.2.2" - resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-1.2.2.tgz#c9df7a3424594ade6bd11900d596696dc06bac57" +spdx-license-ids@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-3.0.0.tgz#7a7cd28470cc6d3a1cfe6d66886f6bc430d3ac87" stackframe@^0.3.1: version "0.3.1" resolved "https://registry.yarnpkg.com/stackframe/-/stackframe-0.3.1.tgz#33aa84f1177a5548c8935533cbfeb3420975f5a4" -"statuses@>= 1.3.1 < 2", statuses@^1.2.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.4.0.tgz#bb73d446da2796106efcc1b601a253d6c46bd087" +"statuses@>= 1.4.0 < 2", statuses@^1.2.0: + version "1.5.0" + resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c" -string-width@^1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-1.0.2.tgz#118bdf5b8cdc51a2a7e70d211e07e2b0b9b107d3" +string_decoder@~1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8" dependencies: - code-point-at "^1.0.0" - is-fullwidth-code-point "^1.0.0" - strip-ansi "^3.0.0" + safe-buffer "~5.1.0" -string_decoder@~0.10.x: - version "0.10.31" - resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-0.10.31.tgz#62e203bc41766c6c28c9fc84301dab1c5310fa94" - -stringstream@~0.0.4: - version "0.0.5" - resolved "https://registry.yarnpkg.com/stringstream/-/stringstream-0.0.5.tgz#4e484cd4de5a0bbbee18e46307710a8a81621878" - -strip-ansi@^3.0.0, strip-ansi@^3.0.1: +strip-ansi@^3.0.0: version "3.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-3.0.1.tgz#6a385fb8853d952d5ff05d0e8aaf94278dc63dcf" dependencies: @@ -1142,9 +936,33 @@ strip-indent@^1.0.1: dependencies: get-stdin "^4.0.1" -supports-color@1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-1.2.0.tgz#ff1ed1e61169d06b3cf2d588e188b18d8847e17e" +superagent@3.8.2: + version "3.8.2" + resolved "https://registry.yarnpkg.com/superagent/-/superagent-3.8.2.tgz#e4a11b9d047f7d3efeb3bbe536d9ec0021d16403" + dependencies: + component-emitter "^1.2.0" + cookiejar "^2.1.0" + debug "^3.1.0" + extend "^3.0.0" + form-data "^2.3.1" + formidable "^1.1.1" + methods "^1.1.1" + mime "^1.4.1" + qs "^6.5.1" + readable-stream "^2.0.5" + +supertest@3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/supertest/-/supertest-3.1.0.tgz#f9ebaf488e60f2176021ec580bdd23ad269e7bc6" + dependencies: + methods "~1.1.2" + superagent "3.8.2" + +supports-color@4.4.0: + version "4.4.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-4.4.0.tgz#883f7ddabc165142b2a61427f3352ded195d1a3e" + dependencies: + has-flag "^2.0.0" supports-color@^2.0.0: version "2.0.0" @@ -1156,50 +974,27 @@ tldjs@2.3.1: dependencies: punycode "^1.4.1" -to-iso-string@0.0.2: - version "0.0.2" - resolved "https://registry.yarnpkg.com/to-iso-string/-/to-iso-string-0.0.2.tgz#4dc19e664dfccbe25bd8db508b00c6da158255d1" - -tough-cookie@~2.2.0: - version "2.2.2" - resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.2.2.tgz#c83a1830f4e5ef0b93ef2a3488e724f8de016ac7" - trim-newlines@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/trim-newlines/-/trim-newlines-1.0.0.tgz#5887966bb582a4503a41eb524f7d35011815a613" -tunnel-agent@~0.4.1: - version "0.4.3" - resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.4.3.tgz#6373db76909fe570e08d73583365ed828a74eeeb" - type-is@^1.5.5: - version "1.6.15" - resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.15.tgz#cab10fb4909e441c82842eafe1ad646c81804410" + 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.15" - -ultron@1.0.x: - version "1.0.2" - resolved "https://registry.yarnpkg.com/ultron/-/ultron-1.0.2.tgz#ace116ab557cd197386a4e88f4685378c8b2e4fa" - -utf-8-validate@1.2.x: - version "1.2.2" - resolved "https://registry.yarnpkg.com/utf-8-validate/-/utf-8-validate-1.2.2.tgz#8bb871a4741e085c70487ca7acdbd7d6d36029eb" - dependencies: - bindings "~1.2.1" - nan "~2.4.0" + mime-types "~2.1.18" util-deprecate@~1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" validate-npm-package-license@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/validate-npm-package-license/-/validate-npm-package-license-3.0.1.tgz#2804babe712ad3379459acfbe24746ab2c303fbc" + version "3.0.3" + resolved "https://registry.yarnpkg.com/validate-npm-package-license/-/validate-npm-package-license-3.0.3.tgz#81643bcbef1bdfecd4623793dc4648948ba98338" dependencies: - spdx-correct "~1.0.0" - spdx-expression-parse "~1.0.0" + spdx-correct "^3.0.0" + spdx-expression-parse "^3.0.0" vary@^1.0.0: version "1.1.2" @@ -1211,38 +1006,19 @@ which@^1.0.5: dependencies: isexe "^2.0.0" -window-size@^0.1.2: - version "0.1.4" - resolved "https://registry.yarnpkg.com/window-size/-/window-size-0.1.4.tgz#f8e1aa1ee5a53ec5bf151ffa09742a6ad7697876" - wordwrap@~0.0.2: version "0.0.3" resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-0.0.3.tgz#a3d5da6cd5c0bc0008d37234bbaf1bed63059107" -wrap-ansi@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-2.1.0.tgz#d8fc3d284dd05794fe84973caecdd1cf824fdd85" - dependencies: - string-width "^1.0.1" - strip-ansi "^3.0.1" - wrappy@1: version "1.0.2" resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" -ws@0.8.0: - version "0.8.0" - resolved "https://registry.yarnpkg.com/ws/-/ws-0.8.0.tgz#ac60ebad312121d01e16cc3383d7ec67ad0f0f1f" +ws@5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/ws/-/ws-5.1.1.tgz#1d43704689711ac1942fd2f283e38f825c4b8b95" dependencies: - options ">=0.0.5" - ultron "1.0.x" - optionalDependencies: - bufferutil "1.2.x" - utf-8-validate "1.2.x" - -xtend@^4.0.0: - version "4.0.1" - resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.1.tgz#a5c6d532be656e23db820efb943a1f04998d63af" + async-limiter "~1.0.0" xtend@~2.0.6: version "2.0.6" @@ -1250,18 +1026,3 @@ xtend@~2.0.6: dependencies: is-object "~0.1.2" object-keys "~0.2.0" - -y18n@^3.2.0: - version "3.2.1" - resolved "https://registry.yarnpkg.com/y18n/-/y18n-3.2.1.tgz#6d15fba884c08679c0d77e88e7759e811e07fa41" - -yargs@3.29.0: - version "3.29.0" - resolved "https://registry.yarnpkg.com/yargs/-/yargs-3.29.0.tgz#1aab9660eae79d8b8f675bcaeeab6ee34c2cf69c" - dependencies: - camelcase "^1.2.1" - cliui "^3.0.3" - decamelize "^1.0.0" - os-locale "^1.4.0" - window-size "^0.1.2" - y18n "^3.2.0"