From 9ebd4077eeb49bc3e257a8a08f3461c8f03baac7 Mon Sep 17 00:00:00 2001 From: Long Nguyen Date: Fri, 5 Feb 2021 17:16:51 +0900 Subject: [PATCH] Support TCP port range limitation --- bin/server | 68 ---------- bin/server.js | 94 ++++++++++++++ lib/ClientManager.js | 140 ++++++++++---------- lib/TunnelAgent.js | 297 ++++++++++++++++++++++--------------------- package.json | 11 +- server.js | 257 +++++++++++++++++++------------------ yarn.lock | 162 +++++++++++++++++++++-- 7 files changed, 602 insertions(+), 427 deletions(-) delete mode 100755 bin/server create mode 100755 bin/server.js diff --git a/bin/server b/bin/server deleted file mode 100755 index a6bba9d..0000000 --- a/bin/server +++ /dev/null @@ -1,68 +0,0 @@ -#!/usr/bin/env node -r esm - -import 'localenv'; -import optimist from 'optimist'; - -import log from 'book'; -import Debug from 'debug'; - -import CreateServer from '../server'; - -const debug = Debug('localtunnel'); - -const argv = optimist - .usage('Usage: $0 --port [num]') - .options('secure', { - default: false, - describe: 'use this flag to indicate proxy over https' - }) - .options('port', { - default: '80', - describe: 'listen on this port for outside requests' - }) - .options('address', { - default: '0.0.0.0', - describe: 'IP address to bind to' - }) - .options('domain', { - describe: 'Specify the base domain name. This is optional if hosting localtunnel from a regular example.com domain. This is required if hosting a localtunnel server from a subdomain (i.e. lt.example.dom where clients will be client-app.lt.example.come)', - }) - .options('max-sockets', { - default: 10, - describe: 'maximum number of tcp sockets each client is allowed to establish at one time (the tunnels)' - }) - .argv; - -if (argv.help) { - optimist.showHelp(); - process.exit(); -} - -const server = CreateServer({ - max_tcp_sockets: argv['max-sockets'], - secure: argv.secure, - domain: argv.domain, -}); - -server.listen(argv.port, argv.address, () => { - debug('server listening on port: %d', server.address().port); -}); - -process.on('SIGINT', () => { - process.exit(); -}); - -process.on('SIGTERM', () => { - process.exit(); -}); - -process.on('uncaughtException', (err) => { - log.error(err); -}); - -process.on('unhandledRejection', (reason, promise) => { - log.error(reason); -}); - -// vim: ft=javascript - diff --git a/bin/server.js b/bin/server.js new file mode 100755 index 0000000..20d8ed0 --- /dev/null +++ b/bin/server.js @@ -0,0 +1,94 @@ +import 'localenv'; +import yargs from 'yargs' +import { hideBin } from 'yargs/helpers' +import log from 'book'; +import Debug from 'debug'; +import CreateServer from '../server'; + +const debug = Debug('localtunnel'); + +const DEFAULT_MIN_PORT = 8000; +const DEFAULT_MAX_PORT = 65535; + +const argv = yargs(hideBin(process.argv)) + .usage('Usage: $0 [option]') + .option('secure', { + alias: 's', + type: 'boolean', + default: false, + description: 'Use this flag to indicate proxy over https' + }) + .option('port', { + alias: 'p', + type: 'number', + default: 80, + description: 'Listen on this port for outside requests' + }) + .option('address', { + default: '0.0.0.0', + description: 'IP address to bind to' + }) + .option('domain', { + type: 'string', + description: 'Specify the base domain name. This is optional if hosting localtunnel from a regular example.com domain. This is required if hosting a localtunnel server from a subdomain (i.e. lt.example.dom where clients will be client-app.lt.example.come)', + }) + .option('max-sockets', { + default: 10, + type: 'count', + description: 'Maximum number of tcp sockets each client is allowed to establish at one time (the tunnels)' + }) + .option('min-port', { + default: DEFAULT_MIN_PORT, + type: 'number', + description: 'The minimum number of TCP ports to use for the localtunnel clients connecting to the server.' + }) + .option('max-port', { + default: DEFAULT_MAX_PORT, + type: 'number', + description: 'The maximum number of TCP ports to use for the localtunnel clients connecting to the server.' + }) + .argv; + +const minPort = argv['min-port']; +const maxPort = argv['max-port']; + +const { address, port, secure, domain, maxSockets } = argv; + +if (Number.isInteger(minPort) && minPort < DEFAULT_MIN_PORT) { + console.error(`min-port must be a number greater than ${DEFAULT_MIN_PORT}`); + process.exit(); +} else if (Number.isInteger(maxPort) && maxPort > DEFAULT_MAX_PORT) { + console.error(`max-port must be a number less than ${DEFAULT_MAX_PORT}`); + process.exit(); +} else if (minPort >= maxPort) { + console.error(`min-port must be less than min-port`); + process.exit(); +} + +const server = CreateServer({ + maxTcpSockets: maxSockets, + secure, + domain, + minPort, + maxPort, +}); + +server.listen(port, address, () => { + debug('server listening on port: %d', server.address().port); +}); + +process.on('SIGINT', () => { + process.exit(); +}); + +process.on('SIGTERM', () => { + process.exit(); +}); + +process.on('uncaughtException', (err) => { + log.error(err); +}); + +process.on('unhandledRejection', (reason) => { + log.error(reason); +}); diff --git a/lib/ClientManager.js b/lib/ClientManager.js index e1a7838..ec48abe 100644 --- a/lib/ClientManager.js +++ b/lib/ClientManager.js @@ -8,89 +8,93 @@ import TunnelAgent from './TunnelAgent'; // // A client is a "user session" established to service a remote localtunnel client class ClientManager { - constructor(opt) { - this.opt = opt || {}; + constructor(opt) { + this.opt = opt || {}; - // id -> client instance - this.clients = new Map(); + // id -> client instance + this.clients = new Map(); - // statistics - this.stats = { - tunnels: 0 - }; + // statistics + this.stats = { + tunnels: 0 + }; - this.debug = Debug('lt:ClientManager'); + this.debug = Debug('lt:ClientManager'); - // This is totally wrong :facepalm: this needs to be per-client... - this.graceTimeout = null; + // This is totally wrong :facepalm: this needs to be per-client... + this.graceTimeout = null; + } + + // 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) { + const clients = this.clients; + const stats = this.stats; + + // can't ask for id already is use + if (clients[id]) { + id = hri.random(); } - // 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) { - const clients = this.clients; - const stats = this.stats; + const maxSockets = this.opt.maxTcpSockets; + const minPort = this.opt.minPort; + const maxPort = this.opt.maxPort; + const agent = new TunnelAgent({ + clientId: id, + maxSockets: 10, + minPort, + maxPort + }); - // can't ask for id already is use - if (clients[id]) { - id = hri.random(); - } + const client = new Client({ + id, + agent, + }); - const maxSockets = this.opt.max_tcp_sockets; - const agent = new TunnelAgent({ - clientId: id, - maxSockets: 10, - }); + // add to clients map immediately + // avoiding races with other clients requesting same id + clients[id] = client; - const client = new Client({ - id, - agent, - }); + client.once('close', () => { + this.removeClient(id); + }); - // add to clients map immediately - // avoiding races with other clients requesting same id - clients[id] = client; - - client.once('close', () => { - this.removeClient(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; - } + // 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, + }; } - - removeClient(id) { - this.debug('removing client: %s', id); - const client = this.clients[id]; - if (!client) { - return; - } - --this.stats.tunnels; - delete this.clients[id]; - client.close(); + catch (err) { + this.removeClient(id); + // rethrow error for upstream to handle + throw err; } + } - hasClient(id) { - return !!this.clients[id]; + removeClient(id) { + this.debug('removing client: %s', id); + const client = this.clients[id]; + if (!client) { + return; } + --this.stats.tunnels; + delete this.clients[id]; + client.close(); + } - getClient(id) { - return this.clients[id]; - } + hasClient(id) { + return !!this.clients[id]; + } + + getClient(id) { + return this.clients[id]; + } } export default ClientManager; diff --git a/lib/TunnelAgent.js b/lib/TunnelAgent.js index efc2231..588309c 100644 --- a/lib/TunnelAgent.js +++ b/lib/TunnelAgent.js @@ -1,8 +1,8 @@ import { Agent } from 'http'; import net from 'net'; -import assert from 'assert'; import log from 'book'; import Debug from 'debug'; +import { getPort } from 'portfinder'; const DEFAULT_MAX_SOCKETS = 10; @@ -10,166 +10,175 @@ const DEFAULT_MAX_SOCKETS = 10; // 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, + 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[${options.clientId}]`); + + // track maximum allowed sockets + this.connectedSockets = 0; + this.maxTcpSockets = options.maxTcpSockets || DEFAULT_MAX_SOCKETS; + + // Port range + this.minPort = options.minPort || 8000; + this.maxPort = options.maxPort || 65535; + + // new tcp server to service requests for this client + this.server = net.createServer(); + + // flag to avoid double starts + this.started = false; + this.closed = false; + } + + stats() { + return { + connectedSockets: this.connectedSockets, + }; + } + + 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) => { + // These errors happen from killed connections, we don't worry about them + if (err.code == 'ECONNRESET' || err.code == 'ETIMEDOUT') { + return; + } + log.error(err); + }); + + return new Promise((resolve, reject) => { + getPort({ + port: this.minPort, // minimum port + stopPort: this.maxPort // maximum port + }, (err, port) => { + if (err) { + reject(err); + } + server.listen(port, () => { + resolve({ + // port for lt client tcp connections + port, + }); }); + }); + }); + } - // sockets we can hand out via createConnection - this.availableSockets = []; + _onClose() { + this.closed = true; + this.debug('closed tcp socket'); + // flush any waiting connections + for (const conn of this.waitingCreateConn) { + conn(new Error('closed'), null); + } + this.waitingCreateConn = []; + this.emit('end'); + } - // 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[${options.clientId}]`); - - // track maximum allowed sockets - this.connectedSockets = 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; - this.closed = false; + // new socket connection from client for tunneling requests to client + _onConnection(socket) { + // no more socket connections allowed + if (this.connectedSockets >= this.maxTcpSockets) { + this.debug('no more sockets allowed'); + socket.destroy(); + return false; } - stats() { - return { - connectedSockets: this.connectedSockets, - }; + socket.once('close', (hadError) => { + this.debug('closed socket (error: %s)', hadError); + this.connectedSockets -= 1; + // remove the socket from available list + const idx = this.availableSockets.indexOf(socket); + if (idx >= 0) { + this.availableSockets.splice(idx, 1); + } + + this.debug('connected sockets: %s', this.connectedSockets); + if (this.connectedSockets <= 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(); + }); + + if (this.connectedSockets === 0) { + this.emit('online'); } - listen() { - const server = this.server; - if (this.started) { - throw new Error('already started'); - } - this.started = true; + this.connectedSockets += 1; + this.debug('new connection from: %s:%s', socket.address().address, socket.address().port); - server.on('close', this._onClose.bind(this)); - server.on('connection', this._onConnection.bind(this)); - server.on('error', (err) => { - // These errors happen from killed connections, we don't worry about them - 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, - }); - }); - }); + // if there are queued callbacks, give this socket now and don't queue into available + const fn = this.waitingCreateConn.shift(); + if (fn) { + this.debug('giving socket to queued conn request'); + setTimeout(() => { + fn(null, socket); + }, 0); + return; } - _onClose() { - this.closed = true; - this.debug('closed tcp socket'); - // flush any waiting connections - for (const conn of this.waitingCreateConn) { - conn(new Error('closed'), null); - } - this.waitingCreateConn = []; - this.emit('end'); + // make socket available for those waiting on sockets + this.availableSockets.push(socket); + } + + // fetch a socket from the available socket pool for the agent + // if no socket is available, queue + // cb(err, socket) + createConnection(options, cb) { + if (this.closed) { + cb(new Error('closed')); + return; } - // new socket connection from client for tunneling requests to client - _onConnection(socket) { - // no more socket connections allowed - if (this.connectedSockets >= this.maxTcpSockets) { - this.debug('no more sockets allowed'); - socket.destroy(); - return false; - } + this.debug('create connection'); - socket.once('close', (hadError) => { - this.debug('closed socket (error: %s)', hadError); - this.connectedSockets -= 1; - // remove the socket from available list - const idx = this.availableSockets.indexOf(socket); - if (idx >= 0) { - this.availableSockets.splice(idx, 1); - } + // socket is a tcp connection back to the user hosting the site + const sock = this.availableSockets.shift(); - this.debug('connected sockets: %s', this.connectedSockets); - if (this.connectedSockets <= 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(); - }); - - if (this.connectedSockets === 0) { - this.emit('online'); - } - - this.connectedSockets += 1; - this.debug('new connection from: %s:%s', socket.address().address, socket.address().port); - - // if there are queued callbacks, give this socket now and don't queue into available - const fn = this.waitingCreateConn.shift(); - if (fn) { - this.debug('giving socket to queued conn request'); - setTimeout(() => { - fn(null, socket); - }, 0); - return; - } - - // make socket available for those waiting on sockets - this.availableSockets.push(socket); + // no available sockets + // wait until we have one + if (!sock) { + this.waitingCreateConn.push(cb); + this.debug('waiting connected: %s', this.connectedSockets); + this.debug('waiting available: %s', this.availableSockets.length); + return; } - // fetch a socket from the available socket pool for the agent - // if no socket is available, queue - // cb(err, socket) - createConnection(options, cb) { - if (this.closed) { - cb(new Error('closed')); - return; - } + this.debug('socket given'); + cb(null, sock); + } - this.debug('create connection'); - - // 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 connected: %s', this.connectedSockets); - this.debug('waiting available: %s', this.availableSockets.length); - return; - } - - this.debug('socket given'); - cb(null, sock); - } - - destroy() { - this.server.close(); - super.destroy(); - } + destroy() { + this.server.close(); + super.destroy(); + } } export default TunnelAgent; diff --git a/package.json b/package.json index 0693b8d..0f960fe 100644 --- a/package.json +++ b/package.json @@ -16,9 +16,10 @@ "koa": "2.5.1", "koa-router": "7.4.0", "localenv": "0.2.2", - "optimist": "0.6.1", + "portfinder": "1.0.28", "pump": "3.0.0", - "tldjs": "2.3.1" + "tldjs": "2.3.1", + "yargs": "16.2.0" }, "devDependencies": { "mocha": "5.1.1", @@ -28,7 +29,7 @@ }, "scripts": { "test": "mocha --check-leaks --require esm './**/*.test.js'", - "start": "./bin/server", - "dev": "node-dev bin/server --port 3000" + "start": "node -r esm ./bin/server --port 3000", + "dev": "node-dev -r esm bin/server --port 3000 --domain http://localhost:3000" } -} +} \ No newline at end of file diff --git a/server.js b/server.js index 21a4af6..87f0fe8 100644 --- a/server.js +++ b/server.js @@ -1,4 +1,3 @@ -import log from 'book'; import Koa from 'koa'; import tldjs from 'tldjs'; import Debug from 'debug'; @@ -10,159 +9,159 @@ import ClientManager from './lib/ClientManager'; const debug = Debug('localtunnel:server'); -export default function(opt) { - opt = opt || {}; +export default function (opt) { + opt = opt || {}; - const validHosts = (opt.domain) ? [opt.domain] : undefined; - const myTldjs = tldjs.fromUserSettings({ validHosts }); - const landingPage = opt.landing || 'https://localtunnel.github.io/www/'; + 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); + function GetClientIdFromHostname(hostname) { + return myTldjs.getSubdomain(hostname); + } + + const manager = new ClientManager(opt); + + const schema = opt.secure ? 'https' : 'http'; + + const app = new Koa(); + const router = new Router(); + + router.get('/api/status', async (ctx, next) => { + const stats = manager.stats; + ctx.body = { + tunnels: stats.tunnels, + mem: process.memoryUsage(), + }; + }); + + router.get('/api/tunnels/:id/status', async (ctx, next) => { + const clientId = ctx.params.id; + const client = manager.getClient(clientId); + if (!client) { + ctx.throw(404); + return; } - const manager = new ClientManager(opt); + const stats = client.stats(); + ctx.body = { + connected_sockets: stats.connectedSockets, + }; + }); - const schema = opt.secure ? 'https' : 'http'; + app.use(router.routes()); + app.use(router.allowedMethods()); - const app = new Koa(); - const router = new Router(); + // root endpoint + app.use(async (ctx, next) => { + const path = ctx.request.path; - router.get('/api/status', async (ctx, next) => { - const stats = manager.stats; - ctx.body = { - tunnels: stats.tunnels, - mem: process.memoryUsage(), - }; - }); + // skip anything not on the root path + if (path !== '/') { + await next(); + return; + } - router.get('/api/tunnels/:id/status', async (ctx, next) => { - const clientId = ctx.params.id; - const client = manager.getClient(clientId); - if (!client) { - ctx.throw(404); - return; - } + const isNewClientRequest = ctx.query['new'] !== undefined; + if (isNewClientRequest) { + const reqId = hri.random(); + debug('making new client with id %s', reqId); + const info = await manager.newClient(reqId); - const stats = client.stats(); - ctx.body = { - connected_sockets: stats.connectedSockets, - }; - }); + const url = schema + '://' + info.id + '.' + ctx.request.host; + info.url = url; + ctx.body = info; + return; + } - app.use(router.routes()); - app.use(router.allowedMethods()); + // no new client request, send to landing page + ctx.redirect(landingPage); + }); - // root endpoint - app.use(async (ctx, next) => { - const path = ctx.request.path; + // anything after the / path is a request for a specific client name + // This is a backwards compat feature + app.use(async (ctx, next) => { + const parts = ctx.request.path.split('/'); - // skip anything not on the root path - if (path !== '/') { - await next(); - return; - } + // any request with several layers of paths is not allowed + // rejects /foo/bar + // allow /foo + if (parts.length !== 2) { + await next(); + return; + } - const isNewClientRequest = ctx.query['new'] !== undefined; - if (isNewClientRequest) { - const reqId = hri.random(); - debug('making new client with id %s', reqId); - const info = await manager.newClient(reqId); + const reqId = parts[1]; - const url = schema + '://' + info.id + '.' + ctx.request.host; - info.url = url; - ctx.body = info; - return; - } + // limit requested hostnames to 63 characters + 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 = { + message: msg, + }; + return; + } - // no new client request, send to landing page - ctx.redirect(landingPage); - }); + debug('making new client with id %s', reqId); + const info = await manager.newClient(reqId); - // anything after the / path is a request for a specific client name - // This is a backwards compat feature - app.use(async (ctx, next) => { - const parts = ctx.request.path.split('/'); + const url = schema + '://' + info.id + '.' + ctx.request.host; + info.url = url; + ctx.body = info; + return; + }); - // any request with several layers of paths is not allowed - // rejects /foo/bar - // allow /foo - if (parts.length !== 2) { - await next(); - return; - } + const server = http.createServer(); - const reqId = parts[1]; + const appCallback = app.callback(); - // limit requested hostnames to 63 characters - 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 = { - message: msg, - }; - return; - } + server.on('request', (req, res) => { + // without a hostname, we won't know who the request is for + const hostname = req.headers.host; + if (!hostname) { + res.statusCode = 400; + res.end('Host header is required'); + return; + } - debug('making new client with id %s', reqId); - const info = await manager.newClient(reqId); + const clientId = GetClientIdFromHostname(hostname); + if (!clientId) { + appCallback(req, res); + return; + } - const url = schema + '://' + info.id + '.' + ctx.request.host; - info.url = url; - ctx.body = info; - return; - }); + const client = manager.getClient(clientId); + if (!client) { + res.statusCode = 404; + res.end('404'); + return; + } - const server = http.createServer(); + client.handleRequest(req, res); + }); - const appCallback = app.callback(); + server.on('upgrade', (req, socket, head) => { + const hostname = req.headers.host; + if (!hostname) { + socket.destroy(); + return; + } - server.on('request', (req, res) => { - // without a hostname, we won't know who the request is for - const hostname = req.headers.host; - if (!hostname) { - res.statusCode = 400; - res.end('Host header is required'); - return; - } + const clientId = GetClientIdFromHostname(hostname); + if (!clientId) { + socket.destroy(); + return; + } - const clientId = GetClientIdFromHostname(hostname); - if (!clientId) { - appCallback(req, res); - return; - } + const client = manager.getClient(clientId); + if (!client) { + socket.destroy(); + return; + } - const client = manager.getClient(clientId); - if (!client) { - res.statusCode = 404; - res.end('404'); - return; - } + client.handleUpgrade(req, socket); + }); - client.handleRequest(req, res); - }); - - server.on('upgrade', (req, socket, head) => { - const hostname = req.headers.host; - if (!hostname) { - socket.destroy(); - return; - } - - const clientId = GetClientIdFromHostname(hostname); - if (!clientId) { - socket.destroy(); - return; - } - - const client = manager.getClient(clientId); - if (!client) { - socket.destroy(); - return; - } - - client.handleUpgrade(req, socket); - }); - - return server; + return server; }; diff --git a/yarn.lock b/yarn.lock index 9ad744b..a90d145 100644 --- a/yarn.lock +++ b/yarn.lock @@ -13,10 +13,22 @@ ansi-regex@^2.0.0: version "2.1.1" resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-2.1.1.tgz#c3b33ab5ee360d86e0e628f0468ae7ef27d654df" +ansi-regex@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.0.tgz#388539f55179bf39339c81af30a654d69f87cb75" + integrity sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg== + ansi-styles@^2.2.1: version "2.2.1" resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-2.2.1.tgz#b432dd3358b634cf75e1e4664368240533c1ddbe" +ansi-styles@^4.0.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937" + integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== + dependencies: + color-convert "^2.0.1" + ansicolors@~0.2.1: version "0.2.1" resolved "https://registry.yarnpkg.com/ansicolors/-/ansicolors-0.2.1.tgz#be089599097b74a5c9c4a84a0cdbcdb62bd87aef" @@ -33,6 +45,13 @@ async-limiter@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/async-limiter/-/async-limiter-1.0.0.tgz#78faed8c3d074ab81f22b4e985d79e8738f720f8" +async@^2.6.2: + version "2.6.3" + resolved "https://registry.yarnpkg.com/async/-/async-2.6.3.tgz#d72625e2344a3656e3a3ad4fa749fa83299d82ff" + integrity sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg== + dependencies: + lodash "^4.17.14" + asynckit@^0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" @@ -103,10 +122,31 @@ cli-usage@^0.1.1: marked "^0.3.12" marked-terminal "^2.0.0" +cliui@^7.0.2: + version "7.0.4" + resolved "https://registry.yarnpkg.com/cliui/-/cliui-7.0.4.tgz#a0265ee655476fc807aea9df3df8df7783808b4f" + integrity sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ== + dependencies: + string-width "^4.2.0" + strip-ansi "^6.0.0" + wrap-ansi "^7.0.0" + co@^4.6.0: version "4.6.0" resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184" +color-convert@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3" + integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ== + dependencies: + color-name "~1.1.4" + +color-name@~1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" + integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== + colors@1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/colors/-/colors-1.0.3.tgz#0433f44d809680fdeb60ed260f1b0c262e82a40b" @@ -179,6 +219,13 @@ debug@*, debug@3.1.0, debug@^3.1.0: dependencies: ms "2.0.0" +debug@^3.1.1: + version "3.2.7" + resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.7.tgz#72580b7e9145fb39b6676f9c5e5fb100b934179a" + integrity sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ== + dependencies: + ms "^2.1.1" + decamelize@^1.1.2: version "1.2.0" resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" @@ -217,6 +264,11 @@ ee-first@1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" +emoji-regex@^8.0.0: + version "8.0.0" + resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" + integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== + end-of-stream@^1.1.0: version "1.4.1" resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.1.tgz#ed29634d19baba463b6ce6b80a37213eab71ec43" @@ -239,6 +291,11 @@ error-stack-parser@1.3.6: dependencies: stackframe "^0.3.1" +escalade@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40" + integrity sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw== + escape-html@~1.0.1: version "1.0.3" resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" @@ -296,6 +353,11 @@ fs.realpath@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" +get-caller-file@^2.0.5: + version "2.0.5" + resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" + integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== + get-stdin@^4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/get-stdin/-/get-stdin-4.0.1.tgz#b968c6b0a04384324902e8bf1a5df32579a450fe" @@ -400,6 +462,11 @@ is-finite@^1.0.0: dependencies: number-is-nan "^1.0.0" +is-fullwidth-code-point@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d" + integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg== + is-generator-function@^1.0.3: version "1.0.7" resolved "https://registry.yarnpkg.com/is-generator-function/-/is-generator-function-1.0.7.tgz#d2132e529bb0000a7f80794d4bdf5cd5e5813522" @@ -586,6 +653,11 @@ 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.17.14: + version "4.17.20" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.20.tgz#b44a9b6297bcb698f1c51a3545a2b3b368d59c52" + integrity sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA== + loud-rejection@^1.0.0: version "1.6.0" resolved "https://registry.yarnpkg.com/loud-rejection/-/loud-rejection-1.6.0.tgz#5b46f80147edee578870f086d04821cf998e551f" @@ -662,9 +734,10 @@ minimist@^1.1.1, minimist@^1.1.3: version "1.2.0" resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284" -minimist@~0.0.1: - version "0.0.10" - resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.10.tgz#de3f98543dbf96082be48ad1a0c7cda836301dcf" +minimist@^1.2.5: + version "1.2.5" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602" + integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw== mkdirp@0.5.1: version "0.5.1" @@ -672,6 +745,13 @@ mkdirp@0.5.1: dependencies: minimist "0.0.8" +mkdirp@^0.5.5: + version "0.5.5" + resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.5.tgz#d91cefd62d1436ca0f41620e251288d420099def" + integrity sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ== + dependencies: + minimist "^1.2.5" + mocha@5.1.1: version "5.1.1" resolved "https://registry.yarnpkg.com/mocha/-/mocha-5.1.1.tgz#b774c75609dac05eb48f4d9ba1d827b97fde8a7b" @@ -692,6 +772,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" @@ -766,13 +851,6 @@ only@0.0.2: version "0.0.2" resolved "https://registry.yarnpkg.com/only/-/only-0.0.2.tgz#2afde84d03e50b9a8edc444e30610a70295edfb4" -optimist@0.6.1: - version "0.6.1" - resolved "https://registry.yarnpkg.com/optimist/-/optimist-0.6.1.tgz#da3ea74686fa21a19a111c326e90eb15a0196686" - dependencies: - minimist "~0.0.1" - wordwrap "~0.0.2" - parse-json@^2.2.0: version "2.2.0" resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-2.2.0.tgz#f480f40434ef80741f8469099f8dea18f55a4dc9" @@ -825,6 +903,15 @@ pinkie@^2.0.0: version "2.0.4" resolved "https://registry.yarnpkg.com/pinkie/-/pinkie-2.0.4.tgz#72556b80cfa0d48a974e80e77248e80ed4f7f870" +portfinder@1.0.28: + version "1.0.28" + resolved "https://registry.yarnpkg.com/portfinder/-/portfinder-1.0.28.tgz#67c4622852bd5374dd1dd900f779f53462fac778" + integrity sha512-Se+2isanIcEqf2XMHjyUKskczxbPH7dQnlMjXX6+dybayyHvAf/TCgyMRlzf/B6QDhAEFOGes0pzRo3by4AbMA== + dependencies: + async "^2.6.2" + debug "^3.1.1" + mkdirp "^0.5.5" + process-nextick-args@~2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.0.tgz#a37d732f4271b4ab1ad070d35508e8290788ffaa" @@ -890,6 +977,11 @@ repeating@^2.0.0: dependencies: is-finite "^1.0.0" +require-directory@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" + integrity sha1-jGStX9MNqxyXbiNE/+f3kqam30I= + resolve@^1.0.0: version "1.7.1" resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.7.1.tgz#aadd656374fd298aee895bc026b8297418677fd3" @@ -946,6 +1038,15 @@ stackframe@^0.3.1: version "1.5.0" resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c" +string-width@^4.1.0, string-width@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.0.tgz#952182c46cc7b2c313d1596e623992bd163b72b5" + integrity sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.0" + string_decoder@~1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8" @@ -958,6 +1059,13 @@ strip-ansi@^3.0.0: dependencies: ansi-regex "^2.0.0" +strip-ansi@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.0.tgz#0b1571dd7669ccd4f3e06e14ef1eed26225ae532" + integrity sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w== + dependencies: + ansi-regex "^5.0.0" + strip-bom@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-2.0.0.tgz#6219a85616520491f35788bdbf1447a99c7e6b0e" @@ -1044,9 +1152,14 @@ which@^1.0.5: dependencies: isexe "^2.0.0" -wordwrap@~0.0.2: - version "0.0.3" - resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-0.0.3.tgz#a3d5da6cd5c0bc0008d37234bbaf1bed63059107" +wrap-ansi@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" + integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" wrappy@1: version "1.0.2" @@ -1064,3 +1177,26 @@ xtend@~2.0.6: dependencies: is-object "~0.1.2" object-keys "~0.2.0" + +y18n@^5.0.5: + version "5.0.5" + resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.5.tgz#8769ec08d03b1ea2df2500acef561743bbb9ab18" + integrity sha512-hsRUr4FFrvhhRH12wOdfs38Gy7k2FFzB9qgN9v3aLykRq0dRcdcpz5C9FxdS2NuhOrI/628b/KSTJ3rwHysYSg== + +yargs-parser@^20.2.2: + version "20.2.4" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.4.tgz#b42890f14566796f85ae8e3a25290d205f154a54" + integrity sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA== + +yargs@16.2.0: + version "16.2.0" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-16.2.0.tgz#1c82bf0f6b6a66eafce7ef30e376f49a12477f66" + integrity sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw== + dependencies: + cliui "^7.0.2" + escalade "^3.1.1" + get-caller-file "^2.0.5" + require-directory "^2.1.1" + string-width "^4.2.0" + y18n "^5.0.5" + yargs-parser "^20.2.2"