Support TCP port range limitation #148
-68
@@ -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
|
|
||||||
|
|
||||||
Executable
+94
@@ -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 <command> [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);
|
||||||
|
});
|
||||||
+72
-68
@@ -8,89 +8,93 @@ import TunnelAgent from './TunnelAgent';
|
|||||||
//
|
//
|
||||||
// A client is a "user session" established to service a remote localtunnel client
|
// A client is a "user session" established to service a remote localtunnel client
|
||||||
class ClientManager {
|
class ClientManager {
|
||||||
constructor(opt) {
|
constructor(opt) {
|
||||||
this.opt = opt || {};
|
this.opt = opt || {};
|
||||||
|
|
||||||
// id -> client instance
|
// id -> client instance
|
||||||
this.clients = new Map();
|
this.clients = new Map();
|
||||||
|
|
||||||
// statistics
|
// statistics
|
||||||
this.stats = {
|
this.stats = {
|
||||||
tunnels: 0
|
tunnels: 0
|
||||||
};
|
};
|
||||||
|
|
||||||
this.debug = Debug('lt:ClientManager');
|
this.debug = Debug('lt:ClientManager');
|
||||||
|
|
||||||
// This is totally wrong :facepalm: this needs to be per-client...
|
// This is totally wrong :facepalm: this needs to be per-client...
|
||||||
this.graceTimeout = null;
|
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`
|
const maxSockets = this.opt.maxTcpSockets;
|
||||||
// if the id is already used, a random id is assigned
|
const minPort = this.opt.minPort;
|
||||||
// if the tunnel could not be created, throws an error
|
const maxPort = this.opt.maxPort;
|
||||||
async newClient(id) {
|
const agent = new TunnelAgent({
|
||||||
const clients = this.clients;
|
clientId: id,
|
||||||
const stats = this.stats;
|
maxSockets: 10,
|
||||||
|
minPort,
|
||||||
|
maxPort
|
||||||
|
});
|
||||||
|
|
||||||
// can't ask for id already is use
|
const client = new Client({
|
||||||
if (clients[id]) {
|
id,
|
||||||
id = hri.random();
|
agent,
|
||||||
}
|
});
|
||||||
|
|
||||||
const maxSockets = this.opt.max_tcp_sockets;
|
// add to clients map immediately
|
||||||
const agent = new TunnelAgent({
|
// avoiding races with other clients requesting same id
|
||||||
clientId: id,
|
clients[id] = client;
|
||||||
maxSockets: 10,
|
|
||||||
});
|
|
||||||
|
|
||||||
const client = new Client({
|
client.once('close', () => {
|
||||||
id,
|
this.removeClient(id);
|
||||||
agent,
|
});
|
||||||
});
|
|
||||||
|
|
||||||
// add to clients map immediately
|
// try/catch used here to remove client id
|
||||||
// avoiding races with other clients requesting same id
|
try {
|
||||||
clients[id] = client;
|
const info = await agent.listen();
|
||||||
|
++stats.tunnels;
|
||||||
client.once('close', () => {
|
return {
|
||||||
this.removeClient(id);
|
id: id,
|
||||||
});
|
port: info.port,
|
||||||
|
max_conn_count: maxSockets,
|
||||||
// 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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
catch (err) {
|
||||||
removeClient(id) {
|
this.removeClient(id);
|
||||||
this.debug('removing client: %s', id);
|
// rethrow error for upstream to handle
|
||||||
const client = this.clients[id];
|
throw err;
|
||||||
if (!client) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
--this.stats.tunnels;
|
|
||||||
delete this.clients[id];
|
|
||||||
client.close();
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
hasClient(id) {
|
removeClient(id) {
|
||||||
return !!this.clients[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) {
|
hasClient(id) {
|
||||||
return this.clients[id];
|
return !!this.clients[id];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getClient(id) {
|
||||||
|
return this.clients[id];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ClientManager;
|
export default ClientManager;
|
||||||
|
|||||||
+153
-144
@@ -1,8 +1,8 @@
|
|||||||
import { Agent } from 'http';
|
import { Agent } from 'http';
|
||||||
import net from 'net';
|
import net from 'net';
|
||||||
import assert from 'assert';
|
|
||||||
import log from 'book';
|
import log from 'book';
|
||||||
import Debug from 'debug';
|
import Debug from 'debug';
|
||||||
|
import { getPort } from 'portfinder';
|
||||||
|
|
||||||
const DEFAULT_MAX_SOCKETS = 10;
|
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
|
// 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
|
// service http requests. This agent is usable wherever one can use an http.Agent
|
||||||
class TunnelAgent extends Agent {
|
class TunnelAgent extends Agent {
|
||||||
constructor(options = {}) {
|
constructor(options = {}) {
|
||||||
super({
|
super({
|
||||||
keepAlive: true,
|
keepAlive: true,
|
||||||
// only allow keepalive to hold on to one socket
|
// 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
|
// this prevents it from holding on to all the sockets so they can be used for upgrades
|
||||||
maxFreeSockets: 1,
|
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
|
_onClose() {
|
||||||
this.availableSockets = [];
|
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
|
// new socket connection from client for tunneling requests to client
|
||||||
// once a socket is available it is handed out to the next callback
|
_onConnection(socket) {
|
||||||
this.waitingCreateConn = [];
|
// no more socket connections allowed
|
||||||
|
if (this.connectedSockets >= this.maxTcpSockets) {
|
||||||
this.debug = Debug(`lt:TunnelAgent[${options.clientId}]`);
|
this.debug('no more sockets allowed');
|
||||||
|
socket.destroy();
|
||||||
// track maximum allowed sockets
|
return false;
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
stats() {
|
socket.once('close', (hadError) => {
|
||||||
return {
|
this.debug('closed socket (error: %s)', hadError);
|
||||||
connectedSockets: this.connectedSockets,
|
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() {
|
this.connectedSockets += 1;
|
||||||
const server = this.server;
|
this.debug('new connection from: %s:%s', socket.address().address, socket.address().port);
|
||||||
if (this.started) {
|
|
||||||
throw new Error('already started');
|
|
||||||
}
|
|
||||||
this.started = true;
|
|
||||||
|
|
||||||
server.on('close', this._onClose.bind(this));
|
// if there are queued callbacks, give this socket now and don't queue into available
|
||||||
server.on('connection', this._onConnection.bind(this));
|
const fn = this.waitingCreateConn.shift();
|
||||||
server.on('error', (err) => {
|
if (fn) {
|
||||||
// These errors happen from killed connections, we don't worry about them
|
this.debug('giving socket to queued conn request');
|
||||||
if (err.code == 'ECONNRESET' || err.code == 'ETIMEDOUT') {
|
setTimeout(() => {
|
||||||
return;
|
fn(null, socket);
|
||||||
}
|
}, 0);
|
||||||
log.error(err);
|
return;
|
||||||
});
|
|
||||||
|
|
||||||
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() {
|
// make socket available for those waiting on sockets
|
||||||
this.closed = true;
|
this.availableSockets.push(socket);
|
||||||
this.debug('closed tcp socket');
|
}
|
||||||
// flush any waiting connections
|
|
||||||
for (const conn of this.waitingCreateConn) {
|
// fetch a socket from the available socket pool for the agent
|
||||||
conn(new Error('closed'), null);
|
// if no socket is available, queue
|
||||||
}
|
// cb(err, socket)
|
||||||
this.waitingCreateConn = [];
|
createConnection(options, cb) {
|
||||||
this.emit('end');
|
if (this.closed) {
|
||||||
|
cb(new Error('closed'));
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// new socket connection from client for tunneling requests to client
|
this.debug('create connection');
|
||||||
_onConnection(socket) {
|
|
||||||
// no more socket connections allowed
|
|
||||||
if (this.connectedSockets >= this.maxTcpSockets) {
|
|
||||||
this.debug('no more sockets allowed');
|
|
||||||
socket.destroy();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
socket.once('close', (hadError) => {
|
// socket is a tcp connection back to the user hosting the site
|
||||||
this.debug('closed socket (error: %s)', hadError);
|
const sock = this.availableSockets.shift();
|
||||||
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);
|
// no available sockets
|
||||||
if (this.connectedSockets <= 0) {
|
// wait until we have one
|
||||||
this.debug('all sockets disconnected');
|
if (!sock) {
|
||||||
this.emit('offline');
|
this.waitingCreateConn.push(cb);
|
||||||
}
|
this.debug('waiting connected: %s', this.connectedSockets);
|
||||||
});
|
this.debug('waiting available: %s', this.availableSockets.length);
|
||||||
|
return;
|
||||||
// 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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// fetch a socket from the available socket pool for the agent
|
this.debug('socket given');
|
||||||
// if no socket is available, queue
|
cb(null, sock);
|
||||||
// cb(err, socket)
|
}
|
||||||
createConnection(options, cb) {
|
|
||||||
if (this.closed) {
|
|
||||||
cb(new Error('closed'));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.debug('create connection');
|
destroy() {
|
||||||
|
this.server.close();
|
||||||
// socket is a tcp connection back to the user hosting the site
|
super.destroy();
|
||||||
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();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default TunnelAgent;
|
export default TunnelAgent;
|
||||||
|
|||||||
+6
-5
@@ -16,9 +16,10 @@
|
|||||||
"koa": "2.5.1",
|
"koa": "2.5.1",
|
||||||
"koa-router": "7.4.0",
|
"koa-router": "7.4.0",
|
||||||
"localenv": "0.2.2",
|
"localenv": "0.2.2",
|
||||||
"optimist": "0.6.1",
|
"portfinder": "1.0.28",
|
||||||
"pump": "3.0.0",
|
"pump": "3.0.0",
|
||||||
"tldjs": "2.3.1"
|
"tldjs": "2.3.1",
|
||||||
|
"yargs": "16.2.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"mocha": "5.1.1",
|
"mocha": "5.1.1",
|
||||||
@@ -28,7 +29,7 @@
|
|||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "mocha --check-leaks --require esm './**/*.test.js'",
|
"test": "mocha --check-leaks --require esm './**/*.test.js'",
|
||||||
"start": "./bin/server",
|
"start": "node -r esm ./bin/server --port 3000",
|
||||||
"dev": "node-dev bin/server --port 3000"
|
"dev": "node-dev -r esm bin/server --port 3000 --domain http://localhost:3000"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
import log from 'book';
|
|
||||||
import Koa from 'koa';
|
import Koa from 'koa';
|
||||||
import tldjs from 'tldjs';
|
import tldjs from 'tldjs';
|
||||||
import Debug from 'debug';
|
import Debug from 'debug';
|
||||||
@@ -10,159 +9,159 @@ import ClientManager from './lib/ClientManager';
|
|||||||
|
|
||||||
const debug = Debug('localtunnel:server');
|
const debug = Debug('localtunnel:server');
|
||||||
|
|
||||||
export default function(opt) {
|
export default function (opt) {
|
||||||
opt = opt || {};
|
opt = opt || {};
|
||||||
|
|
||||||
const validHosts = (opt.domain) ? [opt.domain] : undefined;
|
const validHosts = (opt.domain) ? [opt.domain] : undefined;
|
||||||
const myTldjs = tldjs.fromUserSettings({ validHosts });
|
const myTldjs = tldjs.fromUserSettings({ validHosts });
|
||||||
const landingPage = opt.landing || 'https://localtunnel.github.io/www/';
|
const landingPage = opt.landing || 'https://localtunnel.github.io/www/';
|
||||||
|
|
||||||
function GetClientIdFromHostname(hostname) {
|
function GetClientIdFromHostname(hostname) {
|
||||||
return myTldjs.getSubdomain(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();
|
// root endpoint
|
||||||
const router = new Router();
|
app.use(async (ctx, next) => {
|
||||||
|
const path = ctx.request.path;
|
||||||
|
|
||||||
router.get('/api/status', async (ctx, next) => {
|
// skip anything not on the root path
|
||||||
const stats = manager.stats;
|
if (path !== '/') {
|
||||||
ctx.body = {
|
await next();
|
||||||
tunnels: stats.tunnels,
|
return;
|
||||||
mem: process.memoryUsage(),
|
}
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
router.get('/api/tunnels/:id/status', async (ctx, next) => {
|
const isNewClientRequest = ctx.query['new'] !== undefined;
|
||||||
const clientId = ctx.params.id;
|
if (isNewClientRequest) {
|
||||||
const client = manager.getClient(clientId);
|
const reqId = hri.random();
|
||||||
if (!client) {
|
debug('making new client with id %s', reqId);
|
||||||
ctx.throw(404);
|
const info = await manager.newClient(reqId);
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const stats = client.stats();
|
const url = schema + '://' + info.id + '.' + ctx.request.host;
|
||||||
ctx.body = {
|
info.url = url;
|
||||||
connected_sockets: stats.connectedSockets,
|
ctx.body = info;
|
||||||
};
|
return;
|
||||||
});
|
}
|
||||||
|
|
||||||
app.use(router.routes());
|
// no new client request, send to landing page
|
||||||
app.use(router.allowedMethods());
|
ctx.redirect(landingPage);
|
||||||
|
});
|
||||||
|
|
||||||
// root endpoint
|
// anything after the / path is a request for a specific client name
|
||||||
app.use(async (ctx, next) => {
|
// This is a backwards compat feature
|
||||||
const path = ctx.request.path;
|
app.use(async (ctx, next) => {
|
||||||
|
const parts = ctx.request.path.split('/');
|
||||||
|
|
||||||
// skip anything not on the root path
|
// any request with several layers of paths is not allowed
|
||||||
if (path !== '/') {
|
// rejects /foo/bar
|
||||||
await next();
|
// allow /foo
|
||||||
return;
|
if (parts.length !== 2) {
|
||||||
}
|
await next();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const isNewClientRequest = ctx.query['new'] !== undefined;
|
const reqId = parts[1];
|
||||||
if (isNewClientRequest) {
|
|
||||||
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;
|
// limit requested hostnames to 63 characters
|
||||||
info.url = url;
|
if (! /^(?:[a-z0-9][a-z0-9\-]{4,63}[a-z0-9]|[a-z0-9]{4,63})$/.test(reqId)) {
|
||||||
ctx.body = info;
|
const msg = 'Invalid subdomain. Subdomains must be lowercase and between 4 and 63 alphanumeric characters.';
|
||||||
return;
|
ctx.status = 403;
|
||||||
}
|
ctx.body = {
|
||||||
|
message: msg,
|
||||||
|
};
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// no new client request, send to landing page
|
debug('making new client with id %s', reqId);
|
||||||
ctx.redirect(landingPage);
|
const info = await manager.newClient(reqId);
|
||||||
});
|
|
||||||
|
|
||||||
// anything after the / path is a request for a specific client name
|
const url = schema + '://' + info.id + '.' + ctx.request.host;
|
||||||
// This is a backwards compat feature
|
info.url = url;
|
||||||
app.use(async (ctx, next) => {
|
ctx.body = info;
|
||||||
const parts = ctx.request.path.split('/');
|
return;
|
||||||
|
});
|
||||||
|
|
||||||
// any request with several layers of paths is not allowed
|
const server = http.createServer();
|
||||||
// rejects /foo/bar
|
|
||||||
// allow /foo
|
|
||||||
if (parts.length !== 2) {
|
|
||||||
await next();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const reqId = parts[1];
|
const appCallback = app.callback();
|
||||||
|
|
||||||
// limit requested hostnames to 63 characters
|
server.on('request', (req, res) => {
|
||||||
if (! /^(?:[a-z0-9][a-z0-9\-]{4,63}[a-z0-9]|[a-z0-9]{4,63})$/.test(reqId)) {
|
// without a hostname, we won't know who the request is for
|
||||||
const msg = 'Invalid subdomain. Subdomains must be lowercase and between 4 and 63 alphanumeric characters.';
|
const hostname = req.headers.host;
|
||||||
ctx.status = 403;
|
if (!hostname) {
|
||||||
ctx.body = {
|
res.statusCode = 400;
|
||||||
message: msg,
|
res.end('Host header is required');
|
||||||
};
|
return;
|
||||||
return;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
debug('making new client with id %s', reqId);
|
const clientId = GetClientIdFromHostname(hostname);
|
||||||
const info = await manager.newClient(reqId);
|
if (!clientId) {
|
||||||
|
appCallback(req, res);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const url = schema + '://' + info.id + '.' + ctx.request.host;
|
const client = manager.getClient(clientId);
|
||||||
info.url = url;
|
if (!client) {
|
||||||
ctx.body = info;
|
res.statusCode = 404;
|
||||||
return;
|
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) => {
|
const clientId = GetClientIdFromHostname(hostname);
|
||||||
// without a hostname, we won't know who the request is for
|
if (!clientId) {
|
||||||
const hostname = req.headers.host;
|
socket.destroy();
|
||||||
if (!hostname) {
|
return;
|
||||||
res.statusCode = 400;
|
}
|
||||||
res.end('Host header is required');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const clientId = GetClientIdFromHostname(hostname);
|
const client = manager.getClient(clientId);
|
||||||
if (!clientId) {
|
if (!client) {
|
||||||
appCallback(req, res);
|
socket.destroy();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const client = manager.getClient(clientId);
|
client.handleUpgrade(req, socket);
|
||||||
if (!client) {
|
});
|
||||||
res.statusCode = 404;
|
|
||||||
res.end('404');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
client.handleRequest(req, res);
|
return server;
|
||||||
});
|
|
||||||
|
|
||||||
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;
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -13,10 +13,22 @@ ansi-regex@^2.0.0:
|
|||||||
version "2.1.1"
|
version "2.1.1"
|
||||||
resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-2.1.1.tgz#c3b33ab5ee360d86e0e628f0468ae7ef27d654df"
|
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:
|
ansi-styles@^2.2.1:
|
||||||
version "2.2.1"
|
version "2.2.1"
|
||||||
resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-2.2.1.tgz#b432dd3358b634cf75e1e4664368240533c1ddbe"
|
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:
|
ansicolors@~0.2.1:
|
||||||
version "0.2.1"
|
version "0.2.1"
|
||||||
resolved "https://registry.yarnpkg.com/ansicolors/-/ansicolors-0.2.1.tgz#be089599097b74a5c9c4a84a0cdbcdb62bd87aef"
|
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"
|
version "1.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/async-limiter/-/async-limiter-1.0.0.tgz#78faed8c3d074ab81f22b4e985d79e8738f720f8"
|
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:
|
asynckit@^0.4.0:
|
||||||
version "0.4.0"
|
version "0.4.0"
|
||||||
resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79"
|
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 "^0.3.12"
|
||||||
marked-terminal "^2.0.0"
|
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:
|
co@^4.6.0:
|
||||||
version "4.6.0"
|
version "4.6.0"
|
||||||
resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184"
|
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==
|
||||||
|
|
||||||
[email protected]:
|
[email protected]:
|
||||||
version "1.0.3"
|
version "1.0.3"
|
||||||
resolved "https://registry.yarnpkg.com/colors/-/colors-1.0.3.tgz#0433f44d809680fdeb60ed260f1b0c262e82a40b"
|
resolved "https://registry.yarnpkg.com/colors/-/colors-1.0.3.tgz#0433f44d809680fdeb60ed260f1b0c262e82a40b"
|
||||||
@@ -179,6 +219,13 @@ debug@*, [email protected], debug@^3.1.0:
|
|||||||
dependencies:
|
dependencies:
|
||||||
ms "2.0.0"
|
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:
|
decamelize@^1.1.2:
|
||||||
version "1.2.0"
|
version "1.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290"
|
resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290"
|
||||||
@@ -217,6 +264,11 @@ [email protected]:
|
|||||||
version "1.1.1"
|
version "1.1.1"
|
||||||
resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d"
|
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:
|
end-of-stream@^1.1.0:
|
||||||
version "1.4.1"
|
version "1.4.1"
|
||||||
resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.1.tgz#ed29634d19baba463b6ce6b80a37213eab71ec43"
|
resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.1.tgz#ed29634d19baba463b6ce6b80a37213eab71ec43"
|
||||||
@@ -239,6 +291,11 @@ [email protected]:
|
|||||||
dependencies:
|
dependencies:
|
||||||
stackframe "^0.3.1"
|
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:
|
escape-html@~1.0.1:
|
||||||
version "1.0.3"
|
version "1.0.3"
|
||||||
resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988"
|
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"
|
version "1.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f"
|
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:
|
get-stdin@^4.0.1:
|
||||||
version "4.0.1"
|
version "4.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/get-stdin/-/get-stdin-4.0.1.tgz#b968c6b0a04384324902e8bf1a5df32579a450fe"
|
resolved "https://registry.yarnpkg.com/get-stdin/-/get-stdin-4.0.1.tgz#b968c6b0a04384324902e8bf1a5df32579a450fe"
|
||||||
@@ -400,6 +462,11 @@ is-finite@^1.0.0:
|
|||||||
dependencies:
|
dependencies:
|
||||||
number-is-nan "^1.0.0"
|
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:
|
is-generator-function@^1.0.3:
|
||||||
version "1.0.7"
|
version "1.0.7"
|
||||||
resolved "https://registry.yarnpkg.com/is-generator-function/-/is-generator-function-1.0.7.tgz#d2132e529bb0000a7f80794d4bdf5cd5e5813522"
|
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"
|
version "4.4.0"
|
||||||
resolved "https://registry.yarnpkg.com/lodash.toarray/-/lodash.toarray-4.4.0.tgz#24c4bfcd6b2fba38bfd0594db1179d8e9b656561"
|
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:
|
loud-rejection@^1.0.0:
|
||||||
version "1.6.0"
|
version "1.6.0"
|
||||||
resolved "https://registry.yarnpkg.com/loud-rejection/-/loud-rejection-1.6.0.tgz#5b46f80147edee578870f086d04821cf998e551f"
|
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"
|
version "1.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284"
|
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284"
|
||||||
|
|
||||||
minimist@~0.0.1:
|
minimist@^1.2.5:
|
||||||
version "0.0.10"
|
version "1.2.5"
|
||||||
resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.10.tgz#de3f98543dbf96082be48ad1a0c7cda836301dcf"
|
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602"
|
||||||
|
integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==
|
||||||
|
|
||||||
[email protected]:
|
[email protected]:
|
||||||
version "0.5.1"
|
version "0.5.1"
|
||||||
@@ -672,6 +745,13 @@ [email protected]:
|
|||||||
dependencies:
|
dependencies:
|
||||||
minimist "0.0.8"
|
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"
|
||||||
|
|
||||||
[email protected]:
|
[email protected]:
|
||||||
version "5.1.1"
|
version "5.1.1"
|
||||||
resolved "https://registry.yarnpkg.com/mocha/-/mocha-5.1.1.tgz#b774c75609dac05eb48f4d9ba1d827b97fde8a7b"
|
resolved "https://registry.yarnpkg.com/mocha/-/mocha-5.1.1.tgz#b774c75609dac05eb48f4d9ba1d827b97fde8a7b"
|
||||||
@@ -692,6 +772,11 @@ [email protected]:
|
|||||||
version "2.0.0"
|
version "2.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"
|
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==
|
||||||
|
|
||||||
[email protected]:
|
[email protected]:
|
||||||
version "0.6.1"
|
version "0.6.1"
|
||||||
resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.1.tgz#2b327184e8992101177b28563fb5e7102acd0ca9"
|
resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.1.tgz#2b327184e8992101177b28563fb5e7102acd0ca9"
|
||||||
@@ -766,13 +851,6 @@ [email protected]:
|
|||||||
version "0.0.2"
|
version "0.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/only/-/only-0.0.2.tgz#2afde84d03e50b9a8edc444e30610a70295edfb4"
|
resolved "https://registry.yarnpkg.com/only/-/only-0.0.2.tgz#2afde84d03e50b9a8edc444e30610a70295edfb4"
|
||||||
|
|
||||||
[email protected]:
|
|
||||||
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:
|
parse-json@^2.2.0:
|
||||||
version "2.2.0"
|
version "2.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-2.2.0.tgz#f480f40434ef80741f8469099f8dea18f55a4dc9"
|
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"
|
version "2.0.4"
|
||||||
resolved "https://registry.yarnpkg.com/pinkie/-/pinkie-2.0.4.tgz#72556b80cfa0d48a974e80e77248e80ed4f7f870"
|
resolved "https://registry.yarnpkg.com/pinkie/-/pinkie-2.0.4.tgz#72556b80cfa0d48a974e80e77248e80ed4f7f870"
|
||||||
|
|
||||||
|
[email protected]:
|
||||||
|
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:
|
process-nextick-args@~2.0.0:
|
||||||
version "2.0.0"
|
version "2.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.0.tgz#a37d732f4271b4ab1ad070d35508e8290788ffaa"
|
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:
|
dependencies:
|
||||||
is-finite "^1.0.0"
|
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:
|
resolve@^1.0.0:
|
||||||
version "1.7.1"
|
version "1.7.1"
|
||||||
resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.7.1.tgz#aadd656374fd298aee895bc026b8297418677fd3"
|
resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.7.1.tgz#aadd656374fd298aee895bc026b8297418677fd3"
|
||||||
@@ -946,6 +1038,15 @@ stackframe@^0.3.1:
|
|||||||
version "1.5.0"
|
version "1.5.0"
|
||||||
resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c"
|
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:
|
string_decoder@~1.1.1:
|
||||||
version "1.1.1"
|
version "1.1.1"
|
||||||
resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8"
|
resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8"
|
||||||
@@ -958,6 +1059,13 @@ strip-ansi@^3.0.0:
|
|||||||
dependencies:
|
dependencies:
|
||||||
ansi-regex "^2.0.0"
|
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:
|
strip-bom@^2.0.0:
|
||||||
version "2.0.0"
|
version "2.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-2.0.0.tgz#6219a85616520491f35788bdbf1447a99c7e6b0e"
|
resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-2.0.0.tgz#6219a85616520491f35788bdbf1447a99c7e6b0e"
|
||||||
@@ -1044,9 +1152,14 @@ which@^1.0.5:
|
|||||||
dependencies:
|
dependencies:
|
||||||
isexe "^2.0.0"
|
isexe "^2.0.0"
|
||||||
|
|
||||||
wordwrap@~0.0.2:
|
wrap-ansi@^7.0.0:
|
||||||
version "0.0.3"
|
version "7.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-0.0.3.tgz#a3d5da6cd5c0bc0008d37234bbaf1bed63059107"
|
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:
|
wrappy@1:
|
||||||
version "1.0.2"
|
version "1.0.2"
|
||||||
@@ -1064,3 +1177,26 @@ xtend@~2.0.6:
|
|||||||
dependencies:
|
dependencies:
|
||||||
is-object "~0.1.2"
|
is-object "~0.1.2"
|
||||||
object-keys "~0.2.0"
|
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==
|
||||||
|
|
||||||
|
[email protected]:
|
||||||
|
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"
|
||||||
|
|||||||
Reference in New Issue
Block a user