From 675b527833e8cdc95ddd07ab9d84e5331ed4cda8 Mon Sep 17 00:00:00 2001 From: tedspare Date: Mon, 18 Mar 2024 23:22:59 -0400 Subject: [PATCH] Migrate to Bun. Not yet tidy. --- .npmrc | 1 - .travis.yml | 4 - Dockerfile | 10 +- bin/server | 70 --- bun.lockb | Bin 0 -> 24849 bytes client/demo.ts | 22 + fly.toml | 30 ++ index.ts | 67 +++ lib/Client.js | 132 ------ lib/ClientManager.js | 96 ---- lib/TunnelAgent.js | 175 ------- lib/agent.ts | 199 ++++++++ lib/client.ts | 142 ++++++ lib/manager.ts | 102 ++++ opus.ts | 110 +++++ package.json | 19 +- server.js | 168 ------- server.ts | 178 +++++++ utils.ts | 24 + yarn.lock | 1066 ------------------------------------------ 20 files changed, 886 insertions(+), 1729 deletions(-) delete mode 100644 .npmrc delete mode 100644 .travis.yml delete mode 100755 bin/server create mode 100755 bun.lockb create mode 100644 client/demo.ts create mode 100644 fly.toml create mode 100755 index.ts delete mode 100644 lib/Client.js delete mode 100644 lib/ClientManager.js delete mode 100644 lib/TunnelAgent.js create mode 100644 lib/agent.ts create mode 100644 lib/client.ts create mode 100644 lib/manager.ts create mode 100644 opus.ts delete mode 100644 server.js create mode 100644 server.ts create mode 100644 utils.ts delete mode 100644 yarn.lock diff --git a/.npmrc b/.npmrc deleted file mode 100644 index 1dab4ed..0000000 --- a/.npmrc +++ /dev/null @@ -1 +0,0 @@ -save-exact = true diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index f94a53e..0000000 --- a/.travis.yml +++ /dev/null @@ -1,4 +0,0 @@ -language: node_js -sudo: false -node_js: - - "9.2" diff --git a/Dockerfile b/Dockerfile index e397831..cb30c94 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,15 +1,15 @@ -FROM node:10.1.0-alpine +FROM oven/bun:1 as base WORKDIR /app COPY package.json /app/ -COPY yarn.lock /app/ +COPY bun.lockb /app/ -RUN yarn install --production && yarn cache clean +RUN bun install --frozen-lockfile --production COPY . /app ENV NODE_ENV production -ENTRYPOINT ["node", "-r", "esm", "./bin/server"] +ENTRYPOINT ["bun", "run", "start"] -EXPOSE 1234 +EXPOSE 1234 1235 \ No newline at end of file diff --git a/bin/server b/bin/server deleted file mode 100755 index 9f715b0..0000000 --- a/bin/server +++ /dev/null @@ -1,70 +0,0 @@ -#!/usr/bin/env node -r esm - -import 'localenv'; -import optimist from 'optimist'; -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) { - console.log("Showing 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', () => { - console.error("SIGINT"); - process.exit(); -}); - -process.on('SIGTERM', () => { - console.warn("SIGTERM"); - // process.exit(); -}); - -process.on('uncaughtException', (err) => { - console.error("err:"); - console.error(err); -}); - -process.on('unhandledRejection', (reason, promise) => { - console.error("reason:"); - console.error(reason); -}); - -// vim: ft=javascript - diff --git a/bun.lockb b/bun.lockb new file mode 100755 index 0000000000000000000000000000000000000000..24b7f8d7c3745f4abece1a6a720b57f1620a7df7 GIT binary patch literal 24849 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

`@*Q> z6Pu3Ld#@a)_+Iu#kES`pQb!zfPCM*$)BRiI2IZw2MVbXO;f89K^l8=5u0K$F74mZ5lNFLHimU zV}uvq!AW3t-l7 eW|b_>zN<_uM4S?7K4z&NY;nc3i)lt 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 AYXATM literal 0 HcmV?d00001 diff --git a/client/demo.ts b/client/demo.ts new file mode 100644 index 0000000..fc9a8d2 --- /dev/null +++ b/client/demo.ts @@ -0,0 +1,22 @@ +import { serve } from "bun"; + +const port = 3000; + +serve({ + port, + fetch: async (req) => { + const url = new URL(req.url); + console.log(req.method, url.pathname, url.search); + + const bod = await req.text(); + bod && console.log("Body: ", bod); + + const res = new Response(`hello from localhost:${port}`, { + status: 200, + }); + + return res; + }, +}); + +console.log(`Serving on port ${port}!`); diff --git a/fly.toml b/fly.toml new file mode 100644 index 0000000..2d334c8 --- /dev/null +++ b/fly.toml @@ -0,0 +1,30 @@ +# fly.toml app configuration file generated for tunnel on 2024-03-18T21:37:28-04:00 +# +# See https://fly.io/docs/reference/configuration/ for information about how to use this file. +# + +app = 'tunnel' +primary_region = 'yul' + +[build] + +[http_service] + internal_port = 1234 + force_https = false + auto_stop_machines = true + auto_start_machines = true + min_machines_running = 0 + processes = ['app'] + +[[vm]] + memory = '1gb' + cpu_kind = 'shared' + cpus = 1 + +# [[services]] +# internal_port = 1235 +# protocol = "tcp" + +[[services.ports]] + handlers = ["tls", "http"] + port = 443 \ No newline at end of file diff --git a/index.ts b/index.ts new file mode 100755 index 0000000..fdf5002 --- /dev/null +++ b/index.ts @@ -0,0 +1,67 @@ +import "localenv"; +import optimist from "optimist"; +import Debug from "debug"; +import { CreateServer } from "./server"; +import { type AddressInfo } from "ws"; + +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) { + console.log("Showing 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() as AddressInfo).port); +}); + +process.on("SIGINT", () => { + console.error("SIGINT"); + process.exit(); +}); + +process.on("SIGTERM", () => { + console.warn("SIGTERM"); + // process.exit(); +}); + +process.on("uncaughtException", (err) => { + console.error("err:"); + console.error(err); +}); + +process.on("unhandledRejection", (reason, promise) => { + console.error("reason:"); + console.error(reason); +}); diff --git a/lib/Client.js b/lib/Client.js deleted file mode 100644 index 369eb07..0000000 --- a/lib/Client.js +++ /dev/null @@ -1,132 +0,0 @@ -import http from 'http'; -import Debug from 'debug'; -import pump from 'pump'; -import EventEmitter from 'events'; - -// A client encapsulates req/res handling using an agent -// -// If an agent is destroyed, the request handling will error -// The caller is responsible for handling a failed request -class Client extends EventEmitter { - constructor(options) { - super(); - - const agent = this.agent = options.agent; - const id = this.id = options.id; - - this.debug = Debug(`lt:Client[${this.id}]`); - - // client is given a grace period in which they can connect before they are _removed_ - this.graceTimeout = setTimeout(() => { - this.close(); - }, 1000).unref(); - - agent.on('online', () => { - this.debug('client online %s', id); - clearTimeout(this.graceTimeout); - }); - - agent.on('offline', () => { - this.debug('client offline %s', id); - - // if there was a previous timeout set, we don't want to double trigger - clearTimeout(this.graceTimeout); - - // client is given a grace period in which they can re-connect before they are _removed_ - this.graceTimeout = setTimeout(() => { - this.close(); - }, 1000).unref(); - }); - - // TODO(roman): an agent error removes the client, the user needs to re-connect? - // how does a user realize they need to re-connect vs some random client being assigned same port? - agent.once('error', (err) => { - this.close(); - }); - } - - stats() { - return this.agent.stats(); - } - - close() { - clearTimeout(this.graceTimeout); - this.agent.destroy(); - this.emit('close'); - } - - handleRequest(req, res) { - this.debug('> %s', req.url); - const opt = { - path: req.url, - agent: this.agent, - method: req.method, - headers: req.headers - }; - - const clientReq = http.request(opt, (clientRes) => { - this.debug('< %s', req.url); - // write response code and headers - res.writeHead(clientRes.statusCode, clientRes.headers); - - // using pump is deliberate - see the pump docs for why - pump(clientRes, res); - }); - - // this can happen when underlying agent produces an error - // in our case we 504 gateway error this? - // if we have already sent headers? - clientReq.once('error', (err) => { - // TODO(roman): if headers not sent - respond with gateway unavailable - }); - - // using pump is deliberate - see the pump docs for why - pump(req, clientReq); - } - - handleUpgrade(req, socket) { - this.debug('> [up] %s', req.url); - socket.once('error', (err) => { - // These client side errors can happen if the client dies while we are reading - // We don't need to surface these in our logs. - if (err.code == 'ECONNRESET' || err.code == 'ETIMEDOUT') { - return; - } - console.error(err); - }); - - this.agent.createConnection({}, (err, conn) => { - this.debug('< [up] %s', req.url); - // any errors getting a connection mean we cannot service this request - if (err) { - socket.end(); - return; - } - - // socket met have disconnected while we waiting for a socket - if (!socket.readable || !socket.writable) { - conn.destroy(); - socket.end(); - return; - } - - // websocket requests are special in that we simply re-create the header info - // then directly pipe the socket data - // avoids having to rebuild the request and handle upgrades via the http client - const arr = [`${req.method} ${req.url} HTTP/${req.httpVersion}`]; - for (let i=0 ; i < (req.rawHeaders.length-1) ; i+=2) { - arr.push(`${req.rawHeaders[i]}: ${req.rawHeaders[i+1]}`); - } - - arr.push(''); - arr.push(''); - - // using pump is deliberate - see the pump docs for why - pump(conn, socket); - pump(socket, conn); - conn.write(arr.join('\r\n')); - }); - } -} - -export default Client; \ No newline at end of file diff --git a/lib/ClientManager.js b/lib/ClientManager.js deleted file mode 100644 index e1a7838..0000000 --- a/lib/ClientManager.js +++ /dev/null @@ -1,96 +0,0 @@ -import { hri } from 'human-readable-ids'; -import Debug from 'debug'; - -import Client from './Client'; -import TunnelAgent from './TunnelAgent'; - -// Manage sets of clients -// -// A client is a "user session" established to service a remote localtunnel client -class ClientManager { - constructor(opt) { - this.opt = opt || {}; - - // id -> client instance - this.clients = new Map(); - - // statistics - this.stats = { - tunnels: 0 - }; - - this.debug = Debug('lt:ClientManager'); - - // 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(); - } - - const maxSockets = this.opt.max_tcp_sockets; - const agent = new TunnelAgent({ - clientId: id, - maxSockets: 10, - }); - - const client = new Client({ - id, - agent, - }); - - // 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; - } - } - - 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(); - } - - 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 deleted file mode 100644 index efc2231..0000000 --- a/lib/TunnelAgent.js +++ /dev/null @@ -1,175 +0,0 @@ -import { Agent } from 'http'; -import net from 'net'; -import assert from 'assert'; -import log from 'book'; -import Debug from 'debug'; - -const DEFAULT_MAX_SOCKETS = 10; - -// Implements an http.Agent interface to a pool of tunnel sockets -// A tunnel socket is a connection _from_ a client that will -// service http requests. This agent is usable wherever one can use an http.Agent -class TunnelAgent extends Agent { - constructor(options = {}) { - super({ - keepAlive: true, - // only allow keepalive to hold on to one socket - // this prevents it from holding on to all the sockets so they can be used for upgrades - maxFreeSockets: 1, - }); - - // sockets we can hand out via createConnection - this.availableSockets = []; - - // when a createConnection cannot return a socket, it goes into a queue - // once a socket is available it is handed out to the next callback - this.waitingCreateConn = []; - - this.debug = Debug(`lt:TunnelAgent[${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; - } - - 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) => { - server.listen(() => { - const port = server.address().port; - this.debug('tcp server listening on port: %d', port); - - resolve({ - // port for lt client tcp connections - port: port, - }); - }); - }); - } - - _onClose() { - this.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'); - } - - // 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; - } - - 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'); - } - - 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 - // if no socket is available, queue - // cb(err, socket) - createConnection(options, cb) { - if (this.closed) { - cb(new Error('closed')); - return; - } - - 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(); - } -} - -export default TunnelAgent; diff --git a/lib/agent.ts b/lib/agent.ts new file mode 100644 index 0000000..2b57063 --- /dev/null +++ b/lib/agent.ts @@ -0,0 +1,199 @@ +import { Agent } from "http"; +import net from "net"; +import Debug from "debug"; +import { type AddressInfo } from "ws"; + +const DEFAULT_MAX_SOCKETS = 10; + +// Implements an http.Agent interface to a pool of tunnel sockets +// A tunnel socket is a connection _from_ a client that will +// service http requests. This agent is usable wherever one can use an http.Agent +export class TunnelAgent extends Agent { + public availableSockets: any[]; + public waitingCreateConn: any[]; + public debug: any; + public connectedSockets: number; + public maxTcpSockets: number; + public server: net.Server; + public started: boolean; + public closed: boolean; + + 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; + + // new tcp server to service requests for this client + this.server = net.createServer((e) => { + console.log(`\n\n\n\x1b[33m==>NET \x1b[0m\n`); + console.log(e); + }); + + // flag to avoid double starts + this.started = false; + this.closed = false; + } + + stats() { + return { connectedSockets: this.connectedSockets }; + } + + listen(): Promise<{ port: number }> { + const server = this.server; + + if (this.started) throw new Error("already started"); + + this.started = true; + + server.on("close", (e) => { + this._onClose.bind(this); + }); + + server.on("connection", (e) => { + console.log(`\n\n\n\x1b[33m==>CXN \x1b[0m\n`); + console.log(e); + + return this._onConnection.bind(this); + }); + + server.on("error", (err: any) => { + console.log(`\n\n\n\x1b[33m==>HERE1 \x1b[0m\n`); + // These errors happen from killed connections, we don't worry about them + if (err.code == "ECONNRESET" || err.code == "ETIMEDOUT") { + return; + } + console.error(err); + }); + + return new Promise((resolve) => { + server.listen(() => { + const port = (server.address() as AddressInfo).port; + this.debug("tcp server listening on port: %d", port); + + // port for lt client tcp connections + resolve({ + port, + }); + }); + }); + } + + _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"); + } + + // new socket connection from client for tunneling requests to client + _onConnection(socket) { + console.log(`\n\n\n\x1b[33m==>HERE2 \x1b[0m\n`); + // no more socket connections allowed + if (this.connectedSockets >= this.maxTcpSockets) { + this.debug("no more sockets allowed"); + socket.destroy(); + return false; + } + + 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) => { + console.log(`\n\n\n\x1b[33m==>HERE3 \x1b[0m\n`); + // 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 + // if no socket is available, queue + // cb(err, socket) + createConnection(options, cb) { + console.log(`\n\n\n\x1b[33m==>HERE4 \x1b[0m\n`); + if (this.closed) { + cb(new Error("closed")); + return; + } + + 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(); + } +} diff --git a/lib/client.ts b/lib/client.ts new file mode 100644 index 0000000..cb8d9b0 --- /dev/null +++ b/lib/client.ts @@ -0,0 +1,142 @@ +import http from "http"; +import Debug from "debug"; +import pump from "pump"; +import EventEmitter from "events"; +import { TunnelAgent } from "./agent"; + +// A client encapsulates req/res handling using an agent +// If an agent is destroyed, the request handling will error +// The caller is responsible for handling a failed request +export default class Client extends EventEmitter { + public agent: TunnelAgent; + public id: string; + public debug: any; + public graceTimeout: any; + + constructor(options: { id: string; agent: TunnelAgent }) { + super(); + + const agent = (this.agent = options.agent); + const id = (this.id = options.id); + + this.debug = Debug(`lt:Client[${this.id}]`); + + // client is given a grace period in which they can connect before they are _removed_ + this.graceTimeout = setTimeout(() => { + console.log(`Timed out. Closing client.`); + this.close(); + }, 1000).unref(); + + agent.on("online", () => { + this.debug("client online %s", id); + clearTimeout(this.graceTimeout); + }); + + agent.on("offline", () => { + this.debug("client offline %s", id); + + // if there was a previous timeout set, we don't want to double trigger + clearTimeout(this.graceTimeout); + + // client is given a grace period in which they can re-connect before they are _removed_ + this.graceTimeout = setTimeout(() => { + console.log(`Timed out #2. Closing client.`); + this.close(); + }, 1000).unref(); + }); + + // TODO(roman): an agent error removes the client, the user needs to re-connect? + // how does a user realize they need to re-connect vs some random client being assigned same port? + agent.once("error", (err) => { + console.error(err); + this.close(); + }); + } + + stats() { + return this.agent.stats(); + } + + close() { + clearTimeout(this.graceTimeout); + this.agent.destroy(); + this.emit("close"); + } + + handleRequest(req, res) { + console.log(`\n\n\n\x1b[33m==>HANDLE REQ \x1b[0m\n`); + this.debug("> %s", req.url); + const opt = { + path: req.url, + agent: this.agent, + method: req.method, + headers: req.headers, + }; + + const clientReq = http.request(opt, (clientRes) => { + this.debug("< %s", req.url); + // write response code and headers + res.writeHead(clientRes.statusCode, clientRes.headers); + + // using pump is deliberate - see the pump docs for why + pump(clientRes, res); + }); + + // this can happen when underlying agent produces an error + // in our case we 504 gateway error this if we have already sent headers + clientReq.once("error", (err) => { + console.log(`\n\n\n\x1b[33m==>CLIENT REQ ERR \x1b[0m\n`); + console.log(err); + + // TODO: if headers not sent - respond with gateway unavailable + }); + + // using pump is deliberate - see the pump docs for why + pump(req, clientReq); + } + + handleUpgrade(req, socket) { + this.debug("> [up] %s", req.url); + socket.once("error", (err) => { + // These client side errors can happen if the client dies while we are reading + // We don't need to surface these in our logs. + if (err.code == "ECONNRESET" || err.code == "ETIMEDOUT") { + return; + } + console.error(err); + }); + + this.agent.createConnection({}, (err, conn) => { + this.debug("< [up] %s", req.url); + // any errors getting a connection mean we cannot service this request + if (err) { + socket.end(); + return; + } + + // socket met have disconnected while we waiting for a socket + if (!socket.readable || !socket.writable) { + console.log(`\n\n\n\x1b[33m==>NOT READABLE OR WRITABLE \x1b[0m\n`); + conn.destroy(); + socket.end(); + return; + } + + // websocket requests are special in that we simply re-create the header info + // then directly pipe the socket data + // avoids having to rebuild the request and handle upgrades via the http client + const arr = [`${req.method} ${req.url} HTTP/${req.httpVersion}`]; + for (let i = 0; i < req.rawHeaders.length - 1; i += 2) { + arr.push(`${req.rawHeaders[i]}: ${req.rawHeaders[i + 1]}`); + } + + arr.push(""); + arr.push(""); + + // using pump is deliberate - see the pump docs for why + pump(conn, socket); + pump(socket, conn); + conn.write(arr.join("\r\n")); + }); + } +} diff --git a/lib/manager.ts b/lib/manager.ts new file mode 100644 index 0000000..87b8f92 --- /dev/null +++ b/lib/manager.ts @@ -0,0 +1,102 @@ +import { hri } from "human-readable-ids"; +import Debug from "debug"; +import Client from "./client"; +import { TunnelAgent } from "./agent"; +import { Options } from "../server"; + +// Manage sets of clients +// A client is a "user session" established to service a remote localtunnel client +export class ClientManager { + public opt: Options; + public clients: Map; + public stats: { tunnels: number }; + public debug: any; + + constructor(opt: Options) { + this.opt = opt || {}; + + // id -> client instance + this.clients = new Map(); + + // statistics + this.stats = { + tunnels: 0, + }; + + this.debug = Debug("lt:ClientManager"); + } + + // create a new tunnel with `id` + // if the id is already used, a random id is assigned + // if the tunnel could not be created, throws an error + async newClient(id: string) { + const clients = this.clients; + const stats = this.stats; + + // can't ask for id already is use + if (clients.has(id)) { + id = hri.random(); + } + + const maxSockets = this.opt.max_tcp_sockets; + + const agent = new TunnelAgent({ + clientId: id, + maxSockets: 10, + }); + + const client = new Client({ id, agent }); + + // add to clients map immediately to avoid races with other clients requesting same id + clients.set(id, client); + + client.once("close", (e) => { + console.log(`\n\n\n\x1b[33m==>Closing: \x1b[0m\n`); + console.log(e); + 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, + url: "", + }; + } catch (err) { + this.removeClient(id); + // rethrow error for upstream to handle + throw err; + } + } + + getClient(id: string) { + return this.clients.get(id); + } + + hasClient(id: string) { + return !!this.getClient(id); + } + + removeClient(id: string) { + this.debug("removing client: %s", id); + + const client = this.getClient(id); + + if (!client) { + console.log(`\n\n\n\x1b[33m==>NO CLIENT \x1b[0m\n`); + return; + } + + --this.stats.tunnels; + + delete this.clients[id]; + + client.close(); + } +} diff --git a/opus.ts b/opus.ts new file mode 100644 index 0000000..63dbd45 --- /dev/null +++ b/opus.ts @@ -0,0 +1,110 @@ +import { serve, type Socket } from "bun"; +import { uid, parseResponse, wait } from "./utils"; + +const clientSockets = new Map(); +const clientData = new Map(); + +const port = Bun.env.PORT || 1234; +const protocol = Bun.env.PROTOCOL || "http"; + +serve({ + port, + fetch: async (req: Request) => { + const url = new URL(req.url); + const hostname = `${url.hostname}${url.port && `:${url.port}`}`; + + // For healthchecks + if (url.pathname === "/healthz") return new Response("healthy"); + + if (url.searchParams.has("new")) { + const clientId = uid(); + + const server = Bun.listen({ + hostname: "localhost", + port: 0, // Let the OS assign an available port + socket: { + data(_socket, data) { + clientData.set(clientId, data); + }, + open(socket) { + console.log(`Client ${clientId} connected`); + clientSockets.set(clientId, socket); + }, + close() { + console.log(`Client ${clientId} reconnecting`); + clientSockets.delete(clientId); + clientData.delete(clientId); + }, + error(_socket, error) { + console.error(`Error for client ${clientId}:`, error); + }, + }, + }); + + return new Response( + JSON.stringify({ + id: clientId, + port: server.port, + url: `${protocol}://${clientId}.${hostname}`, + }), + { + headers: { "Content-Type": "application/json" }, + status: 200, + } + ); + } + + if (hostname.split(".").length === 3) { + return new Response("welcome to tunnel. hit ?new to get a URL."); + } + + const subdomain = hostname.split(".")[0]; + const clientSocket = clientSockets.get(subdomain); + + if (!clientSocket) { + console.warn(`Client ${subdomain} not found`); + return new Response("Client not found", { status: 404 }); + } + + console.log(`Forwarding request to client ${subdomain}`); + + // Forward the request to the connected client + console.log(req.method, url.pathname, url.search); + clientSocket.write( + `${req.method} ${url.pathname}${url.search} HTTP/1.1\r\n` + ); + for (const [header, value] of req.headers.entries()) { + clientSocket.write(`${header}: ${value}\r\n`); + } + clientSocket.write("\r\n"); + + if (req.body) { + const reader = req.body.getReader(); + let chunk; + while (!(chunk = await reader.read()).done) { + clientSocket.write(chunk.value); + } + } + + let responseData = clientData.get(subdomain); + let attempts = 10; + + while (!responseData) { + await wait(100); + responseData = clientData.get(subdomain); + if (attempts < 1) { + return new Response("Client did not respond", { status: 504 }); + } + } + + const { body, headers } = parseResponse(responseData); + + return new Response(body, { headers, status: 200 }); + }, + error(error) { + console.error(error); + return new Response(error.message, { status: 500 }); + }, +}); + +console.log(`Listening on port ${port}`); diff --git a/package.json b/package.json index 94cef3b..58996ad 100644 --- a/package.json +++ b/package.json @@ -1,17 +1,15 @@ { - "author": "Roman Shtylman ", - "name": "localtunnel-server", + "name": "tunnel", "description": "expose localhost to the world", - "version": "0.0.8", + "version": "0.0.0", "license": "MIT", "repository": { "type": "git", - "url": "git://github.com/localtunnel/server.git" + "url": "https://github.com/rubriclab/tunnel" }, "dependencies": { - "book": "1.3.3", "debug": "3.1.0", - "esm": "3.0.34", + "human-id": "^4.1.1", "human-readable-ids": "1.0.3", "koa": "2.5.1", "koa-router": "7.4.0", @@ -21,14 +19,11 @@ "tldjs": "2.3.1" }, "devDependencies": { - "mocha": "5.1.1", - "node-dev": "3.1.3", - "supertest": "3.1.0", + "@types/bun": "^1.0.8", "ws": "5.1.1" }, "scripts": { - "test": "mocha --check-leaks --require esm './**/*.test.js'", - "start": "./bin/server --domain tunnel.rubric.sh", - "dev": "node-dev bin/server --port 3000" + "start": "bun run opus.ts", + "dev": "bun run --watch index.ts --domain ted.rubric.sh --port 3001" } } diff --git a/server.js b/server.js deleted file mode 100644 index 21a4af6..0000000 --- a/server.js +++ /dev/null @@ -1,168 +0,0 @@ -import log from 'book'; -import Koa from 'koa'; -import tldjs from 'tldjs'; -import Debug from 'debug'; -import http from 'http'; -import { hri } from 'human-readable-ids'; -import Router from 'koa-router'; - -import ClientManager from './lib/ClientManager'; - -const debug = Debug('localtunnel:server'); - -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/'; - - 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 stats = client.stats(); - ctx.body = { - connected_sockets: stats.connectedSockets, - }; - }); - - app.use(router.routes()); - app.use(router.allowedMethods()); - - // root endpoint - app.use(async (ctx, next) => { - const path = ctx.request.path; - - // skip anything not on the root path - if (path !== '/') { - 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 url = schema + '://' + info.id + '.' + ctx.request.host; - info.url = url; - ctx.body = info; - return; - } - - // no new client request, send to landing page - ctx.redirect(landingPage); - }); - - // 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('/'); - - // any request with several layers of paths is not allowed - // rejects /foo/bar - // allow /foo - if (parts.length !== 2) { - await next(); - return; - } - - const reqId = parts[1]; - - // limit requested hostnames to 63 characters - if (! /^(?:[a-z0-9][a-z0-9\-]{4,63}[a-z0-9]|[a-z0-9]{4,63})$/.test(reqId)) { - const msg = 'Invalid subdomain. Subdomains must be lowercase and between 4 and 63 alphanumeric characters.'; - ctx.status = 403; - ctx.body = { - message: msg, - }; - return; - } - - debug('making new client with id %s', reqId); - const info = await manager.newClient(reqId); - - const url = schema + '://' + info.id + '.' + ctx.request.host; - info.url = url; - ctx.body = info; - return; - }); - - const server = http.createServer(); - - const appCallback = app.callback(); - - server.on('request', (req, res) => { - // without a hostname, we won't know who the request is for - const hostname = req.headers.host; - if (!hostname) { - res.statusCode = 400; - res.end('Host header is required'); - return; - } - - const clientId = GetClientIdFromHostname(hostname); - if (!clientId) { - appCallback(req, res); - return; - } - - const client = manager.getClient(clientId); - if (!client) { - res.statusCode = 404; - res.end('404'); - return; - } - - 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; -}; diff --git a/server.ts b/server.ts new file mode 100644 index 0000000..093776b --- /dev/null +++ b/server.ts @@ -0,0 +1,178 @@ +import Koa from "koa"; +import tldjs from "tldjs"; +import Debug from "debug"; +import http from "http"; +import { hri } from "human-readable-ids"; +import Router from "koa-router"; +import { ClientManager } from "./lib/manager"; + +export type Options = { + domain: string; + secure: boolean; + landing?: string; + max_tcp_sockets?: number; +}; + +const debug = Debug("localtunnel:server"); + +export const CreateServer = (opt: Options) => { + opt = opt || {}; + + const validHosts = opt.domain ? [opt.domain] : undefined; + const myTldjs = tldjs.fromUserSettings({ validHosts }); + const landingPage = opt.landing || "https://localtunnel.github.io/www/"; + + function getClientIdFromHostname(hostname: string) { + 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 stats = client.stats(); + ctx.body = { + connected_sockets: stats.connectedSockets, + }; + }); + + app.use(router.routes()); + app.use(router.allowedMethods()); + + // root endpoint + app.use(async (ctx, next) => { + const path = ctx.request.path; + + // skip anything not on the root path + if (path !== "/") { + 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 url = schema + "://" + info.id + "." + ctx.request.host; + + info.url = url; + ctx.body = info; + + return; + } + + // no new client request, send to landing page + ctx.redirect(landingPage); + }); + + // 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("/"); + + // any request with several layers of paths is not allowed + // rejects /foo/bar; allow /foo + if (parts.length !== 2) { + await next(); + return; + } + + const reqId = parts[1]; + + // limit requested hostnames to 63 characters + if (!/^(?:[a-z0-9][a-z0-9\-]{4,63}[a-z0-9]|[a-z0-9]{4,63})$/.test(reqId)) { + const msg = + "Invalid subdomain. Subdomains must be lowercase and between 4 and 63 alphanumeric characters."; + ctx.status = 403; + ctx.body = { + message: msg, + }; + return; + } + + debug("making new client with id %s", reqId); + const info = await manager.newClient(reqId); + + const url = schema + "://" + info.id + "." + ctx.request.host; + info.url = url; + ctx.body = info; + return; + }); + + const server = http.createServer(); + + const appCallback = app.callback(); + + server.on("request", (req, res) => { + // without a hostname, we won't know who the request is for + const hostname = req.headers.host; + if (!hostname) { + res.statusCode = 400; + res.end("Host header is required"); + return; + } + + const clientId = getClientIdFromHostname(hostname); + if (!clientId) { + appCallback(req, res); + return; + } + + const client = manager.getClient(clientId); + if (!client) { + res.statusCode = 404; + res.end("404"); + return; + } + + client.handleRequest(req, res); + }); + + server.on("upgrade", (req, socket, head) => { + console.log(`\n\n\n\x1b[33m==>UPGRADE \x1b[0m\n`); + 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; +}; diff --git a/utils.ts b/utils.ts new file mode 100644 index 0000000..07f468f --- /dev/null +++ b/utils.ts @@ -0,0 +1,24 @@ +import { humanId } from "human-id"; + +export const uid = () => { + return humanId({ capitalize: false, separator: "-" }); +}; + +export const wait = (ms: number) => new Promise((r) => setTimeout(r, ms)); + +export const parseResponse = (buffer: Buffer) => { + const asString = Buffer.from(buffer).toString(); + const [responseHeaders, body] = asString.split("\r\n\r\n"); + + const headers = new Headers(); + + responseHeaders + .trim() + .split("\r\n") + .forEach((header) => { + const [name, value] = header.split(": "); + if (!name.includes("HTTP")) headers.append(name, value); + }); + + return { body, headers }; +}; diff --git a/yarn.lock b/yarn.lock deleted file mode 100644 index 9ad744b..0000000 --- a/yarn.lock +++ /dev/null @@ -1,1066 +0,0 @@ -# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. -# yarn lockfile v1 - - -accepts@^1.2.2: - version "1.3.5" - resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.5.tgz#eb777df6011723a3b14e8a72c0805c8e86746bd2" - dependencies: - mime-types "~2.1.18" - negotiator "0.6.1" - -ansi-regex@^2.0.0: - version "2.1.1" - resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-2.1.1.tgz#c3b33ab5ee360d86e0e628f0468ae7ef27d654df" - -ansi-styles@^2.2.1: - version "2.2.1" - resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-2.2.1.tgz#b432dd3358b634cf75e1e4664368240533c1ddbe" - -ansicolors@~0.2.1: - version "0.2.1" - resolved "https://registry.yarnpkg.com/ansicolors/-/ansicolors-0.2.1.tgz#be089599097b74a5c9c4a84a0cdbcdb62bd87aef" - -any-promise@^1.1.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/any-promise/-/any-promise-1.3.0.tgz#abc6afeedcea52e809cdc0376aed3ce39635d17f" - -array-find-index@^1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/array-find-index/-/array-find-index-1.0.2.tgz#df010aa1287e164bbda6f9723b0a96a1ec4187a1" - -async-limiter@~1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/async-limiter/-/async-limiter-1.0.0.tgz#78faed8c3d074ab81f22b4e985d79e8738f720f8" - -asynckit@^0.4.0: - version "0.4.0" - resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" - -balanced-match@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" - -book@1.3.3: - version "1.3.3" - resolved "https://registry.yarnpkg.com/book/-/book-1.3.3.tgz#53654254bfe083db102e461544ae9f84be199985" - dependencies: - error-stack-parser "1.3.6" - -brace-expansion@^1.1.7: - version "1.1.11" - resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" - dependencies: - balanced-match "^1.0.0" - concat-map "0.0.1" - -browser-stdout@1.3.1: - version "1.3.1" - resolved "https://registry.yarnpkg.com/browser-stdout/-/browser-stdout-1.3.1.tgz#baa559ee14ced73452229bad7326467c61fabd60" - -builtin-modules@^1.0.0: - version "1.1.1" - resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-1.1.1.tgz#270f076c5a72c02f5b65a47df94c5fe3a278892f" - -camelcase-keys@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/camelcase-keys/-/camelcase-keys-2.1.0.tgz#308beeaffdf28119051efa1d932213c91b8f92e7" - dependencies: - camelcase "^2.0.0" - map-obj "^1.0.0" - -camelcase@^2.0.0: - version "2.1.1" - resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-2.1.1.tgz#7c1d16d679a1bbe59ca02cacecfb011e201f5a1f" - -cardinal@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/cardinal/-/cardinal-1.0.0.tgz#50e21c1b0aa37729f9377def196b5a9cec932ee9" - dependencies: - ansicolors "~0.2.1" - redeyed "~1.0.0" - -chalk@^1.1.3: - version "1.1.3" - resolved "https://registry.yarnpkg.com/chalk/-/chalk-1.1.3.tgz#a8115c55e4a702fe4d150abd3872822a7e09fc98" - dependencies: - ansi-styles "^2.2.1" - escape-string-regexp "^1.0.2" - has-ansi "^2.0.0" - strip-ansi "^3.0.0" - supports-color "^2.0.0" - -cli-table@^0.3.1: - version "0.3.1" - resolved "https://registry.yarnpkg.com/cli-table/-/cli-table-0.3.1.tgz#f53b05266a8b1a0b934b3d0821e6e2dc5914ae23" - dependencies: - colors "1.0.3" - -cli-usage@^0.1.1: - version "0.1.7" - resolved "https://registry.yarnpkg.com/cli-usage/-/cli-usage-0.1.7.tgz#eaf1c9d5b91e22482333072a12127f05cd99a3ba" - dependencies: - marked "^0.3.12" - marked-terminal "^2.0.0" - -co@^4.6.0: - version "4.6.0" - resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184" - -colors@1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/colors/-/colors-1.0.3.tgz#0433f44d809680fdeb60ed260f1b0c262e82a40b" - -combined-stream@1.0.6: - version "1.0.6" - resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.6.tgz#723e7df6e801ac5613113a7e445a9b69cb632818" - dependencies: - delayed-stream "~1.0.0" - -commander@2.11.0: - version "2.11.0" - resolved "https://registry.yarnpkg.com/commander/-/commander-2.11.0.tgz#157152fd1e7a6c8d98a5b715cf376df928004563" - -commander@2.5.0: - version "2.5.0" - resolved "https://registry.yarnpkg.com/commander/-/commander-2.5.0.tgz#d777b6a4d847d423e5d475da864294ac1ff5aa9d" - -component-emitter@^1.2.0: - version "1.2.1" - resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.2.1.tgz#137918d6d78283f7df7a6b7c5a63e140e69425e6" - -concat-map@0.0.1: - version "0.0.1" - resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" - -content-disposition@~0.5.0: - version "0.5.2" - resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.2.tgz#0cf68bb9ddf5f2be7961c3a85178cb85dba78cb4" - -content-type@^1.0.0: - version "1.0.4" - resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b" - -cookiejar@^2.1.0: - version "2.1.1" - resolved "https://registry.yarnpkg.com/cookiejar/-/cookiejar-2.1.1.tgz#41ad57b1b555951ec171412a81942b1e8200d34a" - -cookies@~0.7.0: - version "0.7.1" - resolved "https://registry.yarnpkg.com/cookies/-/cookies-0.7.1.tgz#7c8a615f5481c61ab9f16c833731bcb8f663b99b" - dependencies: - depd "~1.1.1" - keygrip "~1.0.2" - -core-util-is@~1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" - -currently-unhandled@^0.4.1: - version "0.4.1" - resolved "https://registry.yarnpkg.com/currently-unhandled/-/currently-unhandled-0.4.1.tgz#988df33feab191ef799a61369dd76c17adf957ea" - dependencies: - array-find-index "^1.0.1" - -dateformat@~1.0.4-1.2.3: - version "1.0.12" - resolved "https://registry.yarnpkg.com/dateformat/-/dateformat-1.0.12.tgz#9f124b67594c937ff706932e4a642cca8dbbfee9" - dependencies: - get-stdin "^4.0.1" - meow "^3.3.0" - -debounce@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/debounce/-/debounce-1.1.0.tgz#6a1a4ee2a9dc4b7c24bb012558dbcdb05b37f408" - -debug@*, debug@3.1.0, debug@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261" - dependencies: - ms "2.0.0" - -decamelize@^1.1.2: - version "1.2.0" - resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" - -deep-equal@~1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-1.0.1.tgz#f5d260292b660e084eff4cdbc9f08ad3247448b5" - -delayed-stream@~1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" - -delegates@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a" - -depd@^1.1.0, depd@~1.1.1, depd@~1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9" - -destroy@^1.0.3: - version "1.0.4" - resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.0.4.tgz#978857442c44749e4206613e37946205826abd80" - -diff@3.5.0: - version "3.5.0" - resolved "https://registry.yarnpkg.com/diff/-/diff-3.5.0.tgz#800c0dd1e0a8bfbc95835c202ad220fe317e5a12" - -dynamic-dedupe@^0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/dynamic-dedupe/-/dynamic-dedupe-0.2.0.tgz#50f7c28684831ecf1c170aab67a1d5311cdd76ce" - dependencies: - xtend "~2.0.6" - -ee-first@1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" - -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" - dependencies: - once "^1.4.0" - -error-ex@^1.2.0: - version "1.3.1" - resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.1.tgz#f855a86ce61adc4e8621c3cda21e7a7612c3a8dc" - dependencies: - is-arrayish "^0.2.1" - -error-inject@~1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/error-inject/-/error-inject-1.0.0.tgz#e2b3d91b54aed672f309d950d154850fa11d4f37" - -error-stack-parser@1.3.6: - version "1.3.6" - resolved "https://registry.yarnpkg.com/error-stack-parser/-/error-stack-parser-1.3.6.tgz#e0e73b93e417138d1cd7c0b746b1a4a14854c292" - dependencies: - stackframe "^0.3.1" - -escape-html@~1.0.1: - version "1.0.3" - resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" - -escape-string-regexp@1.0.5, escape-string-regexp@^1.0.2: - version "1.0.5" - resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" - -esm@3.0.34: - version "3.0.34" - resolved "https://registry.yarnpkg.com/esm/-/esm-3.0.34.tgz#f86afa35c83a0f535da01d15e625e45794ebddc8" - -esprima@~3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/esprima/-/esprima-3.0.0.tgz#53cf247acda77313e551c3aa2e73342d3fb4f7d9" - -extend@^3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.1.tgz#a755ea7bc1adfcc5a31ce7e762dbaadc5e636444" - -filewatcher@~3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/filewatcher/-/filewatcher-3.0.1.tgz#f4a1957355ddaf443ccd78a895f3d55e23c8a034" - dependencies: - debounce "^1.0.0" - -find-up@^1.0.0: - version "1.1.2" - resolved "https://registry.yarnpkg.com/find-up/-/find-up-1.1.2.tgz#6b2e9822b1a2ce0a60ab64d610eccad53cb24d0f" - dependencies: - path-exists "^2.0.0" - pinkie-promise "^2.0.0" - -foreach@~2.0.1: - version "2.0.5" - resolved "https://registry.yarnpkg.com/foreach/-/foreach-2.0.5.tgz#0bee005018aeb260d0a3af3ae658dd0136ec1b99" - -form-data@^2.3.1: - version "2.3.2" - resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.3.2.tgz#4970498be604c20c005d4f5c23aecd21d6b49099" - dependencies: - asynckit "^0.4.0" - combined-stream "1.0.6" - mime-types "^2.1.12" - -formidable@^1.1.1: - version "1.2.1" - resolved "https://registry.yarnpkg.com/formidable/-/formidable-1.2.1.tgz#70fb7ca0290ee6ff961090415f4b3df3d2082659" - -fresh@^0.5.2: - version "0.5.2" - resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7" - -fs.realpath@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" - -get-stdin@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/get-stdin/-/get-stdin-4.0.1.tgz#b968c6b0a04384324902e8bf1a5df32579a450fe" - -glob@7.1.2: - version "7.1.2" - resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.2.tgz#c19c9df9a028702d678612384a6552404c636d15" - dependencies: - fs.realpath "^1.0.0" - inflight "^1.0.4" - inherits "2" - minimatch "^3.0.4" - once "^1.3.0" - path-is-absolute "^1.0.0" - -graceful-fs@^4.1.2: - version "4.1.11" - resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.1.11.tgz#0e8bdfe4d1ddb8854d64e04ea7c00e2a026e5658" - -growl@1.10.3: - version "1.10.3" - resolved "https://registry.yarnpkg.com/growl/-/growl-1.10.3.tgz#1926ba90cf3edfe2adb4927f5880bc22c66c790f" - -growly@^1.2.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/growly/-/growly-1.3.0.tgz#f10748cbe76af964b7c96c93c6bcc28af120c081" - -has-ansi@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/has-ansi/-/has-ansi-2.0.0.tgz#34f5049ce1ecdf2b0649af3ef24e45ed35416d91" - dependencies: - ansi-regex "^2.0.0" - -has-flag@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-2.0.0.tgz#e8207af1cc7b30d446cc70b734b5e8be18f88d51" - -he@1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/he/-/he-1.1.1.tgz#93410fd21b009735151f8868c2f271f3427e23fd" - -hosted-git-info@^2.1.4: - version "2.6.0" - resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.6.0.tgz#23235b29ab230c576aab0d4f13fc046b0b038222" - -http-assert@^1.1.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/http-assert/-/http-assert-1.3.0.tgz#a31a5cf88c873ecbb5796907d4d6f132e8c01e4a" - dependencies: - deep-equal "~1.0.1" - http-errors "~1.6.1" - -http-errors@^1.2.8, http-errors@^1.3.1, http-errors@~1.6.1: - version "1.6.3" - resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.6.3.tgz#8b55680bb4be283a0b5bf4ea2e38580be1d9320d" - dependencies: - depd "~1.1.2" - inherits "2.0.3" - setprototypeof "1.1.0" - statuses ">= 1.4.0 < 2" - -human-readable-ids@1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/human-readable-ids/-/human-readable-ids-1.0.3.tgz#c8c6c6e95085ccb668087b7dd767834e26ca26d4" - dependencies: - knuth-shuffle "^1.0.0" - -indent-string@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-2.1.0.tgz#8e2d48348742121b4a8218b7a137e9a52049dc80" - dependencies: - repeating "^2.0.0" - -indexof@~0.0.1: - version "0.0.1" - resolved "https://registry.yarnpkg.com/indexof/-/indexof-0.0.1.tgz#82dc336d232b9062179d05ab3293a66059fd435d" - -inflight@^1.0.4: - version "1.0.6" - resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" - dependencies: - once "^1.3.0" - wrappy "1" - -inherits@2, inherits@2.0.3, inherits@~2.0.3: - version "2.0.3" - resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" - -is-arrayish@^0.2.1: - version "0.2.1" - resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" - -is-builtin-module@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/is-builtin-module/-/is-builtin-module-1.0.0.tgz#540572d34f7ac3119f8f76c30cbc1b1e037affbe" - dependencies: - builtin-modules "^1.0.0" - -is-finite@^1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/is-finite/-/is-finite-1.0.2.tgz#cc6677695602be550ef11e8b4aa6305342b6d0aa" - dependencies: - number-is-nan "^1.0.0" - -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" - -is-object@~0.1.2: - version "0.1.2" - resolved "https://registry.yarnpkg.com/is-object/-/is-object-0.1.2.tgz#00efbc08816c33cfc4ac8251d132e10dc65098d7" - -is-utf8@^0.2.0: - version "0.2.1" - resolved "https://registry.yarnpkg.com/is-utf8/-/is-utf8-0.2.1.tgz#4b0da1442104d1b336340e80797e865cf39f7d72" - -is@~0.2.6: - version "0.2.7" - resolved "https://registry.yarnpkg.com/is/-/is-0.2.7.tgz#3b34a2c48f359972f35042849193ae7264b63562" - -isarray@0.0.1: - version "0.0.1" - resolved "https://registry.yarnpkg.com/isarray/-/isarray-0.0.1.tgz#8a18acfca9a8f4177e09abfc6038939b05d1eedf" - -isarray@~1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" - -isexe@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" - -keygrip@~1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/keygrip/-/keygrip-1.0.2.tgz#ad3297c557069dea8bcfe7a4fa491b75c5ddeb91" - -knuth-shuffle@^1.0.0: - version "1.0.8" - resolved "https://registry.yarnpkg.com/knuth-shuffle/-/knuth-shuffle-1.0.8.tgz#929a467b0efd8d297bdcf318ca988a9f1037f80d" - -koa-compose@^3.0.0: - version "3.2.1" - resolved "https://registry.yarnpkg.com/koa-compose/-/koa-compose-3.2.1.tgz#a85ccb40b7d986d8e5a345b3a1ace8eabcf54de7" - dependencies: - any-promise "^1.1.0" - -koa-compose@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/koa-compose/-/koa-compose-4.0.0.tgz#2800a513d9c361ef0d63852b038e4f6f2d5a773c" - -koa-convert@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/koa-convert/-/koa-convert-1.2.0.tgz#da40875df49de0539098d1700b50820cebcd21d0" - dependencies: - co "^4.6.0" - koa-compose "^3.0.0" - -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-router@7.4.0: - version "7.4.0" - resolved "https://registry.yarnpkg.com/koa-router/-/koa-router-7.4.0.tgz#aee1f7adc02d5cb31d7d67465c9eacc825e8c5e0" - dependencies: - debug "^3.1.0" - http-errors "^1.3.1" - koa-compose "^3.0.0" - methods "^1.0.1" - path-to-regexp "^1.1.1" - urijs "^1.19.0" - -koa@2.5.1: - version "2.5.1" - resolved "https://registry.yarnpkg.com/koa/-/koa-2.5.1.tgz#79f8b95f8d72d04fe9a58a8da5ebd6d341103f9c" - dependencies: - accepts "^1.2.2" - content-disposition "~0.5.0" - content-type "^1.0.0" - cookies "~0.7.0" - debug "*" - delegates "^1.0.0" - depd "^1.1.0" - destroy "^1.0.3" - error-inject "~1.0.0" - escape-html "~1.0.1" - fresh "^0.5.2" - http-assert "^1.1.0" - http-errors "^1.2.8" - is-generator-function "^1.0.3" - koa-compose "^4.0.0" - koa-convert "^1.2.0" - koa-is-json "^1.0.0" - mime-types "^2.0.7" - on-finished "^2.1.0" - only "0.0.2" - parseurl "^1.3.0" - statuses "^1.2.0" - type-is "^1.5.5" - vary "^1.0.0" - -load-json-file@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-1.1.0.tgz#956905708d58b4bab4c2261b04f59f31c99374c0" - dependencies: - graceful-fs "^4.1.2" - parse-json "^2.2.0" - pify "^2.0.0" - pinkie-promise "^2.0.0" - strip-bom "^2.0.0" - -localenv@0.2.2: - version "0.2.2" - resolved "https://registry.yarnpkg.com/localenv/-/localenv-0.2.2.tgz#c508f29d3485bdc9341d3ead17f61c5abd1b0bab" - dependencies: - commander "2.5.0" - -lodash._arraycopy@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/lodash._arraycopy/-/lodash._arraycopy-3.0.0.tgz#76e7b7c1f1fb92547374878a562ed06a3e50f6e1" - -lodash._arrayeach@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/lodash._arrayeach/-/lodash._arrayeach-3.0.0.tgz#bab156b2a90d3f1bbd5c653403349e5e5933ef9e" - -lodash._baseassign@^3.0.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/lodash._baseassign/-/lodash._baseassign-3.2.0.tgz#8c38a099500f215ad09e59f1722fd0c52bfe0a4e" - dependencies: - lodash._basecopy "^3.0.0" - lodash.keys "^3.0.0" - -lodash._baseclone@^3.0.0: - version "3.3.0" - resolved "https://registry.yarnpkg.com/lodash._baseclone/-/lodash._baseclone-3.3.0.tgz#303519bf6393fe7e42f34d8b630ef7794e3542b7" - dependencies: - lodash._arraycopy "^3.0.0" - lodash._arrayeach "^3.0.0" - lodash._baseassign "^3.0.0" - lodash._basefor "^3.0.0" - lodash.isarray "^3.0.0" - lodash.keys "^3.0.0" - -lodash._basecopy@^3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/lodash._basecopy/-/lodash._basecopy-3.0.1.tgz#8da0e6a876cf344c0ad8a54882111dd3c5c7ca36" - -lodash._basefor@^3.0.0: - version "3.0.3" - resolved "https://registry.yarnpkg.com/lodash._basefor/-/lodash._basefor-3.0.3.tgz#7550b4e9218ef09fad24343b612021c79b4c20c2" - -lodash._bindcallback@^3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/lodash._bindcallback/-/lodash._bindcallback-3.0.1.tgz#e531c27644cf8b57a99e17ed95b35c748789392e" - -lodash._getnative@^3.0.0: - version "3.9.1" - resolved "https://registry.yarnpkg.com/lodash._getnative/-/lodash._getnative-3.9.1.tgz#570bc7dede46d61cdcde687d65d3eecbaa3aaff5" - -lodash.assign@^4.2.0: - version "4.2.0" - resolved "https://registry.yarnpkg.com/lodash.assign/-/lodash.assign-4.2.0.tgz#0d99f3ccd7a6d261d19bdaeb9245005d285808e7" - -lodash.clonedeep@^3.0.0: - version "3.0.2" - resolved "https://registry.yarnpkg.com/lodash.clonedeep/-/lodash.clonedeep-3.0.2.tgz#a0a1e40d82a5ea89ff5b147b8444ed63d92827db" - dependencies: - lodash._baseclone "^3.0.0" - lodash._bindcallback "^3.0.0" - -lodash.isarguments@^3.0.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz#2f573d85c6a24289ff00663b491c1d338ff3458a" - -lodash.isarray@^3.0.0: - version "3.0.4" - resolved "https://registry.yarnpkg.com/lodash.isarray/-/lodash.isarray-3.0.4.tgz#79e4eb88c36a8122af86f844aa9bcd851b5fbb55" - -lodash.keys@^3.0.0: - version "3.1.2" - resolved "https://registry.yarnpkg.com/lodash.keys/-/lodash.keys-3.1.2.tgz#4dbc0472b156be50a0b286855d1bd0b0c656098a" - dependencies: - lodash._getnative "^3.0.0" - lodash.isarguments "^3.0.0" - lodash.isarray "^3.0.0" - -lodash.toarray@^4.4.0: - version "4.4.0" - resolved "https://registry.yarnpkg.com/lodash.toarray/-/lodash.toarray-4.4.0.tgz#24c4bfcd6b2fba38bfd0594db1179d8e9b656561" - -loud-rejection@^1.0.0: - version "1.6.0" - resolved "https://registry.yarnpkg.com/loud-rejection/-/loud-rejection-1.6.0.tgz#5b46f80147edee578870f086d04821cf998e551f" - dependencies: - currently-unhandled "^0.4.1" - signal-exit "^3.0.0" - -map-obj@^1.0.0, map-obj@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/map-obj/-/map-obj-1.0.1.tgz#d933ceb9205d82bdcf4886f6742bdc2b4dea146d" - -marked-terminal@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/marked-terminal/-/marked-terminal-2.0.0.tgz#5eaf568be66f686541afa52a558280310a31de2d" - dependencies: - cardinal "^1.0.0" - chalk "^1.1.3" - cli-table "^0.3.1" - lodash.assign "^4.2.0" - node-emoji "^1.4.1" - -marked@^0.3.12: - version "0.3.19" - resolved "https://registry.yarnpkg.com/marked/-/marked-0.3.19.tgz#5d47f709c4c9fc3c216b6d46127280f40b39d790" - -media-typer@0.3.0: - version "0.3.0" - resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" - -meow@^3.3.0: - version "3.7.0" - resolved "https://registry.yarnpkg.com/meow/-/meow-3.7.0.tgz#72cb668b425228290abbfa856892587308a801fb" - dependencies: - camelcase-keys "^2.0.0" - decamelize "^1.1.2" - loud-rejection "^1.0.0" - map-obj "^1.0.1" - minimist "^1.1.3" - normalize-package-data "^2.3.4" - object-assign "^4.0.1" - read-pkg-up "^1.0.1" - redent "^1.0.0" - trim-newlines "^1.0.0" - -methods@^1.0.1, methods@^1.1.1, methods@~1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" - -mime-db@~1.33.0: - version "1.33.0" - resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.33.0.tgz#a3492050a5cb9b63450541e39d9788d2272783db" - -mime-types@^2.0.7, mime-types@^2.1.12, mime-types@~2.1.18: - version "2.1.18" - resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.18.tgz#6f323f60a83d11146f831ff11fd66e2fe5503bb8" - dependencies: - mime-db "~1.33.0" - -mime@^1.4.1: - version "1.6.0" - resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" - -minimatch@3.0.4, minimatch@^3.0.4: - version "3.0.4" - resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" - dependencies: - brace-expansion "^1.1.7" - -minimist@0.0.8: - version "0.0.8" - resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d" - -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" - -mkdirp@0.5.1: - version "0.5.1" - resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903" - dependencies: - minimist "0.0.8" - -mocha@5.1.1: - version "5.1.1" - resolved "https://registry.yarnpkg.com/mocha/-/mocha-5.1.1.tgz#b774c75609dac05eb48f4d9ba1d827b97fde8a7b" - dependencies: - browser-stdout "1.3.1" - commander "2.11.0" - debug "3.1.0" - diff "3.5.0" - escape-string-regexp "1.0.5" - glob "7.1.2" - growl "1.10.3" - he "1.1.1" - minimatch "3.0.4" - mkdirp "0.5.1" - supports-color "4.4.0" - -ms@2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" - -negotiator@0.6.1: - version "0.6.1" - resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.1.tgz#2b327184e8992101177b28563fb5e7102acd0ca9" - -node-dev@3.1.3: - version "3.1.3" - resolved "https://registry.yarnpkg.com/node-dev/-/node-dev-3.1.3.tgz#582719223ebdef5d63059e6a7fbcd2399fc0f84d" - dependencies: - dateformat "~1.0.4-1.2.3" - dynamic-dedupe "^0.2.0" - filewatcher "~3.0.0" - minimist "^1.1.3" - node-notifier "^4.0.2" - resolve "^1.0.0" - -node-emoji@^1.4.1: - version "1.8.1" - resolved "https://registry.yarnpkg.com/node-emoji/-/node-emoji-1.8.1.tgz#6eec6bfb07421e2148c75c6bba72421f8530a826" - dependencies: - lodash.toarray "^4.4.0" - -node-notifier@^4.0.2: - version "4.6.1" - resolved "https://registry.yarnpkg.com/node-notifier/-/node-notifier-4.6.1.tgz#056d14244f3dcc1ceadfe68af9cff0c5473a33f3" - dependencies: - cli-usage "^0.1.1" - growly "^1.2.0" - lodash.clonedeep "^3.0.0" - minimist "^1.1.1" - semver "^5.1.0" - shellwords "^0.1.0" - which "^1.0.5" - -normalize-package-data@^2.3.2, normalize-package-data@^2.3.4: - version "2.4.0" - resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-2.4.0.tgz#12f95a307d58352075a04907b84ac8be98ac012f" - dependencies: - hosted-git-info "^2.1.4" - is-builtin-module "^1.0.0" - semver "2 || 3 || 4 || 5" - validate-npm-package-license "^3.0.1" - -number-is-nan@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/number-is-nan/-/number-is-nan-1.0.1.tgz#097b602b53422a522c1afb8790318336941a011d" - -object-assign@^4.0.1: - version "4.1.1" - resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" - -object-keys@~0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-0.2.0.tgz#cddec02998b091be42bf1035ae32e49f1cb6ea67" - dependencies: - foreach "~2.0.1" - indexof "~0.0.1" - is "~0.2.6" - -on-finished@^2.1.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947" - dependencies: - ee-first "1.1.1" - -once@^1.3.0, once@^1.3.1, once@^1.4.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" - dependencies: - wrappy "1" - -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" - dependencies: - error-ex "^1.2.0" - -parseurl@^1.3.0: - version "1.3.2" - resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.2.tgz#fc289d4ed8993119460c156253262cdc8de65bf3" - -path-exists@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-2.1.0.tgz#0feb6c64f0fc518d9a754dd5efb62c7022761f4b" - dependencies: - pinkie-promise "^2.0.0" - -path-is-absolute@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" - -path-parse@^1.0.5: - version "1.0.5" - resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.5.tgz#3c1adf871ea9cd6c9431b6ea2bd74a0ff055c4c1" - -path-to-regexp@^1.1.1: - version "1.7.0" - resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-1.7.0.tgz#59fde0f435badacba103a84e9d3bc64e96b9937d" - dependencies: - isarray "0.0.1" - -path-type@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/path-type/-/path-type-1.1.0.tgz#59c44f7ee491da704da415da5a4070ba4f8fe441" - dependencies: - graceful-fs "^4.1.2" - pify "^2.0.0" - pinkie-promise "^2.0.0" - -pify@^2.0.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c" - -pinkie-promise@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/pinkie-promise/-/pinkie-promise-2.0.1.tgz#2135d6dfa7a358c069ac9b178776288228450ffa" - dependencies: - pinkie "^2.0.0" - -pinkie@^2.0.0: - version "2.0.4" - resolved "https://registry.yarnpkg.com/pinkie/-/pinkie-2.0.4.tgz#72556b80cfa0d48a974e80e77248e80ed4f7f870" - -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" - -pump@3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/pump/-/pump-3.0.0.tgz#b4a2116815bde2f4e1ea602354e8c75565107a64" - dependencies: - end-of-stream "^1.1.0" - once "^1.3.1" - -punycode@^1.4.1: - version "1.4.1" - resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e" - -qs@^6.5.1: - version "6.5.2" - resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36" - -read-pkg-up@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-1.0.1.tgz#9d63c13276c065918d57f002a57f40a1b643fb02" - dependencies: - find-up "^1.0.0" - read-pkg "^1.0.0" - -read-pkg@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-1.1.0.tgz#f5ffaa5ecd29cb31c0474bca7d756b6bb29e3f28" - dependencies: - load-json-file "^1.0.0" - normalize-package-data "^2.3.2" - path-type "^1.0.0" - -readable-stream@^2.0.5: - version "2.3.6" - resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.6.tgz#b11c27d88b8ff1fbe070643cf94b0c79ae1b0aaf" - dependencies: - core-util-is "~1.0.0" - inherits "~2.0.3" - isarray "~1.0.0" - process-nextick-args "~2.0.0" - safe-buffer "~5.1.1" - string_decoder "~1.1.1" - util-deprecate "~1.0.1" - -redent@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/redent/-/redent-1.0.0.tgz#cf916ab1fd5f1f16dfb20822dd6ec7f730c2afde" - dependencies: - indent-string "^2.1.0" - strip-indent "^1.0.1" - -redeyed@~1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/redeyed/-/redeyed-1.0.1.tgz#e96c193b40c0816b00aec842698e61185e55498a" - dependencies: - esprima "~3.0.0" - -repeating@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/repeating/-/repeating-2.0.1.tgz#5214c53a926d3552707527fbab415dbc08d06dda" - dependencies: - is-finite "^1.0.0" - -resolve@^1.0.0: - version "1.7.1" - resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.7.1.tgz#aadd656374fd298aee895bc026b8297418677fd3" - dependencies: - path-parse "^1.0.5" - -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" - -"semver@2 || 3 || 4 || 5", semver@^5.1.0: - version "5.5.0" - resolved "https://registry.yarnpkg.com/semver/-/semver-5.5.0.tgz#dc4bbc7a6ca9d916dee5d43516f0092b58f7b8ab" - -setprototypeof@1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.0.tgz#d0bd85536887b6fe7c0d818cb962d9d91c54e656" - -shellwords@^0.1.0: - version "0.1.1" - resolved "https://registry.yarnpkg.com/shellwords/-/shellwords-0.1.1.tgz#d6b9181c1a48d397324c84871efbcfc73fc0654b" - -signal-exit@^3.0.0: - version "3.0.2" - resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.2.tgz#b5fdc08f1287ea1178628e415e25132b73646c6d" - -spdx-correct@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-3.0.0.tgz#05a5b4d7153a195bc92c3c425b69f3b2a9524c82" - dependencies: - spdx-expression-parse "^3.0.0" - spdx-license-ids "^3.0.0" - -spdx-exceptions@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/spdx-exceptions/-/spdx-exceptions-2.1.0.tgz#2c7ae61056c714a5b9b9b2b2af7d311ef5c78fe9" - -spdx-expression-parse@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/spdx-expression-parse/-/spdx-expression-parse-3.0.0.tgz#99e119b7a5da00e05491c9fa338b7904823b41d0" - dependencies: - spdx-exceptions "^2.1.0" - spdx-license-ids "^3.0.0" - -spdx-license-ids@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-3.0.0.tgz#7a7cd28470cc6d3a1cfe6d66886f6bc430d3ac87" - -stackframe@^0.3.1: - version "0.3.1" - resolved "https://registry.yarnpkg.com/stackframe/-/stackframe-0.3.1.tgz#33aa84f1177a5548c8935533cbfeb3420975f5a4" - -"statuses@>= 1.4.0 < 2", statuses@^1.2.0: - version "1.5.0" - resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c" - -string_decoder@~1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8" - dependencies: - safe-buffer "~5.1.0" - -strip-ansi@^3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-3.0.1.tgz#6a385fb8853d952d5ff05d0e8aaf94278dc63dcf" - dependencies: - ansi-regex "^2.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" - dependencies: - is-utf8 "^0.2.0" - -strip-indent@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/strip-indent/-/strip-indent-1.0.1.tgz#0c7962a6adefa7bbd4ac366460a638552ae1a0a2" - dependencies: - get-stdin "^4.0.1" - -superagent@3.8.2: - version "3.8.2" - resolved "https://registry.yarnpkg.com/superagent/-/superagent-3.8.2.tgz#e4a11b9d047f7d3efeb3bbe536d9ec0021d16403" - dependencies: - component-emitter "^1.2.0" - cookiejar "^2.1.0" - debug "^3.1.0" - extend "^3.0.0" - form-data "^2.3.1" - formidable "^1.1.1" - methods "^1.1.1" - mime "^1.4.1" - qs "^6.5.1" - readable-stream "^2.0.5" - -supertest@3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/supertest/-/supertest-3.1.0.tgz#f9ebaf488e60f2176021ec580bdd23ad269e7bc6" - dependencies: - methods "~1.1.2" - superagent "3.8.2" - -supports-color@4.4.0: - version "4.4.0" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-4.4.0.tgz#883f7ddabc165142b2a61427f3352ded195d1a3e" - dependencies: - has-flag "^2.0.0" - -supports-color@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-2.0.0.tgz#535d045ce6b6363fa40117084629995e9df324c7" - -tldjs@2.3.1: - version "2.3.1" - resolved "https://registry.yarnpkg.com/tldjs/-/tldjs-2.3.1.tgz#cf09c3eb5d7403a9e214b7d65f3cf9651c0ab039" - dependencies: - punycode "^1.4.1" - -trim-newlines@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/trim-newlines/-/trim-newlines-1.0.0.tgz#5887966bb582a4503a41eb524f7d35011815a613" - -type-is@^1.5.5: - version "1.6.16" - resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.16.tgz#f89ce341541c672b25ee7ae3c73dee3b2be50194" - dependencies: - media-typer "0.3.0" - mime-types "~2.1.18" - -urijs@^1.19.0: - version "1.19.1" - resolved "https://registry.yarnpkg.com/urijs/-/urijs-1.19.1.tgz#5b0ff530c0cbde8386f6342235ba5ca6e995d25a" - -util-deprecate@~1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" - -validate-npm-package-license@^3.0.1: - version "3.0.3" - resolved "https://registry.yarnpkg.com/validate-npm-package-license/-/validate-npm-package-license-3.0.3.tgz#81643bcbef1bdfecd4623793dc4648948ba98338" - dependencies: - spdx-correct "^3.0.0" - spdx-expression-parse "^3.0.0" - -vary@^1.0.0: - version "1.1.2" - resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" - -which@^1.0.5: - version "1.3.0" - resolved "https://registry.yarnpkg.com/which/-/which-1.3.0.tgz#ff04bdfc010ee547d780bec38e1ac1c2777d253a" - 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" - -wrappy@1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" - -ws@5.1.1: - version "5.1.1" - resolved "https://registry.yarnpkg.com/ws/-/ws-5.1.1.tgz#1d43704689711ac1942fd2f283e38f825c4b8b95" - dependencies: - async-limiter "~1.0.0" - -xtend@~2.0.6: - version "2.0.6" - resolved "https://registry.yarnpkg.com/xtend/-/xtend-2.0.6.tgz#5ea657a6dba447069c2e59c58a1138cb0c5e6cee" - dependencies: - is-object "~0.1.2" - object-keys "~0.2.0"