From 355d3bf1d429b201b5aabacf576f3be0e9ea8517 Mon Sep 17 00:00:00 2001 From: Ted Spare Date: Thu, 14 Mar 2024 22:59:17 -0400 Subject: [PATCH 01/12] Add logs to server --- bin/server | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/bin/server b/bin/server index a6bba9d..42ed99d 100755 --- a/bin/server +++ b/bin/server @@ -2,10 +2,7 @@ import 'localenv'; import optimist from 'optimist'; - -import log from 'book'; import Debug from 'debug'; - import CreateServer from '../server'; const debug = Debug('localtunnel'); @@ -34,6 +31,7 @@ const argv = optimist .argv; if (argv.help) { + console.log("Showing help"); optimist.showHelp(); process.exit(); } @@ -49,19 +47,23 @@ server.listen(argv.port, argv.address, () => { }); process.on('SIGINT', () => { + console.error("SIGINT"); process.exit(); }); process.on('SIGTERM', () => { + console.error("SIGTERM"); process.exit(); }); process.on('uncaughtException', (err) => { - log.error(err); + console.error("err:"); + console.error(err); }); process.on('unhandledRejection', (reason, promise) => { - log.error(reason); + console.error("reason:"); + console.error(reason); }); // vim: ft=javascript -- 2.54.0 From 2cdd618814187335d5de0915df6d409062d2d4c9 Mon Sep 17 00:00:00 2001 From: Ted Spare Date: Thu, 14 Mar 2024 23:10:09 -0400 Subject: [PATCH 02/12] Add domain to start script --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 0693b8d..94cef3b 100644 --- a/package.json +++ b/package.json @@ -28,7 +28,7 @@ }, "scripts": { "test": "mocha --check-leaks --require esm './**/*.test.js'", - "start": "./bin/server", + "start": "./bin/server --domain tunnel.rubric.sh", "dev": "node-dev bin/server --port 3000" } } -- 2.54.0 From bc00ee9dab111e4fbf8e0666bd80614c9ba9d9a1 Mon Sep 17 00:00:00 2001 From: Ted Spare Date: Thu, 14 Mar 2024 23:22:41 -0400 Subject: [PATCH 03/12] Update server --- bin/server | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bin/server b/bin/server index 42ed99d..9f715b0 100755 --- a/bin/server +++ b/bin/server @@ -52,8 +52,8 @@ process.on('SIGINT', () => { }); process.on('SIGTERM', () => { - console.error("SIGTERM"); - process.exit(); + console.warn("SIGTERM"); + // process.exit(); }); process.on('uncaughtException', (err) => { -- 2.54.0 From 8744289c304ebb263a6c5c558b35fa185de96cd1 Mon Sep 17 00:00:00 2001 From: Ted Spare Date: Fri, 15 Mar 2024 09:50:39 -0400 Subject: [PATCH 04/12] Expose port --- Dockerfile | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Dockerfile b/Dockerfile index ddda30b..e397831 100644 --- a/Dockerfile +++ b/Dockerfile @@ -11,3 +11,5 @@ COPY . /app ENV NODE_ENV production ENTRYPOINT ["node", "-r", "esm", "./bin/server"] + +EXPOSE 1234 -- 2.54.0 From 675b527833e8cdc95ddd07ab9d84e5331ed4cda8 Mon Sep 17 00:00:00 2001 From: tedspare Date: Mon, 18 Mar 2024 23:22:59 -0400 Subject: [PATCH 05/12] 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" -- 2.54.0 From 18041ae3fb4febf0a9bd3798df46c19da95ac7b6 Mon Sep 17 00:00:00 2001 From: tedspare Date: Tue, 19 Mar 2024 22:05:08 -0400 Subject: [PATCH 06/12] Migrate to Bun websockets --- README.md | 70 ------------ client.ts | 41 +++++++ client/demo.ts => demo.ts | 0 lib/Client.test.js | 157 --------------------------- lib/ClientManager.test.js | 90 ---------------- lib/TunnelAgent.test.js | 183 ------------------------------- lib/agent.ts | 199 ---------------------------------- lib/client.ts | 142 ------------------------ lib/manager.ts | 102 ------------------ opus.ts | 110 ------------------- package.json | 17 +-- server.test.js | 109 ------------------- server.ts | 222 +++++++++++--------------------------- 13 files changed, 106 insertions(+), 1336 deletions(-) delete mode 100644 README.md create mode 100644 client.ts rename client/demo.ts => demo.ts (100%) delete mode 100644 lib/Client.test.js delete mode 100644 lib/ClientManager.test.js delete mode 100644 lib/TunnelAgent.test.js delete mode 100644 lib/agent.ts delete mode 100644 lib/client.ts delete mode 100644 lib/manager.ts delete mode 100644 opus.ts delete mode 100644 server.test.js diff --git a/README.md b/README.md deleted file mode 100644 index 9c829f0..0000000 --- a/README.md +++ /dev/null @@ -1,70 +0,0 @@ -# localtunnel-server - -[![Build Status](https://travis-ci.org/localtunnel/server.svg?branch=master)](https://travis-ci.org/localtunnel/server) - -localtunnel exposes your localhost to the world for easy testing and sharing! No need to mess with DNS or deploy just to have others test out your changes. - -This repo is the server component. If you are just looking for the CLI localtunnel app, see (https://github.com/localtunnel/localtunnel). - -## overview ## - -The default localtunnel client connects to the `localtunnel.me` server. You can, however, easily set up and run your own server. In order to run your own localtunnel server you must ensure that your server can meet the following requirements: - -* You can set up DNS entries for your `domain.tld` and `*.domain.tld` (or `sub.domain.tld` and `*.sub.domain.tld`). -* The server can accept incoming TCP connections for any non-root TCP port (i.e. ports over 1000). - -The above are important as the client will ask the server for a subdomain under a particular domain. The server will listen on any OS-assigned TCP port for client connections. - -#### setup - -```shell -# pick a place where the files will live -git clone git://github.com/defunctzombie/localtunnel-server.git -cd localtunnel-server -npm install - -# server set to run on port 1234 -bin/server --port 1234 -``` - -The localtunnel server is now running and waiting for client requests on port 1234. You will most likely want to set up a reverse proxy to listen on port 80 (or start localtunnel on port 80 directly). - -**NOTE** By default, localtunnel will use subdomains for clients, if you plan to host your localtunnel server itself on a subdomain you will need to use the _--domain_ option and specify the domain name behind which you are hosting localtunnel. (i.e. my-localtunnel-server.example.com) - -#### use your server - -You can now use your domain with the `--host` flag for the `lt` client. - -```shell -lt --host http://sub.example.tld:1234 --port 9000 -``` - -You will be assigned a URL similar to `heavy-puma-9.sub.example.com:1234`. - -If your server is acting as a reverse proxy (i.e. nginx) and is able to listen on port 80, then you do not need the `:1234` part of the hostname for the `lt` client. - -## REST API - -### POST /api/tunnels - -Create a new tunnel. A LocalTunnel client posts to this enpoint to request a new tunnel with a specific name or a randomly assigned name. - -### GET /api/status - -General server information. - -## Deploy - -You can deploy your own localtunnel server using the prebuilt docker image. - -**Note** This assumes that you have a proxy in front of the server to handle the http(s) requests and forward them to the localtunnel server on port 3000. You can use our [localtunnel-nginx](https://github.com/localtunnel/nginx) to accomplish this. - -If you do not want ssl support for your own tunnel (not recommended), then you can just run the below with `--port 80` instead. - -``` -docker run -d \ - --restart always \ - --name localtunnel \ - --net host \ - defunctzombie/localtunnel-server:latest --port 3000 -``` diff --git a/client.ts b/client.ts new file mode 100644 index 0000000..28e57aa --- /dev/null +++ b/client.ts @@ -0,0 +1,41 @@ +import { parseArgs } from "util"; + +const serverUrl = `ws://localhost:1234?new`; + +async function main({ url }: { url: string }) { + const socket = new WebSocket(serverUrl); + + socket.addEventListener("message", (event) => { + const data = JSON.parse(event.data); + console.log("message:", data); + + if (data.method) { + fetch(`${url}${data.path}`, { + method: data.method, + headers: data.headers, + }) + .then((res) => res.text()) + .then(socket.send); + } + }); + + socket.addEventListener("open", (event) => { + console.log("socket ready:", !!(event.target as any).readyState); + socket.ping(); + }); +} + +const { values } = parseArgs({ + args: Bun.argv, + options: { + port: { + type: "string", + required: true, + short: "p", + }, + }, + allowPositionals: true, +}); + +if (!values.port) throw "pass --port 3000"; +main({ url: `localhost:${values.port}` }); diff --git a/client/demo.ts b/demo.ts similarity index 100% rename from client/demo.ts rename to demo.ts diff --git a/lib/Client.test.js b/lib/Client.test.js deleted file mode 100644 index 5a0afd0..0000000 --- a/lib/Client.test.js +++ /dev/null @@ -1,157 +0,0 @@ -import assert from 'assert'; -import http from 'http'; -import { Duplex } from 'stream'; -import WebSocket from 'ws'; -import net from 'net'; - -import Client from './Client'; - -class DummySocket extends Duplex { - constructor(options) { - super(options); - } - - _write(chunk, encoding, callback) { - callback(); - } - - _read(size) { - this.push('HTTP/1.1 304 Not Modified\r\nX-Powered-By: dummy\r\n\r\n\r\n'); - this.push(null); - } -} - -class DummyWebsocket extends Duplex { - constructor(options) { - super(options); - this.sentHeader = false; - } - - _write(chunk, encoding, callback) { - const str = chunk.toString(); - // if chunk contains `GET / HTTP/1.1` -> queue headers - // otherwise echo back received data - if (str.indexOf('GET / HTTP/1.1') === 0) { - const arr = [ - 'HTTP/1.1 101 Switching Protocols', - 'Upgrade: websocket', - 'Connection: Upgrade', - ]; - this.push(arr.join('\r\n')); - this.push('\r\n\r\n'); - } - else { - this.push(str); - } - callback(); - } - - _read(size) { - // nothing to implement - } -} - -class DummyAgent extends http.Agent { - constructor() { - super(); - } - - createConnection(options, cb) { - cb(null, new DummySocket()); - } -} - -describe('Client', () => { - it('should handle request', async () => { - const agent = new DummyAgent(); - const client = new Client({ agent }); - - const server = http.createServer((req, res) => { - client.handleRequest(req, res); - }); - - await new Promise(resolve => server.listen(resolve)); - - const address = server.address(); - const opt = { - host: 'localhost', - port: address.port, - path: '/', - }; - - const res = await new Promise((resolve) => { - const req = http.get(opt, (res) => { - resolve(res); - }); - req.end(); - }); - assert.equal(res.headers['x-powered-by'], 'dummy'); - server.close(); - }); - - it('should handle upgrade', async () => { - // need a websocket server and a socket for it - class DummyWebsocketAgent extends http.Agent { - constructor() { - super(); - } - - createConnection(options, cb) { - cb(null, new DummyWebsocket()); - } - } - - const agent = new DummyWebsocketAgent(); - const client = new Client({ agent }); - - const server = http.createServer(); - server.on('upgrade', (req, socket, head) => { - client.handleUpgrade(req, socket); - }); - - await new Promise(resolve => server.listen(resolve)); - - const address = server.address(); - - const netClient = await new Promise((resolve) => { - const newClient = net.createConnection({ port: address.port }, () => { - resolve(newClient); - }); - }); - - const out = [ - 'GET / HTTP/1.1', - 'Connection: Upgrade', - 'Upgrade: websocket' - ]; - - netClient.write(out.join('\r\n') + '\r\n\r\n'); - - { - const data = await new Promise((resolve) => { - netClient.once('data', (chunk) => { - resolve(chunk.toString()); - }); - }); - const exp = [ - 'HTTP/1.1 101 Switching Protocols', - 'Upgrade: websocket', - 'Connection: Upgrade', - ]; - assert.equal(exp.join('\r\n') + '\r\n\r\n', data); - } - - { - netClient.write('foobar'); - const data = await new Promise((resolve) => { - netClient.once('data', (chunk) => { - resolve(chunk.toString()); - }); - }); - assert.equal('foobar', data); - } - - netClient.destroy(); - server.close(); - }); -}); diff --git a/lib/ClientManager.test.js b/lib/ClientManager.test.js deleted file mode 100644 index d85abe5..0000000 --- a/lib/ClientManager.test.js +++ /dev/null @@ -1,90 +0,0 @@ -import assert from 'assert'; -import net from 'net'; - -import ClientManager from './ClientManager'; - -describe('ClientManager', () => { - it('should construct with no tunnels', () => { - const manager = new ClientManager(); - assert.equal(manager.stats.tunnels, 0); - }); - - it('should create a new client with random id', async () => { - const manager = new ClientManager(); - const client = await manager.newClient(); - assert(manager.hasClient(client.id)); - manager.removeClient(client.id); - }); - - it('should create a new client with id', async () => { - const manager = new ClientManager(); - const client = await manager.newClient('foobar'); - assert(manager.hasClient('foobar')); - manager.removeClient('foobar'); - }); - - it('should create a new client with random id if previous exists', async () => { - const manager = new ClientManager(); - const clientA = await manager.newClient('foobar'); - const clientB = await manager.newClient('foobar'); - assert(clientA.id, 'foobar'); - assert(manager.hasClient(clientB.id)); - assert(clientB.id != clientA.id); - manager.removeClient(clientB.id); - manager.removeClient('foobar'); - }); - - it('should remove client once it goes offline', async () => { - const manager = new ClientManager(); - const client = await manager.newClient('foobar'); - - const socket = await new Promise((resolve) => { - const netClient = net.createConnection({ port: client.port }, () => { - resolve(netClient); - }); - }); - const closePromise = new Promise(resolve => socket.once('close', resolve)); - socket.end(); - await closePromise; - - // should still have client - grace period has not expired - assert(manager.hasClient('foobar')); - - // wait past grace period (1s) - await new Promise(resolve => setTimeout(resolve, 1500)); - assert(!manager.hasClient('foobar')); - }).timeout(5000); - - it('should remove correct client once it goes offline', async () => { - const manager = new ClientManager(); - const clientFoo = await manager.newClient('foo'); - const clientBar = await manager.newClient('bar'); - - const socket = await new Promise((resolve) => { - const netClient = net.createConnection({ port: clientFoo.port }, () => { - resolve(netClient); - }); - }); - - await new Promise(resolve => setTimeout(resolve, 1500)); - - // foo should still be ok - assert(manager.hasClient('foo')); - - // clientBar shound be removed - nothing connected to it - assert(!manager.hasClient('bar')); - - manager.removeClient('foo'); - socket.end(); - }).timeout(5000); - - it('should remove clients if they do not connect within 5 seconds', async () => { - const manager = new ClientManager(); - const clientFoo = await manager.newClient('foo'); - assert(manager.hasClient('foo')); - - // wait past grace period (1s) - await new Promise(resolve => setTimeout(resolve, 1500)); - assert(!manager.hasClient('foo')); - }).timeout(5000); -}); diff --git a/lib/TunnelAgent.test.js b/lib/TunnelAgent.test.js deleted file mode 100644 index 9505f35..0000000 --- a/lib/TunnelAgent.test.js +++ /dev/null @@ -1,183 +0,0 @@ -import http from 'http'; -import net from 'net'; -import assert from 'assert'; - -import TunnelAgent from './TunnelAgent'; - -describe('TunnelAgent', () => { - it('should create an empty agent', async () => { - const agent = new TunnelAgent(); - assert.equal(agent.started, false); - - const info = await agent.listen(); - assert.ok(info.port > 0); - agent.destroy(); - }); - - it('should create a new server and accept connections', async () => { - const agent = new TunnelAgent(); - assert.equal(agent.started, false); - - const info = await agent.listen(); - const sock = net.createConnection({ port: info.port }); - - // in this test we wait for the socket to be connected - await new Promise(resolve => sock.once('connect', resolve)); - - const agentSock = await new Promise((resolve, reject) => { - agent.createConnection({}, (err, sock) => { - if (err) { - reject(err); - } - resolve(sock); - }); - }); - - agentSock.write('foo'); - await new Promise(resolve => sock.once('readable', resolve)); - - assert.equal('foo', sock.read().toString()); - agent.destroy(); - sock.destroy(); - }); - - it('should reject connections over the max', async () => { - const agent = new TunnelAgent({ - maxTcpSockets: 2, - }); - assert.equal(agent.started, false); - - const info = await agent.listen(); - const sock1 = net.createConnection({ port: info.port }); - const sock2 = net.createConnection({ port: info.port }); - - // two valid socket connections - const p1 = new Promise(resolve => sock1.once('connect', resolve)); - const p2 = new Promise(resolve => sock2.once('connect', resolve)); - await Promise.all([p1, p2]); - - const sock3 = net.createConnection({ port: info.port }); - const p3 = await new Promise(resolve => sock3.once('close', resolve)); - - agent.destroy(); - sock1.destroy(); - sock2.destroy(); - sock3.destroy(); - }); - - it('should queue createConnection requests', async () => { - const agent = new TunnelAgent(); - assert.equal(agent.started, false); - - const info = await agent.listen(); - - // create a promise for the next connection - let fulfilled = false; - const waitSockPromise = new Promise((resolve, reject) => { - agent.createConnection({}, (err, sock) => { - fulfilled = true; - if (err) { - reject(err); - } - resolve(sock); - }); - }); - - // check that the next socket is not yet available - await new Promise(resolve => setTimeout(resolve, 500)); - assert(!fulfilled); - - // connect, this will make a socket available - const sock = net.createConnection({ port: info.port }); - await new Promise(resolve => sock.once('connect', resolve)); - - const anotherAgentSock = await waitSockPromise; - agent.destroy(); - sock.destroy(); - }); - - it('should should emit a online event when a socket connects', async () => { - const agent = new TunnelAgent(); - const info = await agent.listen(); - - const onlinePromise = new Promise(resolve => agent.once('online', resolve)); - - const sock = net.createConnection({ port: info.port }); - await new Promise(resolve => sock.once('connect', resolve)); - - await onlinePromise; - agent.destroy(); - sock.destroy(); - }); - - it('should emit offline event when socket disconnects', async () => { - const agent = new TunnelAgent(); - const info = await agent.listen(); - - const offlinePromise = new Promise(resolve => agent.once('offline', resolve)); - - const sock = net.createConnection({ port: info.port }); - await new Promise(resolve => sock.once('connect', resolve)); - - sock.end(); - await offlinePromise; - agent.destroy(); - sock.destroy(); - }); - - it('should emit offline event only when last socket disconnects', async () => { - const agent = new TunnelAgent(); - const info = await agent.listen(); - - const offlinePromise = new Promise(resolve => agent.once('offline', resolve)); - - const sockA = net.createConnection({ port: info.port }); - await new Promise(resolve => sockA.once('connect', resolve)); - const sockB = net.createConnection({ port: info.port }); - await new Promise(resolve => sockB.once('connect', resolve)); - - sockA.end(); - - const timeout = new Promise(resolve => setTimeout(resolve, 500)); - await Promise.race([offlinePromise, timeout]); - - sockB.end(); - await offlinePromise; - - agent.destroy(); - }); - - it('should error an http request', async () => { - class ErrorAgent extends http.Agent { - constructor() { - super(); - } - - createConnection(options, cb) { - cb(new Error('foo')); - } - } - - const agent = new ErrorAgent(); - - const opt = { - host: 'localhost', - port: 1234, - path: '/', - agent: agent, - }; - - const err = await new Promise((resolve) => { - const req = http.get(opt, (res) => {}); - req.once('error', resolve); - }); - assert.equal(err.message, 'foo'); - }); - - it('should return stats', async () => { - const agent = new TunnelAgent(); - assert.deepEqual(agent.stats(), { - connectedSockets: 0, - }); - }); -}); diff --git a/lib/agent.ts b/lib/agent.ts deleted file mode 100644 index 2b57063..0000000 --- a/lib/agent.ts +++ /dev/null @@ -1,199 +0,0 @@ -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 deleted file mode 100644 index cb8d9b0..0000000 --- a/lib/client.ts +++ /dev/null @@ -1,142 +0,0 @@ -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 deleted file mode 100644 index 87b8f92..0000000 --- a/lib/manager.ts +++ /dev/null @@ -1,102 +0,0 @@ -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 deleted file mode 100644 index 63dbd45..0000000 --- a/opus.ts +++ /dev/null @@ -1,110 +0,0 @@ -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 58996ad..ab92f21 100644 --- a/package.json +++ b/package.json @@ -8,22 +8,13 @@ "url": "https://github.com/rubriclab/tunnel" }, "dependencies": { - "debug": "3.1.0", - "human-id": "^4.1.1", - "human-readable-ids": "1.0.3", - "koa": "2.5.1", - "koa-router": "7.4.0", - "localenv": "0.2.2", - "optimist": "0.6.1", - "pump": "3.0.0", - "tldjs": "2.3.1" + "human-id": "^4.1.1" }, "devDependencies": { - "@types/bun": "^1.0.8", - "ws": "5.1.1" + "@types/bun": "^1.0.8" }, "scripts": { - "start": "bun run opus.ts", - "dev": "bun run --watch index.ts --domain ted.rubric.sh --port 3001" + "start": "bun run server.ts", + "lt": "bun run client/index.ts" } } diff --git a/server.test.js b/server.test.js deleted file mode 100644 index 10bf8bc..0000000 --- a/server.test.js +++ /dev/null @@ -1,109 +0,0 @@ -import request from 'supertest'; -import assert from 'assert'; -import { Server as WebSocketServer } from 'ws'; -import WebSocket from 'ws'; -import net from 'net'; - -import createServer from './server'; - -describe('Server', () => { - it('server starts and stops', async () => { - const server = createServer(); - await new Promise(resolve => server.listen(resolve)); - await new Promise(resolve => server.close(resolve)); - }); - - it('should redirect root requests to landing page', async () => { - const server = createServer(); - const res = await request(server).get('/'); - assert.equal('https://localtunnel.github.io/www/', res.headers.location); - }); - - it('should support custom base domains', async () => { - const server = createServer({ - domain: 'domain.example.com', - }); - - const res = await request(server).get('/'); - assert.equal('https://localtunnel.github.io/www/', res.headers.location); - }); - - it('reject long domain name requests', async () => { - const server = createServer(); - const res = await request(server).get('/thisdomainisoutsidethesizeofwhatweallowwhichissixtythreecharacters'); - assert.equal(res.body.message, 'Invalid subdomain. Subdomains must be lowercase and between 4 and 63 alphanumeric characters.'); - }); - - it('should upgrade websocket requests', async () => { - const hostname = 'websocket-test'; - const server = createServer({ - domain: 'example.com', - }); - await new Promise(resolve => server.listen(resolve)); - - const res = await request(server).get('/websocket-test'); - const localTunnelPort = res.body.port; - - const wss = await new Promise((resolve) => { - const wsServer = new WebSocketServer({ port: 0 }, () => { - resolve(wsServer); - }); - }); - - const websocketServerPort = wss.address().port; - - const ltSocket = net.createConnection({ port: localTunnelPort }); - const wsSocket = net.createConnection({ port: websocketServerPort }); - ltSocket.pipe(wsSocket).pipe(ltSocket); - - wss.once('connection', (ws) => { - ws.once('message', (message) => { - ws.send(message); - }); - }); - - const ws = new WebSocket('http://localhost:' + server.address().port, { - headers: { - host: hostname + '.example.com', - } - }); - - ws.on('open', () => { - ws.send('something'); - }); - - await new Promise((resolve) => { - ws.once('message', (msg) => { - assert.equal(msg, 'something'); - resolve(); - }); - }); - - wss.close(); - await new Promise(resolve => server.close(resolve)); - }); - - it('should support the /api/tunnels/:id/status endpoint', async () => { - const server = createServer(); - await new Promise(resolve => server.listen(resolve)); - - // no such tunnel yet - const res = await request(server).get('/api/tunnels/foobar-test/status'); - assert.equal(res.statusCode, 404); - - // request a new client called foobar-test - { - const res = await request(server).get('/foobar-test'); - } - - { - const res = await request(server).get('/api/tunnels/foobar-test/status'); - assert.equal(res.statusCode, 200); - assert.deepEqual(res.body, { - connected_sockets: 0, - }); - } - - await new Promise(resolve => server.close(resolve)); - }); -}); \ No newline at end of file diff --git a/server.ts b/server.ts index 093776b..c07a17f 100644 --- a/server.ts +++ b/server.ts @@ -1,178 +1,78 @@ -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"; +import { ServerWebSocket, serve } from "bun"; +import { uid, wait } from "./utils"; -export type Options = { - domain: string; - secure: boolean; - landing?: string; - max_tcp_sockets?: number; -}; +type Client = { id: string }; -const debug = Debug("localtunnel:server"); +const port = 1234; +const protocol = "http"; +const clients = new Map>(); +const clientData = new Map(); -export const CreateServer = (opt: Options) => { - opt = opt || {}; +serve({ + port, + fetch: async (req, server) => { + const reqUrl = new URL(req.url); - 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; + if (reqUrl.searchParams.has("new")) { + const upgraded = server.upgrade(req, { data: { id: uid() } }); + if (upgraded) return; + else return new Response("Upgrade failed :(", { status: 500 }); } - const stats = client.stats(); - ctx.body = { - connected_sockets: stats.connectedSockets, - }; - }); + console.log("user req:", req.url); + const subdomain = reqUrl.hostname.split(".")[0]; - 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; + if (!clients.has(subdomain)) { + console.log(`\x1b[31m${subdomain} not found \x1b[0m`); + return new Response("client not found :(", { status: 404 }); } - const isNewClientRequest = ctx.query["new"] !== undefined; - if (isNewClientRequest) { - const reqId = hri.random(); + // The magic: forward the req to the client + const client = clients.get(subdomain)!; + const { method, url, headers } = req; + const path = new URL(url).pathname; + client.send(JSON.stringify({ method, path, headers })); - debug("making new client with id %s", reqId); + // Wait for the client to cache its response above + await wait(1); - const info = await manager.newClient(reqId); + let retries = 5; + let res = clientData.get(subdomain); - const url = schema + "://" + info.id + "." + ctx.request.host; + while (!res) { + await wait(1000); + retries--; - info.url = url; - ctx.body = info; + res = clientData.get(subdomain); - return; + if (retries < 1) { + console.log(`\x1b[31m${subdomain} not responding \x1b[0m`); + return new Response("client not responding :(", { status: 500 }); + } } - // no new client request, send to landing page - ctx.redirect(landingPage); - }); + return new Response(res, { status: 200 }); + }, + websocket: { + open(ws) { + console.log("connecting to", ws.data.id); + clients.set(ws.data.id, ws); + ws.send( + JSON.stringify({ + id: ws.data.id, + url: `${protocol}://${ws.data.id}.localhost:${port}`, + }) + ); + }, + message(ws, message) { + console.log("message from", ws.data.id, message); + clientData.set(ws.data.id, message); + }, + close(ws) { + console.log(`closing ${ws.data.id}`); + clients.delete(ws.data.id); + }, + }, +}); - // 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; -}; +console.log(`Websocket server on port ${port}`); -- 2.54.0 From 27991c991743fdb7a9351b4ac98177472ad2bc1a Mon Sep 17 00:00:00 2001 From: tedspare Date: Tue, 19 Mar 2024 22:05:28 -0400 Subject: [PATCH 07/12] Bun install --- bun.lockb | Bin 24849 -> 3046 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/bun.lockb b/bun.lockb index 24b7f8d7c3745f4abece1a6a720b57f1620a7df7..ea105d1ac19ec739d2974e0c912af7fe46477003 100755 GIT binary patch delta 945 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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 -- 2.54.0 From af3d9577523da73569a7b4a93dea5f5a7696f47a Mon Sep 17 00:00:00 2001 From: tedspare Date: Tue, 19 Mar 2024 22:06:32 -0400 Subject: [PATCH 08/12] Add demo script --- package.json | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index ab92f21..46e6660 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,8 @@ "@types/bun": "^1.0.8" }, "scripts": { - "start": "bun run server.ts", - "lt": "bun run client/index.ts" + "server": "bun run server.ts", + "lt": "bun run client/index.ts", + "demo": "bun run demo.ts" } } -- 2.54.0 From 60b3960f52dffe3b4e883d7535c73764a03b951a Mon Sep 17 00:00:00 2001 From: tedspare Date: Tue, 19 Mar 2024 22:17:49 -0400 Subject: [PATCH 09/12] Prepare for deploy --- Dockerfile | 4 ++-- client.ts | 4 +++- fly.toml | 12 +----------- package.json | 2 +- 4 files changed, 7 insertions(+), 15 deletions(-) diff --git a/Dockerfile b/Dockerfile index cb30c94..3e0d6cb 100644 --- a/Dockerfile +++ b/Dockerfile @@ -10,6 +10,6 @@ RUN bun install --frozen-lockfile --production COPY . /app ENV NODE_ENV production -ENTRYPOINT ["bun", "run", "start"] +ENTRYPOINT ["bun", "run", "server"] -EXPOSE 1234 1235 \ No newline at end of file +EXPOSE 1234 \ No newline at end of file diff --git a/client.ts b/client.ts index 28e57aa..5cd5562 100644 --- a/client.ts +++ b/client.ts @@ -15,7 +15,9 @@ async function main({ url }: { url: string }) { headers: data.headers, }) .then((res) => res.text()) - .then(socket.send); + .then((res) => { + socket.send(res); + }); } }); diff --git a/fly.toml b/fly.toml index 2d334c8..a031bec 100644 --- a/fly.toml +++ b/fly.toml @@ -1,7 +1,5 @@ # 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' @@ -19,12 +17,4 @@ primary_region = 'yul' [[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 + cpus = 1 \ No newline at end of file diff --git a/package.json b/package.json index 46e6660..200a69b 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,7 @@ }, "scripts": { "server": "bun run server.ts", - "lt": "bun run client/index.ts", + "lt": "bun run client.ts", "demo": "bun run demo.ts" } } -- 2.54.0 From ba1b5ec6a14c2f31f36fe5c5579e17ac63d3fd60 Mon Sep 17 00:00:00 2001 From: tedspare Date: Tue, 19 Mar 2024 22:27:58 -0400 Subject: [PATCH 10/12] Add domain config --- client.ts | 11 +++++++---- server.ts | 4 +++- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/client.ts b/client.ts index 5cd5562..3a55ce2 100644 --- a/client.ts +++ b/client.ts @@ -1,8 +1,7 @@ import { parseArgs } from "util"; -const serverUrl = `ws://localhost:1234?new`; - -async function main({ url }: { url: string }) { +async function main({ url, domain }: { url: string; domain?: string }) { + const serverUrl = `ws://${domain || "localhost:1234"}?new`; const socket = new WebSocket(serverUrl); socket.addEventListener("message", (event) => { @@ -35,9 +34,13 @@ const { values } = parseArgs({ required: true, short: "p", }, + domain: { + type: "string", + short: "d", + }, }, allowPositionals: true, }); if (!values.port) throw "pass --port 3000"; -main({ url: `localhost:${values.port}` }); +main({ url: `localhost:${values.port}`, domain: values.domain }); diff --git a/server.ts b/server.ts index c07a17f..4b33bb0 100644 --- a/server.ts +++ b/server.ts @@ -5,6 +5,8 @@ type Client = { id: string }; const port = 1234; const protocol = "http"; +const domain = Bun.env.DOMAIN || `localhost:${port}`; +console.log("domain:", domain); const clients = new Map>(); const clientData = new Map(); @@ -60,7 +62,7 @@ serve({ ws.send( JSON.stringify({ id: ws.data.id, - url: `${protocol}://${ws.data.id}.localhost:${port}`, + url: `${protocol}://${ws.data.id}.${domain}`, }) ); }, -- 2.54.0 From 07f1ae7eb77a87d59257901df9acae3382885fe3 Mon Sep 17 00:00:00 2001 From: tedspare Date: Tue, 19 Mar 2024 22:31:29 -0400 Subject: [PATCH 11/12] Update name --- package.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 200a69b..ca7d5e2 100644 --- a/package.json +++ b/package.json @@ -1,11 +1,11 @@ { - "name": "tunnel", + "name": "bunl", "description": "expose localhost to the world", "version": "0.0.0", "license": "MIT", "repository": { "type": "git", - "url": "https://github.com/rubriclab/tunnel" + "url": "https://github.com/rubriclab/bunl" }, "dependencies": { "human-id": "^4.1.1" @@ -16,6 +16,6 @@ "scripts": { "server": "bun run server.ts", "lt": "bun run client.ts", - "demo": "bun run demo.ts" + "demo": "bun run --hot demo.ts" } } -- 2.54.0 From 1e7ba9606f3d3c34759da741617643b783461221 Mon Sep 17 00:00:00 2001 From: DexterStorey Date: Wed, 20 Mar 2024 05:13:57 -0400 Subject: [PATCH 12/12] subdomain arg --- client.ts | 10 +++++++--- server.ts | 3 ++- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/client.ts b/client.ts index 3a55ce2..1b7d624 100644 --- a/client.ts +++ b/client.ts @@ -1,7 +1,7 @@ import { parseArgs } from "util"; -async function main({ url, domain }: { url: string; domain?: string }) { - const serverUrl = `ws://${domain || "localhost:1234"}?new`; +async function main({ url, domain, subdomain }: { url: string; domain?: string, subdomain?: string }) { + const serverUrl = `ws://${domain || "localhost:1234"}?new${subdomain ? `&subdomain=${subdomain}` : ""}`; const socket = new WebSocket(serverUrl); socket.addEventListener("message", (event) => { @@ -38,9 +38,13 @@ const { values } = parseArgs({ type: "string", short: "d", }, + subdomain: { + type: "string", + short: "s", + }, }, allowPositionals: true, }); if (!values.port) throw "pass --port 3000"; -main({ url: `localhost:${values.port}`, domain: values.domain }); +main({ url: `localhost:${values.port}`, domain: values.domain, subdomain: values.subdomain }); diff --git a/server.ts b/server.ts index 4b33bb0..95ba200 100644 --- a/server.ts +++ b/server.ts @@ -16,7 +16,8 @@ serve({ const reqUrl = new URL(req.url); if (reqUrl.searchParams.has("new")) { - const upgraded = server.upgrade(req, { data: { id: uid() } }); + const id = reqUrl.searchParams.get("subdomain") || uid(); + const upgraded = server.upgrade(req, { data: { id } }); if (upgraded) return; else return new Response("Upgrade failed :(", { status: 500 }); } -- 2.54.0