diff --git a/bin/server b/bin/server index a6bba9d8..ea7ba2b4 100755 --- a/bin/server +++ b/bin/server @@ -31,6 +31,14 @@ const argv = optimist default: 10, describe: 'maximum number of tcp sockets each client is allowed to establish at one time (the tunnels)' }) + .options('range', { + default: null, + describe: 'will bind incoming connections only on ports in range xxx:xxxx' + }) + .options('jwt-shared-secret', { + default: null, + describe: 'JWT shared secret used to encode tokens' + }) .argv; if (argv.help) { @@ -42,6 +50,8 @@ const server = CreateServer({ max_tcp_sockets: argv['max-sockets'], secure: argv.secure, domain: argv.domain, + range: argv.range, + jwt_shared_secret: argv['jwt-shared-secret'] }); server.listen(argv.port, argv.address, () => { diff --git a/lib/Client.js b/lib/Client.js index 369eb079..bca0c5ad 100644 --- a/lib/Client.js +++ b/lib/Client.js @@ -2,6 +2,7 @@ import http from 'http'; import Debug from 'debug'; import pump from 'pump'; import EventEmitter from 'events'; +import jwt from 'jsonwebtoken'; // A client encapsulates req/res handling using an agent // @@ -13,6 +14,7 @@ class Client extends EventEmitter { const agent = this.agent = options.agent; const id = this.id = options.id; + this.securityToken = options.securityToken; this.debug = Debug(`lt:Client[${this.id}]`); @@ -45,6 +47,21 @@ class Client extends EventEmitter { }); } + isSecurityTokenEqual(securityToken) { + const decodeJWT = (token) => { + return jwt.decode(token.replace(/Bearer /, ''), {complete: false}); + } + if (this.securityToken === null) { + return false; + } + try{ + return decodeJWT(this.securityToken).name === decodeJWT(securityToken).name; + }catch(error){ + this.debug('error with jwt %s %s', securityToken, error); + return false; + } + } + stats() { return this.agent.stats(); } @@ -129,4 +146,4 @@ class Client extends EventEmitter { } } -export default Client; \ No newline at end of file +export default Client; diff --git a/lib/ClientManager.js b/lib/ClientManager.js index e1a78386..53a32508 100644 --- a/lib/ClientManager.js +++ b/lib/ClientManager.js @@ -3,6 +3,7 @@ import Debug from 'debug'; import Client from './Client'; import TunnelAgent from './TunnelAgent'; +import PortManager from "./PortManager"; // Manage sets of clients // @@ -13,6 +14,7 @@ class ClientManager { // id -> client instance this.clients = new Map(); + this.portManager = new PortManager({range: this.opt.range||null}) // statistics this.stats = { @@ -28,7 +30,7 @@ class ClientManager { // create a new tunnel with `id` // if the id is already used, a random id is assigned // if the tunnel could not be created, throws an error - async newClient(id) { + async newClient(id, securityToken) { const clients = this.clients; const stats = this.stats; @@ -39,6 +41,7 @@ class ClientManager { const maxSockets = this.opt.max_tcp_sockets; const agent = new TunnelAgent({ + portManager: this.portManager, clientId: id, maxSockets: 10, }); @@ -46,6 +49,7 @@ class ClientManager { const client = new Client({ id, agent, + securityToken }); // add to clients map immediately @@ -79,6 +83,7 @@ class ClientManager { if (!client) { return; } + this.portManager.release(client.agent.port); --this.stats.tunnels; delete this.clients[id]; client.close(); diff --git a/lib/ClientManager.test.js b/lib/ClientManager.test.js index d85abe55..852be61a 100644 --- a/lib/ClientManager.test.js +++ b/lib/ClientManager.test.js @@ -11,22 +11,22 @@ describe('ClientManager', () => { it('should create a new client with random id', async () => { const manager = new ClientManager(); - const client = await manager.newClient(); + const client = await manager.newClient(null, null); 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'); + const client = await manager.newClient('foobar', null); 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'); + const clientA = await manager.newClient('foobar', null); + const clientB = await manager.newClient('foobar', null); assert(clientA.id, 'foobar'); assert(manager.hasClient(clientB.id)); assert(clientB.id != clientA.id); @@ -36,7 +36,7 @@ describe('ClientManager', () => { it('should remove client once it goes offline', async () => { const manager = new ClientManager(); - const client = await manager.newClient('foobar'); + const client = await manager.newClient('foobar', null); const socket = await new Promise((resolve) => { const netClient = net.createConnection({ port: client.port }, () => { @@ -57,8 +57,8 @@ describe('ClientManager', () => { it('should remove correct client once it goes offline', async () => { const manager = new ClientManager(); - const clientFoo = await manager.newClient('foo'); - const clientBar = await manager.newClient('bar'); + const clientFoo = await manager.newClient('foo', null); + const clientBar = await manager.newClient('bar', null); const socket = await new Promise((resolve) => { const netClient = net.createConnection({ port: clientFoo.port }, () => { @@ -80,7 +80,7 @@ describe('ClientManager', () => { it('should remove clients if they do not connect within 5 seconds', async () => { const manager = new ClientManager(); - const clientFoo = await manager.newClient('foo'); + const clientFoo = await manager.newClient('foo', null); assert(manager.hasClient('foo')); // wait past grace period (1s) diff --git a/lib/PortManager.js b/lib/PortManager.js new file mode 100644 index 00000000..cffc4ac8 --- /dev/null +++ b/lib/PortManager.js @@ -0,0 +1,60 @@ +import Debug from 'debug'; + +class PortManager { + constructor(opt) { + this.debug = Debug('lt:PortManager'); + this.range = opt.range || null; + this.first = null; + this.last = null; + this.pool = {}; + this.initializePool(); + } + + initializePool() { + if (this.range === null) { + return; + } + + if (!/^[0-9]+:[0-9]+$/.test(this.range)) { + throw new Error('Bad range expression: ' + this.range); + } + + [this.first, this.last] = this.range.split(':').map((port) => parseInt(port)); + + if (this.first > this.last) { + throw new Error('Bad range expression min > max: ' + this.range); + } + + for (let port = this.first; port <= this.last; port++) { + this.pool['_' + port] = null; + } + this.debug = Debug('lt:PortManager'); + this.debug('Pool initialized ' + JSON.stringify(this.pool)); + } + + release(port) { + if (this.range === null) { + return; + } + this.debug('Release port ' + port); + this.pool['_' + port] = null; + } + + getNextAvailable(clientId) { + if (this.range === null) { + return null; + } + + for (let port = this.first; port <= this.last; port++) { + if (this.pool['_' + port] === null) { + this.pool['_' + port] = clientId; + this.debug('Port found ' + port); + return port; + } + } + this.debug('No more ports available '); + throw new Error('No more ports available in range ' + this.range); + } +} + +export default PortManager; diff --git a/lib/PortManager.test.js b/lib/PortManager.test.js new file mode 100644 index 00000000..eb403e13 --- /dev/null +++ b/lib/PortManager.test.js @@ -0,0 +1,51 @@ +import assert from 'assert'; + +import PortManager from './PortManager'; + +describe('PortManager', () => { + it('should construct with no range', () => { + const portManager = new PortManager({}); + assert.equal(portManager.range, null); + assert.equal(portManager.first, null); + assert.equal(portManager.last, null); + }); + + it('should construct with range', () => { + const portManager = new PortManager({range: '10:20'}); + assert.equal(portManager.range, '10:20'); + assert.equal(portManager.first, 10); + assert.equal(portManager.last, 20); + }); + + it('should not construct with bad range expression', () => { + assert.throws(()=>{ + new PortManager({range: 'a1020'}); + }, /Bad range expression: a1020/) + }); + + it('should not construct with bad range max>min', () => { + assert.throws(()=>{ + new PortManager({range: '20:10'}); + }, /Bad range expression min > max: 20:10/) + }); + + it('should work has expected', async () => { + const portManager = new PortManager({range: '10:12'}); + assert.equal(10,portManager.getNextAvailable('a')); + assert.equal(11,portManager.getNextAvailable('b')); + assert.equal(12,portManager.getNextAvailable('c')); + + assert.throws(()=>{ + portManager.getNextAvailable(); + }, /No more ports available in range 10:12/) + + portManager.release(11); + assert.equal(11,portManager.getNextAvailable('bb')); + + portManager.release(10); + portManager.release(12); + + assert.equal(10,portManager.getNextAvailable('cc')); + assert.equal(12,portManager.getNextAvailable('dd')); + }); +}); diff --git a/lib/TunnelAgent.js b/lib/TunnelAgent.js index efc2231b..a953d726 100644 --- a/lib/TunnelAgent.js +++ b/lib/TunnelAgent.js @@ -20,6 +20,9 @@ class TunnelAgent extends Agent { // sockets we can hand out via createConnection this.availableSockets = []; + this.port = null; + this.clientId = options.clientId + this.portManager = options.portManager || null; // 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 @@ -63,13 +66,14 @@ class TunnelAgent extends Agent { }); return new Promise((resolve) => { - server.listen(() => { - const port = server.address().port; - this.debug('tcp server listening on port: %d', port); + const port = this.portManager ? this.portManager.getNextAvailable(this.options.clientId) : null; + server.listen(port,() => { + this.port = server.address().port + this.debug('tcp server listening on port: %d (%s)', this.port, this.clientId); resolve({ // port for lt client tcp connections - port: port, + port: this.port, }); }); }); @@ -115,6 +119,9 @@ class TunnelAgent extends Agent { 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 + if(this.portManager){ + this.portManager.release(this.port); + } socket.destroy(); }); diff --git a/package.json b/package.json index 0693b8dd..39b58f59 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,7 @@ "esm": "3.0.34", "human-readable-ids": "1.0.3", "koa": "2.5.1", + "koa-jwt": "4.0.0", "koa-router": "7.4.0", "localenv": "0.2.2", "optimist": "0.6.1", @@ -29,6 +30,6 @@ "scripts": { "test": "mocha --check-leaks --require esm './**/*.test.js'", "start": "./bin/server", - "dev": "node-dev bin/server --port 3000" + "dev": "node-dev bin/server --require esm --port 3000" } } diff --git a/server.js b/server.js index 21a4af69..b5fb32ef 100644 --- a/server.js +++ b/server.js @@ -5,11 +5,18 @@ import Debug from 'debug'; import http from 'http'; import { hri } from 'human-readable-ids'; import Router from 'koa-router'; +import jwt from'koa-jwt'; import ClientManager from './lib/ClientManager'; const debug = Debug('localtunnel:server'); +function addJwtMiddleware(app, opt) { + app.use(jwt({ + secret: opt.jwt_shared_secret + })); +} + export default function(opt) { opt = opt || {}; @@ -28,6 +35,10 @@ export default function(opt) { const app = new Koa(); const router = new Router(); + if (opt.jwt_shared_secret){ + addJwtMiddleware(app, opt); + } + router.get('/api/status', async (ctx, next) => { const stats = manager.stats; ctx.body = { @@ -50,6 +61,44 @@ export default function(opt) { }; }); + router.get('/api/tunnels/:id/kill', async (ctx, next) => { + const clientId = ctx.params.id; + if (!opt.jwt_shared_secret){ + debug('disconnecting client with id %s, error: jwt_shared_secret is not used', clientId); + ctx.throw(403, { + success: false, + message: 'jwt_shared_secret is not used' + }); + return; + } + + if (!manager.hasClient(clientId)) { + debug('disconnecting client with id %s, error: client is not connected', clientId); + ctx.throw(404, { + success: false, + message: `client with id ${clientId} is not connected` + }); + } + + const securityToken = ctx.request.headers.authorization; + if (!manager.getClient(clientId).isSecurityTokenEqual(securityToken)) { + debug('disconnecting client with id %s, error: securityToken is not equal ', clientId); + ctx.throw(403, { + success: false, + message: `client with id ${clientId} has not the same securityToken than ${securityToken}` + }); + } + + debug('disconnecting client with id %s', clientId); + manager.removeClient(clientId); + + ctx.statusCode = 200; + ctx.body = { + success: true, + message: `client with id ${clientId} is disconected` + }; + }); + app.use(router.routes()); app.use(router.allowedMethods()); @@ -67,7 +116,7 @@ export default function(opt) { if (isNewClientRequest) { const reqId = hri.random(); debug('making new client with id %s', reqId); - const info = await manager.newClient(reqId); + const info = await manager.newClient(reqId, opt.jwt_shared_secret ? ctx.request.headers.authorization : null); const url = schema + '://' + info.id + '.' + ctx.request.host; info.url = url; @@ -105,7 +154,7 @@ export default function(opt) { } debug('making new client with id %s', reqId); - const info = await manager.newClient(reqId); + const info = await manager.newClient(reqId, opt.jwt_shared_secret ? ctx.request.headers.authorization : null); const url = schema + '://' + info.id + '.' + ctx.request.host; info.url = url; diff --git a/server.test.js b/server.test.js index 10bf8bcf..0d3bb028 100644 --- a/server.test.js +++ b/server.test.js @@ -1,8 +1,9 @@ import request from 'supertest'; import assert from 'assert'; -import { Server as WebSocketServer } from 'ws'; -import WebSocket from 'ws'; +import WebSocket, {Server as WebSocketServer} from 'ws'; import net from 'net'; +import jwt from 'jsonwebtoken'; + import createServer from './server'; @@ -34,6 +35,30 @@ describe('Server', () => { assert.equal(res.body.message, 'Invalid subdomain. Subdomains must be lowercase and between 4 and 63 alphanumeric characters.'); }); + it('reject request without jwt if required', async () => { + const server = createServer({jwt_shared_secret: 'thekey'}); + const res = await request(server).get('/subdomain'); + assert.equal(res.status, 401); + }); + + it('reject request with invalid jwt if required', async () => { + const server = createServer({jwt_shared_secret: 'thekey'}); + const jwtoken = jwt.sign({ + foo: 'bar' + }, 'thebadkey'); + const res = await request(server).get('/subdomain').set('Authorization', `Bearer ${jwtoken}`); + assert.equal(res.status, 401); + }); + + it('accept request with valid jwt if required', async () => { + const server = createServer({jwt_shared_secret: 'thekey'}); + const jwtoken = jwt.sign({ + foo: 'bar' + }, 'thekey'); + const res = await request(server).get('/subdomain').set('Authorization', `Bearer ${jwtoken}`); + assert.equal(res.status, 200); + }); + it('should upgrade websocket requests', async () => { const hostname = 'websocket-test'; const server = createServer({ @@ -106,4 +131,53 @@ describe('Server', () => { await new Promise(resolve => server.close(resolve)); }); -}); \ No newline at end of file + + it('should not support the /api/tunnels/:id/kill endpoint if jwt authorization is not enable on server', async () => { + const server = createServer(); + await new Promise(resolve => server.listen(resolve)); + + const res = await request(server).get('/api/tunnels/foobar-test/kill'); + assert.equal(res.statusCode, 403); + assert.equal(res.text, 'jwt_shared_secret is not used'); + + await new Promise(resolve => server.close(resolve)); + }); + + it('should throw error when calling /api/tunnels/:id/kill endpoint if id does not exists', async () => { + const server = createServer({jwt_shared_secret: 'thekey'}); + await new Promise(resolve => server.listen(resolve)); + + { + const jwtoken = jwt.sign({ + name: 'bar' + }, 'thekey'); + await request(server).get('/foobar-test').set('Authorization', `Bearer ${jwtoken}`); + // no such tunnel yet + const res = await request(server).get('/api/tunnels/foobar-test2/kill').set('Authorization', `Bearer ${jwtoken}`); + assert.equal(res.statusCode, 404); + assert.equal(res.text, 'client with id foobar-test2 is not connected'); + } + + await new Promise(resolve => server.close(resolve)); + }); + + it('should disconnect client when calling /api/tunnels/:id/kill endpoint', async () => { + const server = createServer({jwt_shared_secret: 'thekey'}); + await new Promise(resolve => server.listen(resolve)); + + { + const jwtoken = jwt.sign({ + name: 'bar' + }, 'thekey'); + await request(server).get('/foobar-test').set('Authorization', `Bearer ${jwtoken}`); + + const res = await request(server).get('/api/tunnels/foobar-test/kill').set('Authorization', `Bearer ${jwtoken}`); + assert.equal(res.statusCode, 200); + assert.equal(res.text, '{"success":true,"message":"client with id foobar-test is disconected"}'); + const statusResult = await request(server).get('/api/tunnels/foobar-test/status').set('Authorization', `Bearer ${jwtoken}`); + assert.equal(statusResult.text, 'Not Found'); + } + + await new Promise(resolve => server.close(resolve)); + }); +}); diff --git a/yarn.lock b/yarn.lock index 9ad744bf..b54472e2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9,6 +9,14 @@ accepts@^1.2.2: mime-types "~2.1.18" negotiator "0.6.1" +aggregate-error@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/aggregate-error/-/aggregate-error-3.1.0.tgz#92670ff50f5359bdb7a3e0d40d0ec30c5737687a" + integrity sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA== + dependencies: + clean-stack "^2.0.0" + indent-string "^4.0.0" + ansi-regex@^2.0.0: version "2.1.1" resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-2.1.1.tgz#c3b33ab5ee360d86e0e628f0468ae7ef27d654df" @@ -58,6 +66,11 @@ browser-stdout@1.3.1: version "1.3.1" resolved "https://registry.yarnpkg.com/browser-stdout/-/browser-stdout-1.3.1.tgz#baa559ee14ced73452229bad7326467c61fabd60" +buffer-equal-constant-time@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz#f8e71132f7ffe6e01a5c9697a4c6f3e48d5cc819" + integrity sha1-+OcRMvf/5uAaXJaXpMbz5I1cyBk= + builtin-modules@^1.0.0: version "1.1.1" resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-1.1.1.tgz#270f076c5a72c02f5b65a47df94c5fe3a278892f" @@ -90,6 +103,11 @@ chalk@^1.1.3: strip-ansi "^3.0.0" supports-color "^2.0.0" +clean-stack@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/clean-stack/-/clean-stack-2.2.0.tgz#ee8472dbb129e727b31e8a10a427dee9dfe4008b" + integrity sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A== + cli-table@^0.3.1: version "0.3.1" resolved "https://registry.yarnpkg.com/cli-table/-/cli-table-0.3.1.tgz#f53b05266a8b1a0b934b3d0821e6e2dc5914ae23" @@ -213,6 +231,13 @@ dynamic-dedupe@^0.2.0: dependencies: xtend "~2.0.6" +ecdsa-sig-formatter@1.0.11: + version "1.0.11" + resolved "https://registry.yarnpkg.com/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz#ae0f0fa2d85045ef14a817daa3ce9acd0489e5bf" + integrity sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ== + dependencies: + safe-buffer "^5.0.1" + ee-first@1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" @@ -369,6 +394,11 @@ indent-string@^2.1.0: dependencies: repeating "^2.0.0" +indent-string@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-4.0.0.tgz#624f8f4497d619b2d9768531d58f4122854d7251" + integrity sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg== + indexof@~0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/indexof/-/indexof-0.0.1.tgz#82dc336d232b9062179d05ab3293a66059fd435d" @@ -428,6 +458,39 @@ isexe@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" +jsonwebtoken@^8.5.1: + version "8.5.1" + resolved "https://registry.yarnpkg.com/jsonwebtoken/-/jsonwebtoken-8.5.1.tgz#00e71e0b8df54c2121a1f26137df2280673bcc0d" + integrity sha512-XjwVfRS6jTMsqYs0EsuJ4LGxXV14zQybNd4L2r0UvbVnSF9Af8x7p5MzbJ90Ioz/9TI41/hTCvznF/loiSzn8w== + dependencies: + jws "^3.2.2" + lodash.includes "^4.3.0" + lodash.isboolean "^3.0.3" + lodash.isinteger "^4.0.4" + lodash.isnumber "^3.0.3" + lodash.isplainobject "^4.0.6" + lodash.isstring "^4.0.1" + lodash.once "^4.0.0" + ms "^2.1.1" + semver "^5.6.0" + +jwa@^1.4.1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/jwa/-/jwa-1.4.1.tgz#743c32985cb9e98655530d53641b66c8645b039a" + integrity sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA== + dependencies: + buffer-equal-constant-time "1.0.1" + ecdsa-sig-formatter "1.0.11" + safe-buffer "^5.0.1" + +jws@^3.2.2: + version "3.2.2" + resolved "https://registry.yarnpkg.com/jws/-/jws-3.2.2.tgz#001099f3639468c9414000e99995fa52fb478304" + integrity sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA== + dependencies: + jwa "^1.4.1" + safe-buffer "^5.0.1" + keygrip@~1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/keygrip/-/keygrip-1.0.2.tgz#ad3297c557069dea8bcfe7a4fa491b75c5ddeb91" @@ -457,6 +520,15 @@ 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-jwt@4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/koa-jwt/-/koa-jwt-4.0.0.tgz#4cac70cde5e225bc961a266303d4b6e873cba9bc" + integrity sha512-n56AG98tWQDtvVZwtVFKuPn1pGPOvtkKFEotSPRsdqKmZJqRdppDRD0toiiK7kefMLnVBzFbocaPyaI5WK/iyQ== + dependencies: + jsonwebtoken "^8.5.1" + koa-unless "^1.0.7" + p-any "^2.1.0" + koa-router@7.4.0: version "7.4.0" resolved "https://registry.yarnpkg.com/koa-router/-/koa-router-7.4.0.tgz#aee1f7adc02d5cb31d7d67465c9eacc825e8c5e0" @@ -468,6 +540,11 @@ koa-router@7.4.0: path-to-regexp "^1.1.1" urijs "^1.19.0" +koa-unless@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/koa-unless/-/koa-unless-1.0.7.tgz#b9df375e2b4da3043918d48622520c2c0b79f032" + integrity sha1-ud83XitNowQ5GNSGIlIMLAt58DI= + koa@2.5.1: version "2.5.1" resolved "https://registry.yarnpkg.com/koa/-/koa-2.5.1.tgz#79f8b95f8d72d04fe9a58a8da5ebd6d341103f9c" @@ -566,6 +643,11 @@ lodash.clonedeep@^3.0.0: lodash._baseclone "^3.0.0" lodash._bindcallback "^3.0.0" +lodash.includes@^4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/lodash.includes/-/lodash.includes-4.3.0.tgz#60bb98a87cb923c68ca1e51325483314849f553f" + integrity sha1-YLuYqHy5I8aMoeUTJUgzFISfVT8= + lodash.isarguments@^3.0.0: version "3.1.0" resolved "https://registry.yarnpkg.com/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz#2f573d85c6a24289ff00663b491c1d338ff3458a" @@ -574,6 +656,31 @@ lodash.isarray@^3.0.0: version "3.0.4" resolved "https://registry.yarnpkg.com/lodash.isarray/-/lodash.isarray-3.0.4.tgz#79e4eb88c36a8122af86f844aa9bcd851b5fbb55" +lodash.isboolean@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz#6c2e171db2a257cd96802fd43b01b20d5f5870f6" + integrity sha1-bC4XHbKiV82WgC/UOwGyDV9YcPY= + +lodash.isinteger@^4.0.4: + version "4.0.4" + resolved "https://registry.yarnpkg.com/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz#619c0af3d03f8b04c31f5882840b77b11cd68343" + integrity sha1-YZwK89A/iwTDH1iChAt3sRzWg0M= + +lodash.isnumber@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz#3ce76810c5928d03352301ac287317f11c0b1ffc" + integrity sha1-POdoEMWSjQM1IwGsKHMX8RwLH/w= + +lodash.isplainobject@^4.0.6: + version "4.0.6" + resolved "https://registry.yarnpkg.com/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz#7c526a52d89b45c45cc690b88163be0497f550cb" + integrity sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs= + +lodash.isstring@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/lodash.isstring/-/lodash.isstring-4.0.1.tgz#d527dfb5456eca7cc9bb95d5daeaf88ba54a5451" + integrity sha1-1SfftUVuynzJu5XV2ur4i6VKVFE= + lodash.keys@^3.0.0: version "3.1.2" resolved "https://registry.yarnpkg.com/lodash.keys/-/lodash.keys-3.1.2.tgz#4dbc0472b156be50a0b286855d1bd0b0c656098a" @@ -582,6 +689,11 @@ lodash.keys@^3.0.0: lodash.isarguments "^3.0.0" lodash.isarray "^3.0.0" +lodash.once@^4.0.0: + version "4.1.1" + resolved "https://registry.yarnpkg.com/lodash.once/-/lodash.once-4.1.1.tgz#0dd3971213c7c56df880977d504c88fb471a97ac" + integrity sha1-DdOXEhPHxW34gJd9UEyI+0cal6w= + lodash.toarray@^4.4.0: version "4.4.0" resolved "https://registry.yarnpkg.com/lodash.toarray/-/lodash.toarray-4.4.0.tgz#24c4bfcd6b2fba38bfd0594db1179d8e9b656561" @@ -692,6 +804,11 @@ ms@2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" +ms@^2.1.1: + version "2.1.3" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" + integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== + negotiator@0.6.1: version "0.6.1" resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.1.tgz#2b327184e8992101177b28563fb5e7102acd0ca9" @@ -773,6 +890,28 @@ optimist@0.6.1: minimist "~0.0.1" wordwrap "~0.0.2" +p-any@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/p-any/-/p-any-2.1.0.tgz#719489408e14f5f941a748f1e817f5c71cab35cb" + integrity sha512-JAERcaMBLYKMq+voYw36+x5Dgh47+/o7yuv2oQYuSSUml4YeqJEFznBrY2UeEkoSHqBua6hz518n/PsowTYLLg== + dependencies: + p-cancelable "^2.0.0" + p-some "^4.0.0" + type-fest "^0.3.0" + +p-cancelable@^2.0.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/p-cancelable/-/p-cancelable-2.1.1.tgz#aab7fbd416582fa32a3db49859c122487c5ed2cf" + integrity sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg== + +p-some@^4.0.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/p-some/-/p-some-4.1.0.tgz#28e73bc1e0d62db54c2ed513acd03acba30d5c04" + integrity sha512-MF/HIbq6GeBqTrTIl5OJubzkGU+qfFhAFi0gnTAK6rgEIJIknEiABHOTtQu4e6JiXjIwuMPMUFQzyHh5QjCl1g== + dependencies: + aggregate-error "^3.0.0" + p-cancelable "^2.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" @@ -896,6 +1035,11 @@ resolve@^1.0.0: dependencies: path-parse "^1.0.5" +safe-buffer@^5.0.1: + version "5.2.1" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" + integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== + 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" @@ -904,6 +1048,11 @@ safe-buffer@~5.1.0, safe-buffer@~5.1.1: version "5.5.0" resolved "https://registry.yarnpkg.com/semver/-/semver-5.5.0.tgz#dc4bbc7a6ca9d916dee5d43516f0092b58f7b8ab" +semver@^5.6.0: + version "5.7.1" + resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" + integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ== + setprototypeof@1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.0.tgz#d0bd85536887b6fe7c0d818cb962d9d91c54e656" @@ -1012,6 +1161,11 @@ trim-newlines@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/trim-newlines/-/trim-newlines-1.0.0.tgz#5887966bb582a4503a41eb524f7d35011815a613" +type-fest@^0.3.0: + version "0.3.1" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.3.1.tgz#63d00d204e059474fe5e1b7c011112bbd1dc29e1" + integrity sha512-cUGJnCdr4STbePCgqNFbpVNCepa+kAVohJs1sLhxzdH+gnEoOd8VhbYa7pD3zZYGiURWM2xzEII3fQcRizDkYQ== + type-is@^1.5.5: version "1.6.16" resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.16.tgz#f89ce341541c672b25ee7ae3c73dee3b2be50194"