From c624943877a7679722bd72964045869dd562a2b6 Mon Sep 17 00:00:00 2001 From: mgudesblatart Date: Wed, 28 Feb 2018 21:10:33 -0500 Subject: [PATCH 01/20] added subdomain support --- README.md | 10 +- bin/server | 8 +- package-lock.json | 2134 +++++++++++++++++++++++++++++++++++++++++++++ server.js | 29 +- test/simple.js | 42 +- test/websocket.js | 34 +- 6 files changed, 2243 insertions(+), 14 deletions(-) create mode 100644 package-lock.json diff --git a/README.md b/README.md index c557c94..534d26b 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ localtunnel exposes your localhost to the world for easy testing and sharing! No This repo is the server component. If you are just looking for the CLI localtunnel app, see (https://github.com/localtunnel/localtunnel). -## overview ## +## 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: @@ -15,7 +15,7 @@ The default localtunnel client connects to the `localtunnel.me` server. You can, 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 +## setup ```shell # pick a place where the files will live @@ -29,7 +29,7 @@ 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). -#### use your server +## use your server You can now use your domain with the `--host` flag for the `lt` client. @@ -41,6 +41,8 @@ You will be assigned a URL similar to `qdci.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. +Note that to use sub-hosts you should pass `--sub-host` option to server + ## Deploy You can deploy your own localtunnel server using the prebuilt docker image. @@ -54,5 +56,5 @@ docker run -d \ --restart always \ --name localtunnel \ --net host \ - defunctzombie/localtunnel-server:latest --port 3000 + defunctzombie/localtunnel-server:latest --port 3000 --sub-host sub ``` diff --git a/bin/server b/bin/server index 16a943d..8a15e4e 100755 --- a/bin/server +++ b/bin/server @@ -17,6 +17,10 @@ const argv = optimist default: '80', describe: 'listen on this port for outside requests' }) + .options('sub-host', { + describe: 'additionally listen on these sub hosts for outside requests', + type: 'array' + }) .options('max-sockets', { default: 10, describe: 'maximum number of tcp sockets each client is allowed to establish at one time (the tunnels)' @@ -30,7 +34,9 @@ if (argv.help) { const server = require('../server')({ max_tcp_sockets: argv['max-sockets'], - secure: argv.secure + secure: argv.secure, + port: argv.port, + subHost: argv['sub-host'] }); server.listen(argv.port, () => { diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..2109975 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,2134 @@ +{ + "name": "localtunnel-server", + "version": "0.0.8", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "accepts": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.4.tgz", + "integrity": "sha1-hiRnWMfdbSGmR0/whKR0DsBesh8=", + "requires": { + "mime-types": "2.1.18", + "negotiator": "0.6.1" + } + }, + "ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=" + }, + "ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=" + }, + "ansicolors": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/ansicolors/-/ansicolors-0.2.1.tgz", + "integrity": "sha1-vgiVmQl7dKXJxKhKDNvNtivYeu8=", + "dev": true + }, + "any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha1-q8av7tzqUugJzcA3au0845Y10X8=" + }, + "array-find-index": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-find-index/-/array-find-index-1.0.2.tgz", + "integrity": "sha1-3wEKoSh+Fku9pvlyOwqWoexBh6E=", + "dev": true + }, + "asn1": { + "version": "0.1.11", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.1.11.tgz", + "integrity": "sha1-VZvhg3bQik7E2+gId9J4GGObLfc=", + "dev": true + }, + "assert-plus": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-0.1.5.tgz", + "integrity": "sha1-7nQAlBMALYTOxyGcasgRgS5yMWA=", + "dev": true + }, + "async": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/async/-/async-2.6.0.tgz", + "integrity": "sha512-xAfGg1/NTLBBKlHFmnd7PlmUW9KhVQIUuSrYem9xzFUZy13ScvtyGGejaae9iAVRiRq9+Cx7DPFaAAhCpyxyPw==", + "dev": true, + "requires": { + "lodash": "4.17.5" + } + }, + "aws-sign2": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.6.0.tgz", + "integrity": "sha1-FDQt0428yU0OW4fXY81jYSwOeU8=", + "dev": true + }, + "babel-code-frame": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-code-frame/-/babel-code-frame-6.26.0.tgz", + "integrity": "sha1-Y/1D99weO7fONZR9uP42mj9Yx0s=", + "requires": { + "chalk": "1.1.3", + "esutils": "2.0.2", + "js-tokens": "3.0.2" + } + }, + "babel-core": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-core/-/babel-core-6.26.0.tgz", + "integrity": "sha1-rzL3izGm/O8RnIew/Y2XU/A6C7g=", + "requires": { + "babel-code-frame": "6.26.0", + "babel-generator": "6.26.1", + "babel-helpers": "6.24.1", + "babel-messages": "6.23.0", + "babel-register": "6.26.0", + "babel-runtime": "6.26.0", + "babel-template": "6.26.0", + "babel-traverse": "6.26.0", + "babel-types": "6.26.0", + "babylon": "6.18.0", + "convert-source-map": "1.5.1", + "debug": "2.6.9", + "json5": "0.5.1", + "lodash": "4.17.5", + "minimatch": "3.0.4", + "path-is-absolute": "1.0.1", + "private": "0.1.8", + "slash": "1.0.0", + "source-map": "0.5.7" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + } + } + } + }, + "babel-generator": { + "version": "6.26.1", + "resolved": "https://registry.npmjs.org/babel-generator/-/babel-generator-6.26.1.tgz", + "integrity": "sha512-HyfwY6ApZj7BYTcJURpM5tznulaBvyio7/0d4zFOeMPUmfxkCjHocCuoLa2SAGzBI8AREcH3eP3758F672DppA==", + "requires": { + "babel-messages": "6.23.0", + "babel-runtime": "6.26.0", + "babel-types": "6.26.0", + "detect-indent": "4.0.0", + "jsesc": "1.3.0", + "lodash": "4.17.5", + "source-map": "0.5.7", + "trim-right": "1.0.1" + } + }, + "babel-helpers": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-helpers/-/babel-helpers-6.24.1.tgz", + "integrity": "sha1-NHHenK7DiOXIUOWX5Yom3fN2ArI=", + "requires": { + "babel-runtime": "6.26.0", + "babel-template": "6.26.0" + } + }, + "babel-messages": { + "version": "6.23.0", + "resolved": "https://registry.npmjs.org/babel-messages/-/babel-messages-6.23.0.tgz", + "integrity": "sha1-8830cDhYA1sqKVHG7F7fbGLyYw4=", + "requires": { + "babel-runtime": "6.26.0" + } + }, + "babel-plugin-transform-es2015-modules-commonjs": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-modules-commonjs/-/babel-plugin-transform-es2015-modules-commonjs-6.26.0.tgz", + "integrity": "sha1-DYOUApt9xqvhqX7xgeAHWN0uXYo=", + "requires": { + "babel-plugin-transform-strict-mode": "6.24.1", + "babel-runtime": "6.26.0", + "babel-template": "6.26.0", + "babel-types": "6.26.0" + } + }, + "babel-plugin-transform-strict-mode": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-strict-mode/-/babel-plugin-transform-strict-mode-6.24.1.tgz", + "integrity": "sha1-1fr3qleKZbvlkc9e2uBKDGcCB1g=", + "requires": { + "babel-runtime": "6.26.0", + "babel-types": "6.26.0" + } + }, + "babel-register": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-register/-/babel-register-6.26.0.tgz", + "integrity": "sha1-btAhFz4vy0htestFxgCahW9kcHE=", + "requires": { + "babel-core": "6.26.0", + "babel-runtime": "6.26.0", + "core-js": "2.5.3", + "home-or-tmp": "2.0.0", + "lodash": "4.17.5", + "mkdirp": "0.5.1", + "source-map-support": "0.4.18" + } + }, + "babel-runtime": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.26.0.tgz", + "integrity": "sha1-llxwWGaOgrVde/4E/yM3vItWR/4=", + "requires": { + "core-js": "2.5.3", + "regenerator-runtime": "0.11.1" + } + }, + "babel-template": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-template/-/babel-template-6.26.0.tgz", + "integrity": "sha1-3gPi0WOWsGn0bdn/+FIfsaDjXgI=", + "requires": { + "babel-runtime": "6.26.0", + "babel-traverse": "6.26.0", + "babel-types": "6.26.0", + "babylon": "6.18.0", + "lodash": "4.17.5" + } + }, + "babel-traverse": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-traverse/-/babel-traverse-6.26.0.tgz", + "integrity": "sha1-RqnL1+3MYsjlwGTi0tjQ9ANXZu4=", + "requires": { + "babel-code-frame": "6.26.0", + "babel-messages": "6.23.0", + "babel-runtime": "6.26.0", + "babel-types": "6.26.0", + "babylon": "6.18.0", + "debug": "2.6.9", + "globals": "9.18.0", + "invariant": "2.2.3", + "lodash": "4.17.5" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + } + } + } + }, + "babel-types": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-types/-/babel-types-6.26.0.tgz", + "integrity": "sha1-o7Bz+Uq0nrb6Vc1lInozQ4BjJJc=", + "requires": { + "babel-runtime": "6.26.0", + "esutils": "2.0.2", + "lodash": "4.17.5", + "to-fast-properties": "1.0.3" + } + }, + "babylon": { + "version": "6.18.0", + "resolved": "https://registry.npmjs.org/babylon/-/babylon-6.18.0.tgz", + "integrity": "sha512-q/UEjfGJ2Cm3oKV71DJz9d25TPnq5rhBVL2Q4fA5wcC3jcrdn7+SssEybFIxwAvvP+YCsCYNKughoF33GxgycQ==" + }, + "balanced-match": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" + }, + "bindings": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.2.1.tgz", + "integrity": "sha1-FK1hE4EtLTfXLme0ystLtyZQXxE=", + "dev": true, + "optional": true + }, + "bl": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/bl/-/bl-1.0.3.tgz", + "integrity": "sha1-/FQhoo/UImA2w7OJGmaiW8ZNIm4=", + "dev": true, + "requires": { + "readable-stream": "2.0.6" + } + }, + "bluebird": { + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.5.1.tgz", + "integrity": "sha512-MKiLiV+I1AA596t9w1sQJ8jkiSr5+ZKi0WKrYGUn6d1Fx+Ij4tIj+m2WMQSGczs5jZVxV339chE8iwk6F64wjA==" + }, + "book": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/book/-/book-1.3.3.tgz", + "integrity": "sha512-9UspsnNrD3nNbsrNhzKFp+tv0PvPVa4e/a7Nii7kr3bO4r55Y42gJfDxJnPBE0OlaV68Bo68sc+DtzC5A4uIPA==", + "requires": { + "error-stack-parser": "1.3.6" + } + }, + "boom": { + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/boom/-/boom-2.10.1.tgz", + "integrity": "sha1-OciRjO/1eZ+D+UkqhI9iWt0Mdm8=", + "dev": true, + "requires": { + "hoek": "2.16.3" + } + }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "requires": { + "balanced-match": "1.0.0", + "concat-map": "0.0.1" + } + }, + "bufferutil": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/bufferutil/-/bufferutil-1.2.1.tgz", + "integrity": "sha1-N75dNuHgZJIiHmjUdLGsWOUQy9c=", + "dev": true, + "optional": true, + "requires": { + "bindings": "1.2.1", + "nan": "2.9.2" + } + }, + "builtin-modules": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-1.1.1.tgz", + "integrity": "sha1-Jw8HbFpywC9bZaR9+Uxf46J4iS8=", + "dev": true + }, + "camelcase": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-1.2.1.tgz", + "integrity": "sha1-m7UwTS4LVmmLLHWLCKPqqdqlijk=", + "dev": true + }, + "camelcase-keys": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-2.1.0.tgz", + "integrity": "sha1-MIvur/3ygRkFHvodkyITyRuPkuc=", + "dev": true, + "requires": { + "camelcase": "2.1.1", + "map-obj": "1.0.1" + }, + "dependencies": { + "camelcase": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-2.1.1.tgz", + "integrity": "sha1-fB0W1nmhu+WcoCys7PsBHiAfWh8=", + "dev": true + } + } + }, + "cardinal": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/cardinal/-/cardinal-1.0.0.tgz", + "integrity": "sha1-UOIcGwqjdyn5N33vGWtanOyTLuk=", + "dev": true, + "requires": { + "ansicolors": "0.2.1", + "redeyed": "1.0.1" + } + }, + "caseless": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.11.0.tgz", + "integrity": "sha1-cVuW6phBWTzDMGeSP17GDr2k99c=", + "dev": true + }, + "chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "requires": { + "ansi-styles": "2.2.1", + "escape-string-regexp": "1.0.5", + "has-ansi": "2.0.0", + "strip-ansi": "3.0.1", + "supports-color": "2.0.0" + } + }, + "cli-table": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/cli-table/-/cli-table-0.3.1.tgz", + "integrity": "sha1-9TsFJmqLGguTSz0IIebi3FkUriM=", + "dev": true, + "requires": { + "colors": "1.0.3" + } + }, + "cli-usage": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/cli-usage/-/cli-usage-0.1.7.tgz", + "integrity": "sha512-x/Q52iLSZsRrRb2ePmTsVYXrGcrPQ8G4yRAY7QpMlumxAfPVrnDOH2X6Z5s8qsAX7AA7YuIi8AXFrvH0wWEesA==", + "dev": true, + "requires": { + "marked": "0.3.17", + "marked-terminal": "2.0.0" + } + }, + "cliui": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-3.2.0.tgz", + "integrity": "sha1-EgYBU3qRbSmUD5NNo7SNWFo5IT0=", + "dev": true, + "requires": { + "string-width": "1.0.2", + "strip-ansi": "3.0.1", + "wrap-ansi": "2.1.0" + } + }, + "co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=" + }, + "code-point-at": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", + "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=", + "dev": true + }, + "colors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/colors/-/colors-1.0.3.tgz", + "integrity": "sha1-BDP0TYCWgP3rYO0mDxsMJi6CpAs=", + "dev": true + }, + "combined-stream": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.6.tgz", + "integrity": "sha1-cj599ugBrFYTETp+RFqbactjKBg=", + "dev": true, + "requires": { + "delayed-stream": "1.0.0" + } + }, + "commander": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.5.0.tgz", + "integrity": "sha1-13e2pNhH1CPl1HXahkKUrB/1qp0=" + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" + }, + "content-disposition": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.2.tgz", + "integrity": "sha1-DPaLud318r55YcOoUXjLhdunjLQ=" + }, + "content-type": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", + "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==" + }, + "convert-source-map": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.5.1.tgz", + "integrity": "sha1-uCeAl7m8IpNl3lxiz1/K7YtVmeU=" + }, + "cookies": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/cookies/-/cookies-0.7.1.tgz", + "integrity": "sha1-fIphX1SBxhq58WyDNzG8uPZjuZs=", + "requires": { + "depd": "1.1.2", + "keygrip": "1.0.2" + } + }, + "core-js": { + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.5.3.tgz", + "integrity": "sha1-isw4NFgk8W2DZbfJtCWRaOjtYD4=" + }, + "core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", + "dev": true + }, + "cryptiles": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/cryptiles/-/cryptiles-2.0.5.tgz", + "integrity": "sha1-O9/s3GCBR8HGcgL6KR59ylnqo7g=", + "dev": true, + "requires": { + "boom": "2.10.1" + } + }, + "ctype": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/ctype/-/ctype-0.5.3.tgz", + "integrity": "sha1-gsGMJGH3QRTvFsE1IkrQuRRMoS8=", + "dev": true + }, + "currently-unhandled": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/currently-unhandled/-/currently-unhandled-0.4.1.tgz", + "integrity": "sha1-mI3zP+qxke95mmE2nddsF635V+o=", + "dev": true, + "requires": { + "array-find-index": "1.0.2" + } + }, + "dateformat": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-1.0.12.tgz", + "integrity": "sha1-nxJLZ1lMk3/3BpMuSmQsyo27/uk=", + "dev": true, + "requires": { + "get-stdin": "4.0.1", + "meow": "3.7.0" + } + }, + "debounce": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/debounce/-/debounce-1.1.0.tgz", + "integrity": "sha512-ZQVKfRVlwRfD150ndzEK8M90ABT+Y/JQKs4Y7U4MXdpuoUkkrr4DwKbVux3YjylA5bUMUj0Nc3pMxPJX6N2QQQ==", + "dev": true + }, + "debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "requires": { + "ms": "2.0.0" + } + }, + "decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=", + "dev": true + }, + "deep-equal": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.0.1.tgz", + "integrity": "sha1-9dJgKStmDghO/0zbyfCK0yR0SLU=" + }, + "delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=", + "dev": true + }, + "delegates": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=" + }, + "depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=" + }, + "destroy": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", + "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=" + }, + "detect-indent": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-4.0.0.tgz", + "integrity": "sha1-920GQ1LN9Docts5hnE7jqUdd4gg=", + "requires": { + "repeating": "2.0.1" + } + }, + "diff": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-1.4.0.tgz", + "integrity": "sha1-fyjS657nsVqX79ic5j3P2qPMur8=", + "dev": true + }, + "dynamic-dedupe": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/dynamic-dedupe/-/dynamic-dedupe-0.2.0.tgz", + "integrity": "sha1-UPfChoSDHs8cFwqrZ6HVMRzdds4=", + "dev": true, + "requires": { + "xtend": "2.0.6" + }, + "dependencies": { + "xtend": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-2.0.6.tgz", + "integrity": "sha1-XqZXptukRwacLlnFihE4ywxebO4=", + "dev": true, + "requires": { + "is-object": "0.1.2", + "object-keys": "0.2.0" + } + } + } + }, + "ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" + }, + "end-of-stream": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.1.tgz", + "integrity": "sha512-1MkrZNvWTKCaigbn+W15elq2BB/L22nqrSY5DKlo3X6+vclJm8Bb5djXJBmEX6fS3+zCh/F4VBK5Z2KxJt4s2Q==", + "requires": { + "once": "1.4.0" + } + }, + "error-ex": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.1.tgz", + "integrity": "sha1-+FWobOYa3E6GIcPNoh56dhLDqNw=", + "dev": true, + "requires": { + "is-arrayish": "0.2.1" + } + }, + "error-inject": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/error-inject/-/error-inject-1.0.0.tgz", + "integrity": "sha1-4rPZG1Su1nLzCdlQ0VSFD6EdTzc=" + }, + "error-stack-parser": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/error-stack-parser/-/error-stack-parser-1.3.6.tgz", + "integrity": "sha1-4Oc7k+QXE40c18C3RrGkoUhUwpI=", + "requires": { + "stackframe": "0.3.1" + } + }, + "escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=" + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" + }, + "esprima": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-3.0.0.tgz", + "integrity": "sha1-U88kes2ncxPlUcOqLnM0LT+099k=", + "dev": true + }, + "esutils": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.2.tgz", + "integrity": "sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs=" + }, + "extend": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.1.tgz", + "integrity": "sha1-p1Xqe8Gt/MWjHOfnYtuq3F5jZEQ=", + "dev": true + }, + "filewatcher": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/filewatcher/-/filewatcher-3.0.1.tgz", + "integrity": "sha1-9KGVc1Xdr0Q8zXiolfPVXiPIoDQ=", + "dev": true, + "requires": { + "debounce": "1.1.0" + } + }, + "find-up": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-1.1.2.tgz", + "integrity": "sha1-ay6YIrGizgpgq2TWEOzK1TyyTQ8=", + "dev": true, + "requires": { + "path-exists": "2.1.0", + "pinkie-promise": "2.0.1" + } + }, + "foreach": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/foreach/-/foreach-2.0.5.tgz", + "integrity": "sha1-C+4AUBiusmDQo6865ljdATbsG5k=", + "dev": true + }, + "forever-agent": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", + "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=", + "dev": true + }, + "form-data": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-1.0.1.tgz", + "integrity": "sha1-rjFduaSQf6BlUCMEpm13M0de43w=", + "dev": true, + "requires": { + "async": "2.6.0", + "combined-stream": "1.0.6", + "mime-types": "2.1.18" + } + }, + "fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=" + }, + "generate-function": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.0.0.tgz", + "integrity": "sha1-aFj+fAlpt9TpCTM3ZHrHn2DfvnQ=", + "dev": true + }, + "generate-object-property": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/generate-object-property/-/generate-object-property-1.2.0.tgz", + "integrity": "sha1-nA4cQDCM6AT0eDYYuTf6iPmdUNA=", + "dev": true, + "requires": { + "is-property": "1.0.2" + } + }, + "get-stdin": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-4.0.1.tgz", + "integrity": "sha1-uWjGsKBDhDJJAui/Gl3zJXmkUP4=", + "dev": true + }, + "glob": { + "version": "3.2.11", + "resolved": "https://registry.npmjs.org/glob/-/glob-3.2.11.tgz", + "integrity": "sha1-Spc/Y1uRkPcV0QmH1cAP0oFevj0=", + "dev": true, + "requires": { + "inherits": "2.0.3", + "minimatch": "0.3.0" + }, + "dependencies": { + "minimatch": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-0.3.0.tgz", + "integrity": "sha1-J12O2qxPG7MyZHIInnlJyDlGmd0=", + "dev": true, + "requires": { + "lru-cache": "2.7.3", + "sigmund": "1.0.1" + } + } + } + }, + "globals": { + "version": "9.18.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-9.18.0.tgz", + "integrity": "sha512-S0nG3CLEQiY/ILxqtztTWH/3iRRdyBLw6KMDxnKMchrtbj2OFmehVh0WUCfW3DUrIgx/qFrJPICrq4Z4sTR9UQ==" + }, + "graceful-fs": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz", + "integrity": "sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg=", + "dev": true + }, + "growl": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/growl/-/growl-1.9.2.tgz", + "integrity": "sha1-Dqd0NxXbjY3ixe3hd14bRayFwC8=", + "dev": true + }, + "growly": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/growly/-/growly-1.3.0.tgz", + "integrity": "sha1-8QdIy+dq+WS3yWyTxrzCivEgwIE=", + "dev": true + }, + "har-validator": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-2.0.6.tgz", + "integrity": "sha1-zcvAgYgmWtEZtqWnyKtw7s+10n0=", + "dev": true, + "requires": { + "chalk": "1.1.3", + "commander": "2.14.1", + "is-my-json-valid": "2.17.2", + "pinkie-promise": "2.0.1" + }, + "dependencies": { + "commander": { + "version": "2.14.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.14.1.tgz", + "integrity": "sha512-+YR16o3rK53SmWHU3rEM3tPAh2rwb1yPcQX5irVn7mb0gXbwuCCrnkbV5+PBfETdfg1vui07nM6PCG1zndcjQw==", + "dev": true + } + } + }, + "has-ansi": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", + "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=", + "requires": { + "ansi-regex": "2.1.1" + } + }, + "hawk": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/hawk/-/hawk-3.1.3.tgz", + "integrity": "sha1-B4REvXwWQLD+VA0sm3PVlnjo4cQ=", + "dev": true, + "requires": { + "boom": "2.10.1", + "cryptiles": "2.0.5", + "hoek": "2.16.3", + "sntp": "1.0.9" + } + }, + "hoek": { + "version": "2.16.3", + "resolved": "https://registry.npmjs.org/hoek/-/hoek-2.16.3.tgz", + "integrity": "sha1-ILt0A9POo5jpHcRxCo/xuCdKJe0=", + "dev": true + }, + "home-or-tmp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/home-or-tmp/-/home-or-tmp-2.0.0.tgz", + "integrity": "sha1-42w/LSyufXRqhX440Y1fMqeILbg=", + "requires": { + "os-homedir": "1.0.2", + "os-tmpdir": "1.0.2" + } + }, + "hosted-git-info": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.5.0.tgz", + "integrity": "sha512-pNgbURSuab90KbTqvRPsseaTxOJCZBD0a7t+haSN33piP9cCM4l0CqdzAif2hUqm716UovKB2ROmiabGAKVXyg==", + "dev": true + }, + "http-assert": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/http-assert/-/http-assert-1.3.0.tgz", + "integrity": "sha1-oxpc+IyHPsu1eWkH1NbxMujAHko=", + "requires": { + "deep-equal": "1.0.1", + "http-errors": "1.6.2" + } + }, + "http-errors": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.2.tgz", + "integrity": "sha1-CgAsyFcHGSp+eUbO7cERVfYOxzY=", + "requires": { + "depd": "1.1.1", + "inherits": "2.0.3", + "setprototypeof": "1.0.3", + "statuses": "1.4.0" + }, + "dependencies": { + "depd": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.1.tgz", + "integrity": "sha1-V4O04cRZ8G+lyif5kfPQbnoxA1k=" + } + } + }, + "http-signature": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-0.11.0.tgz", + "integrity": "sha1-F5bPZ6ABrVzWhJ3KCZFIXwkIn+Y=", + "dev": true, + "requires": { + "asn1": "0.1.11", + "assert-plus": "0.1.5", + "ctype": "0.5.3" + } + }, + "indent-string": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-2.1.0.tgz", + "integrity": "sha1-ji1INIdCEhtKghi3oTfppSBJ3IA=", + "dev": true, + "requires": { + "repeating": "2.0.1" + } + }, + "indexof": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/indexof/-/indexof-0.0.1.tgz", + "integrity": "sha1-gtwzbSMrkGIXnQWrMpOmYFn9Q10=", + "dev": true + }, + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" + }, + "invariant": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.3.tgz", + "integrity": "sha512-7Z5PPegwDTyjbaeCnV0efcyS6vdKAU51kpEmS7QFib3P4822l8ICYyMn7qvJnc+WzLoDsuI9gPMKbJ8pCu8XtA==", + "requires": { + "loose-envify": "1.3.1" + } + }, + "invert-kv": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-1.0.0.tgz", + "integrity": "sha1-EEqOSqym09jNFXqO+L+rLXo//bY=", + "dev": true + }, + "is": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/is/-/is-0.2.7.tgz", + "integrity": "sha1-OzSixI81mXLzUEKEkZOucmS2NWI=", + "dev": true + }, + "is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=", + "dev": true + }, + "is-builtin-module": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-builtin-module/-/is-builtin-module-1.0.0.tgz", + "integrity": "sha1-VAVy0096wxGfj3bDDLwbHgN6/74=", + "dev": true, + "requires": { + "builtin-modules": "1.1.1" + } + }, + "is-finite": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-finite/-/is-finite-1.0.2.tgz", + "integrity": "sha1-zGZ3aVYCvlUO8R6LSqYwU0K20Ko=", + "requires": { + "number-is-nan": "1.0.1" + } + }, + "is-fullwidth-code-point": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", + "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", + "dev": true, + "requires": { + "number-is-nan": "1.0.1" + } + }, + "is-generator-function": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.0.7.tgz", + "integrity": "sha512-YZc5EwyO4f2kWCax7oegfuSr9mFz1ZvieNYBEjmukLxgXfBUbxAWGVF7GZf0zidYtoBl3WvC07YK0wT76a+Rtw==" + }, + "is-my-ip-valid": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-my-ip-valid/-/is-my-ip-valid-1.0.0.tgz", + "integrity": "sha512-gmh/eWXROncUzRnIa1Ubrt5b8ep/MGSnfAUI3aRp+sqTCs1tv1Isl8d8F6JmkN3dXKc3ehZMrtiPN9eL03NuaQ==", + "dev": true + }, + "is-my-json-valid": { + "version": "2.17.2", + "resolved": "https://registry.npmjs.org/is-my-json-valid/-/is-my-json-valid-2.17.2.tgz", + "integrity": "sha512-IBhBslgngMQN8DDSppmgDv7RNrlFotuuDsKcrCP3+HbFaVivIBU7u9oiiErw8sH4ynx3+gOGQ3q2otkgiSi6kg==", + "dev": true, + "requires": { + "generate-function": "2.0.0", + "generate-object-property": "1.2.0", + "is-my-ip-valid": "1.0.0", + "jsonpointer": "4.0.1", + "xtend": "4.0.1" + } + }, + "is-object": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/is-object/-/is-object-0.1.2.tgz", + "integrity": "sha1-AO+8CIFsM8/ErIJR0TLhDcZQmNc=", + "dev": true + }, + "is-property": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz", + "integrity": "sha1-V/4cTkhHTt1lsJkR8msc1Ald2oQ=", + "dev": true + }, + "is-utf8": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-utf8/-/is-utf8-0.2.1.tgz", + "integrity": "sha1-Sw2hRCEE0bM2NA6AeX6GXPOffXI=", + "dev": true + }, + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", + "dev": true + }, + "isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", + "dev": true + }, + "isstream": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", + "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=", + "dev": true + }, + "jade": { + "version": "0.26.3", + "resolved": "https://registry.npmjs.org/jade/-/jade-0.26.3.tgz", + "integrity": "sha1-jxDXl32NefL2/4YqgbBRPMslaGw=", + "dev": true, + "requires": { + "commander": "0.6.1", + "mkdirp": "0.3.0" + }, + "dependencies": { + "commander": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-0.6.1.tgz", + "integrity": "sha1-+mihT2qUXVTbvlDYzbMyDp47GgY=", + "dev": true + }, + "mkdirp": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.3.0.tgz", + "integrity": "sha1-G79asbqCevI1dRQ0kEJkVfSB/h4=", + "dev": true + } + } + }, + "js-tokens": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-3.0.2.tgz", + "integrity": "sha1-mGbfOVECEw449/mWvOtlRDIJwls=" + }, + "jsesc": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-1.3.0.tgz", + "integrity": "sha1-RsP+yMGJKxKwgz25vHYiF226s0s=" + }, + "json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=", + "dev": true + }, + "json5": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/json5/-/json5-0.5.1.tgz", + "integrity": "sha1-Hq3nrMASA0rYTiOWdn6tn6VJWCE=" + }, + "jsonpointer": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/jsonpointer/-/jsonpointer-4.0.1.tgz", + "integrity": "sha1-T9kss04OnbPInIYi7PUfm5eMbLk=", + "dev": true + }, + "keygrip": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/keygrip/-/keygrip-1.0.2.tgz", + "integrity": "sha1-rTKXxVcGneqLz+ek+kkbdcXd65E=" + }, + "koa": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/koa/-/koa-2.4.1.tgz", + "integrity": "sha512-3caQ9OyLDYSL3wAhVfv2s9k3tLNgW18QxnKIPaRjzG9uXyDhp4tOo+U+XtbY+xbzEiCW5smjxMCegpZqCjmjMw==", + "requires": { + "accepts": "1.3.4", + "content-disposition": "0.5.2", + "content-type": "1.0.4", + "cookies": "0.7.1", + "debug": "3.1.0", + "delegates": "1.0.0", + "depd": "1.1.2", + "destroy": "1.0.4", + "error-inject": "1.0.0", + "escape-html": "1.0.3", + "fresh": "0.5.2", + "http-assert": "1.3.0", + "http-errors": "1.6.2", + "is-generator-function": "1.0.7", + "koa-compose": "4.0.0", + "koa-convert": "1.2.0", + "koa-is-json": "1.0.0", + "mime-types": "2.1.18", + "on-finished": "2.3.0", + "only": "0.0.2", + "parseurl": "1.3.2", + "statuses": "1.4.0", + "type-is": "1.6.16", + "vary": "1.1.2" + } + }, + "koa-compose": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/koa-compose/-/koa-compose-4.0.0.tgz", + "integrity": "sha1-KAClE9nDYe8NY4UrA45Pby1adzw=" + }, + "koa-convert": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/koa-convert/-/koa-convert-1.2.0.tgz", + "integrity": "sha1-2kCHXfSd4FOQmNFwC1CCDOvNIdA=", + "requires": { + "co": "4.6.0", + "koa-compose": "3.2.1" + }, + "dependencies": { + "koa-compose": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/koa-compose/-/koa-compose-3.2.1.tgz", + "integrity": "sha1-qFzLQLfZhtjlo0Wzoazo6rz1Tec=", + "requires": { + "any-promise": "1.3.0" + } + } + } + }, + "koa-is-json": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/koa-is-json/-/koa-is-json-1.0.0.tgz", + "integrity": "sha1-JzwH7c3Ljfaiwat9We52SRRR7BQ=" + }, + "lcid": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/lcid/-/lcid-1.0.0.tgz", + "integrity": "sha1-MIrMr6C8SDo4Z7S28rlQYlHRuDU=", + "dev": true, + "requires": { + "invert-kv": "1.0.0" + } + }, + "load-json-file": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz", + "integrity": "sha1-lWkFcI1YtLq0wiYbBPWfMcmTdMA=", + "dev": true, + "requires": { + "graceful-fs": "4.1.11", + "parse-json": "2.2.0", + "pify": "2.3.0", + "pinkie-promise": "2.0.1", + "strip-bom": "2.0.0" + } + }, + "localenv": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/localenv/-/localenv-0.2.2.tgz", + "integrity": "sha1-xQjynTSFvck0HT6tF/YcWr0bC6s=", + "requires": { + "commander": "2.5.0" + } + }, + "localtunnel": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/localtunnel/-/localtunnel-1.8.0.tgz", + "integrity": "sha1-6P66X6AVv6LMHF2v8Jc+rzrfMzc=", + "dev": true, + "requires": { + "debug": "2.2.0", + "openurl": "1.1.0", + "request": "2.65.0", + "yargs": "3.29.0" + }, + "dependencies": { + "debug": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.2.0.tgz", + "integrity": "sha1-+HBX6ZWxofauaklgZkE3vFbwOdo=", + "dev": true, + "requires": { + "ms": "0.7.1" + } + }, + "ms": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/ms/-/ms-0.7.1.tgz", + "integrity": "sha1-nNE8A62/8ltl7/3nzoZO6VIBcJg=", + "dev": true + } + } + }, + "lodash": { + "version": "4.17.5", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.5.tgz", + "integrity": "sha512-svL3uiZf1RwhH+cWrfZn3A4+U58wbP0tGVTLQPbjplZxZ8ROD9VLuNgsRniTlLe7OlSqR79RUehXgpBW/s0IQw==" + }, + "lodash._arraycopy": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/lodash._arraycopy/-/lodash._arraycopy-3.0.0.tgz", + "integrity": "sha1-due3wfH7klRzdIeKVi7Qaj5Q9uE=", + "dev": true + }, + "lodash._arrayeach": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/lodash._arrayeach/-/lodash._arrayeach-3.0.0.tgz", + "integrity": "sha1-urFWsqkNPxu9XGU0AzSeXlkz754=", + "dev": true + }, + "lodash._baseassign": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/lodash._baseassign/-/lodash._baseassign-3.2.0.tgz", + "integrity": "sha1-jDigmVAPIVrQnlnxci/QxSv+Ck4=", + "dev": true, + "requires": { + "lodash._basecopy": "3.0.1", + "lodash.keys": "3.1.2" + } + }, + "lodash._baseclone": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/lodash._baseclone/-/lodash._baseclone-3.3.0.tgz", + "integrity": "sha1-MDUZv2OT/n5C802LYw73eU41Qrc=", + "dev": true, + "requires": { + "lodash._arraycopy": "3.0.0", + "lodash._arrayeach": "3.0.0", + "lodash._baseassign": "3.2.0", + "lodash._basefor": "3.0.3", + "lodash.isarray": "3.0.4", + "lodash.keys": "3.1.2" + } + }, + "lodash._basecopy": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/lodash._basecopy/-/lodash._basecopy-3.0.1.tgz", + "integrity": "sha1-jaDmqHbPNEwK2KVIghEd08XHyjY=", + "dev": true + }, + "lodash._basefor": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash._basefor/-/lodash._basefor-3.0.3.tgz", + "integrity": "sha1-dVC06SGO8J+tJDQ7YSAhx5tMIMI=", + "dev": true + }, + "lodash._bindcallback": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/lodash._bindcallback/-/lodash._bindcallback-3.0.1.tgz", + "integrity": "sha1-5THCdkTPi1epnhftlbNcdIeJOS4=", + "dev": true + }, + "lodash._getnative": { + "version": "3.9.1", + "resolved": "https://registry.npmjs.org/lodash._getnative/-/lodash._getnative-3.9.1.tgz", + "integrity": "sha1-VwvH3t5G1hzc3mh9ZdPuy6o6r/U=", + "dev": true + }, + "lodash.assign": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.assign/-/lodash.assign-4.2.0.tgz", + "integrity": "sha1-DZnzzNem0mHRm9rrkkUAXShYCOc=", + "dev": true + }, + "lodash.clonedeep": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-3.0.2.tgz", + "integrity": "sha1-oKHkDYKl6on/WxR7hETtY9koJ9s=", + "dev": true, + "requires": { + "lodash._baseclone": "3.3.0", + "lodash._bindcallback": "3.0.1" + } + }, + "lodash.isarguments": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz", + "integrity": "sha1-L1c9hcaiQon/AGY7SRwdM4/zRYo=", + "dev": true + }, + "lodash.isarray": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/lodash.isarray/-/lodash.isarray-3.0.4.tgz", + "integrity": "sha1-eeTriMNqgSKvhvhEqpvNhRtfu1U=", + "dev": true + }, + "lodash.keys": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/lodash.keys/-/lodash.keys-3.1.2.tgz", + "integrity": "sha1-TbwEcrFWvlCgsoaFXRvQsMZWCYo=", + "dev": true, + "requires": { + "lodash._getnative": "3.9.1", + "lodash.isarguments": "3.1.0", + "lodash.isarray": "3.0.4" + } + }, + "lodash.toarray": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.toarray/-/lodash.toarray-4.4.0.tgz", + "integrity": "sha1-JMS/zWsvuji/0FlNsRedjptlZWE=", + "dev": true + }, + "loose-envify": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.3.1.tgz", + "integrity": "sha1-0aitM/qc4OcT1l/dCsi3SNR4yEg=", + "requires": { + "js-tokens": "3.0.2" + } + }, + "loud-rejection": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/loud-rejection/-/loud-rejection-1.6.0.tgz", + "integrity": "sha1-W0b4AUft7leIcPCG0Eghz5mOVR8=", + "dev": true, + "requires": { + "currently-unhandled": "0.4.1", + "signal-exit": "3.0.2" + } + }, + "lru-cache": { + "version": "2.7.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-2.7.3.tgz", + "integrity": "sha1-bUUk6LlV+V1PW1iFHOId1y+06VI=", + "dev": true + }, + "map-obj": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-1.0.1.tgz", + "integrity": "sha1-2TPOuSBdgr3PSIb2dCvcK03qFG0=", + "dev": true + }, + "marked": { + "version": "0.3.17", + "resolved": "https://registry.npmjs.org/marked/-/marked-0.3.17.tgz", + "integrity": "sha512-+AKbNsjZl6jFfLPwHhWmGTqE009wTKn3RTmn9K8oUKHrX/abPJjtcRtXpYB/FFrwPJRUA86LX/de3T0knkPCmQ==", + "dev": true + }, + "marked-terminal": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/marked-terminal/-/marked-terminal-2.0.0.tgz", + "integrity": "sha1-Xq9Wi+ZvaGVBr6UqVYKAMQox3i0=", + "dev": true, + "requires": { + "cardinal": "1.0.0", + "chalk": "1.1.3", + "cli-table": "0.3.1", + "lodash.assign": "4.2.0", + "node-emoji": "1.8.1" + } + }, + "media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=" + }, + "meow": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/meow/-/meow-3.7.0.tgz", + "integrity": "sha1-cstmi0JSKCkKu/qFaJJYcwioAfs=", + "dev": true, + "requires": { + "camelcase-keys": "2.1.0", + "decamelize": "1.2.0", + "loud-rejection": "1.6.0", + "map-obj": "1.0.1", + "minimist": "1.2.0", + "normalize-package-data": "2.4.0", + "object-assign": "4.1.1", + "read-pkg-up": "1.0.1", + "redent": "1.0.0", + "trim-newlines": "1.0.0" + }, + "dependencies": { + "minimist": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", + "dev": true + } + } + }, + "mime-db": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.33.0.tgz", + "integrity": "sha512-BHJ/EKruNIqJf/QahvxwQZXKygOQ256myeN/Ew+THcAa5q+PjyTTMMeNQC4DZw5AwfvelsUrA6B67NKMqXDbzQ==" + }, + "mime-types": { + "version": "2.1.18", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.18.tgz", + "integrity": "sha512-lc/aahn+t4/SWV/qcmumYjymLsWfN3ELhpmVuUFjgsORruuZPVSwAQryq+HHGvO/SI2KVX26bx+En+zhM8g8hQ==", + "requires": { + "mime-db": "1.33.0" + } + }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "requires": { + "brace-expansion": "1.1.11" + } + }, + "minimist": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", + "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=" + }, + "mkdirp": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", + "requires": { + "minimist": "0.0.8" + } + }, + "mocha": { + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-2.5.3.tgz", + "integrity": "sha1-FhvlvetJZ3HrmzV0UFC2IrWu/Fg=", + "dev": true, + "requires": { + "commander": "2.3.0", + "debug": "2.2.0", + "diff": "1.4.0", + "escape-string-regexp": "1.0.2", + "glob": "3.2.11", + "growl": "1.9.2", + "jade": "0.26.3", + "mkdirp": "0.5.1", + "supports-color": "1.2.0", + "to-iso-string": "0.0.2" + }, + "dependencies": { + "commander": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.3.0.tgz", + "integrity": "sha1-/UMOiJgy7DU7ms0d4hfBHLPu+HM=", + "dev": true + }, + "debug": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.2.0.tgz", + "integrity": "sha1-+HBX6ZWxofauaklgZkE3vFbwOdo=", + "dev": true, + "requires": { + "ms": "0.7.1" + } + }, + "escape-string-regexp": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.2.tgz", + "integrity": "sha1-Tbwv5nTnGUnK8/smlc5/LcHZqNE=", + "dev": true + }, + "ms": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/ms/-/ms-0.7.1.tgz", + "integrity": "sha1-nNE8A62/8ltl7/3nzoZO6VIBcJg=", + "dev": true + }, + "supports-color": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-1.2.0.tgz", + "integrity": "sha1-/x7R5hFp0Gs88tWI4YixjYhH4X4=", + "dev": true + } + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + }, + "nan": { + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.9.2.tgz", + "integrity": "sha512-ltW65co7f3PQWBDbqVvaU1WtFJUsNW7sWWm4HINhbMQIyVyzIeyZ8toX5TC5eeooE6piZoaEh4cZkueSKG3KYw==", + "dev": true, + "optional": true + }, + "negotiator": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.1.tgz", + "integrity": "sha1-KzJxhOiZIQEXeyhWP7XnECrNDKk=" + }, + "node-dev": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/node-dev/-/node-dev-3.1.3.tgz", + "integrity": "sha1-WCcZIj69711jBZ5qf7zSOZ/A+E0=", + "dev": true, + "requires": { + "dateformat": "1.0.12", + "dynamic-dedupe": "0.2.0", + "filewatcher": "3.0.1", + "minimist": "1.2.0", + "node-notifier": "4.6.1", + "resolve": "1.5.0" + }, + "dependencies": { + "minimist": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", + "dev": true + } + } + }, + "node-emoji": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/node-emoji/-/node-emoji-1.8.1.tgz", + "integrity": "sha512-+ktMAh1Jwas+TnGodfCfjUbJKoANqPaJFN0z0iqh41eqD8dvguNzcitVSBSVK1pidz0AqGbLKcoVuVLRVZ/aVg==", + "dev": true, + "requires": { + "lodash.toarray": "4.4.0" + } + }, + "node-notifier": { + "version": "4.6.1", + "resolved": "https://registry.npmjs.org/node-notifier/-/node-notifier-4.6.1.tgz", + "integrity": "sha1-BW0UJE89zBzq3+aK+c/wxUc6M/M=", + "dev": true, + "requires": { + "cli-usage": "0.1.7", + "growly": "1.3.0", + "lodash.clonedeep": "3.0.2", + "minimist": "1.2.0", + "semver": "5.5.0", + "shellwords": "0.1.1", + "which": "1.3.0" + }, + "dependencies": { + "minimist": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", + "dev": true + } + } + }, + "node-uuid": { + "version": "1.4.8", + "resolved": "https://registry.npmjs.org/node-uuid/-/node-uuid-1.4.8.tgz", + "integrity": "sha1-sEDrCSOWivq/jTL7HxfxFn/auQc=", + "dev": true + }, + "normalize-package-data": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.4.0.tgz", + "integrity": "sha512-9jjUFbTPfEy3R/ad/2oNbKtW9Hgovl5O1FvFWKkKblNXoN/Oou6+9+KKohPK13Yc3/TyunyWhJp6gvRNR/PPAw==", + "dev": true, + "requires": { + "hosted-git-info": "2.5.0", + "is-builtin-module": "1.0.0", + "semver": "5.5.0", + "validate-npm-package-license": "3.0.3" + } + }, + "number-is-nan": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", + "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=" + }, + "oauth-sign": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.8.2.tgz", + "integrity": "sha1-Rqarfwrq2N6unsBWV4C31O/rnUM=", + "dev": true + }, + "object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", + "dev": true + }, + "object-keys": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-0.2.0.tgz", + "integrity": "sha1-zd7AKZiwkb5CvxA1rjLknxy26mc=", + "dev": true, + "requires": { + "foreach": "2.0.5", + "indexof": "0.0.1", + "is": "0.2.7" + } + }, + "on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=", + "requires": { + "ee-first": "1.1.1" + } + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "requires": { + "wrappy": "1.0.2" + } + }, + "only": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/only/-/only-0.0.2.tgz", + "integrity": "sha1-Kv3oTQPlC5qO3EROMGEKcCle37Q=" + }, + "openurl": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/openurl/-/openurl-1.1.0.tgz", + "integrity": "sha1-4vIYnZmcBIIyAfCD8PGnzYkDGHo=", + "dev": true + }, + "optimist": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/optimist/-/optimist-0.6.1.tgz", + "integrity": "sha1-2j6nRob6IaGaERwybpDrFaAZZoY=", + "requires": { + "minimist": "0.0.8", + "wordwrap": "0.0.3" + } + }, + "options": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/options/-/options-0.0.6.tgz", + "integrity": "sha1-7CLTEoBrtT5zF3Pnza788cZDEo8=", + "dev": true + }, + "os-homedir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", + "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=" + }, + "os-locale": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-1.4.0.tgz", + "integrity": "sha1-IPnxeuKe00XoveWDsT0gCYA8FNk=", + "dev": true, + "requires": { + "lcid": "1.0.0" + } + }, + "os-tmpdir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=" + }, + "parse-json": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz", + "integrity": "sha1-9ID0BDTvgHQfhGkJn43qGPVaTck=", + "dev": true, + "requires": { + "error-ex": "1.3.1" + } + }, + "parseurl": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.2.tgz", + "integrity": "sha1-/CidTtiZMRlGDBViUyYs3I3mW/M=" + }, + "path-exists": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-2.1.0.tgz", + "integrity": "sha1-D+tsZPD8UY2adU3V77YscCJ2H0s=", + "dev": true, + "requires": { + "pinkie-promise": "2.0.1" + } + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" + }, + "path-parse": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.5.tgz", + "integrity": "sha1-PBrfhx6pzWyUMbbqK9dKD/BVxME=", + "dev": true + }, + "path-type": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-1.1.0.tgz", + "integrity": "sha1-WcRPfuSR2nBNpBXaWkBwuk+P5EE=", + "dev": true, + "requires": { + "graceful-fs": "4.1.11", + "pify": "2.3.0", + "pinkie-promise": "2.0.1" + } + }, + "pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", + "dev": true + }, + "pinkie": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz", + "integrity": "sha1-clVrgM+g1IqXToDnckjoDtT3+HA=", + "dev": true + }, + "pinkie-promise": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz", + "integrity": "sha1-ITXW36ejWMBprJsXh3YogihFD/o=", + "dev": true, + "requires": { + "pinkie": "2.0.4" + } + }, + "private": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/private/-/private-0.1.8.tgz", + "integrity": "sha512-VvivMrbvd2nKkiG38qjULzlc+4Vx4wm/whI9pQD35YrARNnhxeiRktSOhSukRLFNlzg6Br/cJPet5J/u19r/mg==" + }, + "process-nextick-args": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-1.0.7.tgz", + "integrity": "sha1-FQ4gt1ZZCtP5EJPyWk8q2L/zC6M=", + "dev": true + }, + "pump": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-2.0.0.tgz", + "integrity": "sha512-6MYypjOvtiXhBSTOD0Zs5eNjCGfnqi5mPsCsW+dgKTxrZzQMZQNpBo3XRkLx7id753f3EeyHLBqzqqUymIolgw==", + "requires": { + "end-of-stream": "1.4.1", + "once": "1.4.0" + } + }, + "qs": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-5.2.1.tgz", + "integrity": "sha1-gB/uAw4LlFDWOFrcSKTMVbRK7fw=", + "dev": true + }, + "read-pkg": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-1.1.0.tgz", + "integrity": "sha1-9f+qXs0pyzHAR0vKfXVra7KePyg=", + "dev": true, + "requires": { + "load-json-file": "1.1.0", + "normalize-package-data": "2.4.0", + "path-type": "1.1.0" + } + }, + "read-pkg-up": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-1.0.1.tgz", + "integrity": "sha1-nWPBMnbAZZGNV/ACpX9AobZD+wI=", + "dev": true, + "requires": { + "find-up": "1.1.2", + "read-pkg": "1.1.0" + } + }, + "readable-stream": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.0.6.tgz", + "integrity": "sha1-j5A0HmilPMySh4jaz80Rs265t44=", + "dev": true, + "requires": { + "core-util-is": "1.0.2", + "inherits": "2.0.3", + "isarray": "1.0.0", + "process-nextick-args": "1.0.7", + "string_decoder": "0.10.31", + "util-deprecate": "1.0.2" + } + }, + "redent": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-1.0.0.tgz", + "integrity": "sha1-z5Fqsf1fHxbfsggi3W7H9zDCr94=", + "dev": true, + "requires": { + "indent-string": "2.1.0", + "strip-indent": "1.0.1" + } + }, + "redeyed": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/redeyed/-/redeyed-1.0.1.tgz", + "integrity": "sha1-6WwZO0DAgWsArshCaY5hGF5VSYo=", + "dev": true, + "requires": { + "esprima": "3.0.0" + } + }, + "regenerator-runtime": { + "version": "0.11.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz", + "integrity": "sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==" + }, + "repeating": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/repeating/-/repeating-2.0.1.tgz", + "integrity": "sha1-UhTFOpJtNVJwdSf7q0FdvAjQbdo=", + "requires": { + "is-finite": "1.0.2" + } + }, + "request": { + "version": "2.65.0", + "resolved": "https://registry.npmjs.org/request/-/request-2.65.0.tgz", + "integrity": "sha1-zBo7xyuWJUc0/DQpbaMi+Uht3ro=", + "dev": true, + "requires": { + "aws-sign2": "0.6.0", + "bl": "1.0.3", + "caseless": "0.11.0", + "combined-stream": "1.0.6", + "extend": "3.0.1", + "forever-agent": "0.6.1", + "form-data": "1.0.1", + "har-validator": "2.0.6", + "hawk": "3.1.3", + "http-signature": "0.11.0", + "isstream": "0.1.2", + "json-stringify-safe": "5.0.1", + "mime-types": "2.1.18", + "node-uuid": "1.4.8", + "oauth-sign": "0.8.2", + "qs": "5.2.1", + "stringstream": "0.0.5", + "tough-cookie": "2.2.2", + "tunnel-agent": "0.4.3" + } + }, + "resolve": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.5.0.tgz", + "integrity": "sha512-hgoSGrc3pjzAPHNBg+KnFcK2HwlHTs/YrAGUr6qgTVUZmXv1UEXXl0bZNBKMA9fud6lRYFdPGz0xXxycPzmmiw==", + "dev": true, + "requires": { + "path-parse": "1.0.5" + } + }, + "semver": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.5.0.tgz", + "integrity": "sha512-4SJ3dm0WAwWy/NVeioZh5AntkdJoWKxHxcmyP622fOkgHa4z3R0TdBJICINyaSDE6uNwVc8gZr+ZinwZAH4xIA==", + "dev": true + }, + "setprototypeof": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.0.3.tgz", + "integrity": "sha1-ZlZ+NwQ+608E2RvWWMDL77VbjgQ=" + }, + "shellwords": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/shellwords/-/shellwords-0.1.1.tgz", + "integrity": "sha512-vFwSUfQvqybiICwZY5+DAWIPLKsWO31Q91JSKl3UYv+K5c2QRPzn0qzec6QPu1Qc9eHYItiP3NdJqNVqetYAww==", + "dev": true + }, + "sigmund": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/sigmund/-/sigmund-1.0.1.tgz", + "integrity": "sha1-P/IfGYytIXX587eBhT/ZTQ0ZtZA=", + "dev": true + }, + "signal-exit": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", + "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=", + "dev": true + }, + "slash": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-1.0.0.tgz", + "integrity": "sha1-xB8vbDn8FtHNF61LXYlhFK5HDVU=" + }, + "sntp": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/sntp/-/sntp-1.0.9.tgz", + "integrity": "sha1-ZUEYTMkK7qbG57NeJlkIJEPGYZg=", + "dev": true, + "requires": { + "hoek": "2.16.3" + } + }, + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=" + }, + "source-map-support": { + "version": "0.4.18", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.4.18.tgz", + "integrity": "sha512-try0/JqxPLF9nOjvSta7tVondkP5dwgyLDjVoyMDlmjugT2lRZ1OfsrYTkCd2hkDnJTKRbO/Rl3orm8vlsUzbA==", + "requires": { + "source-map": "0.5.7" + } + }, + "spdx-correct": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.0.0.tgz", + "integrity": "sha512-N19o9z5cEyc8yQQPukRCZ9EUmb4HUpnrmaL/fxS2pBo2jbfcFRVuFZ/oFC+vZz0MNNk0h80iMn5/S6qGZOL5+g==", + "dev": true, + "requires": { + "spdx-expression-parse": "3.0.0", + "spdx-license-ids": "3.0.0" + } + }, + "spdx-exceptions": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.1.0.tgz", + "integrity": "sha512-4K1NsmrlCU1JJgUrtgEeTVyfx8VaYea9J9LvARxhbHtVtohPs/gFGG5yy49beySjlIMhhXZ4QqujIZEfS4l6Cg==", + "dev": true + }, + "spdx-expression-parse": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.0.tgz", + "integrity": "sha512-Yg6D3XpRD4kkOmTpdgbUiEJFKghJH03fiC1OPll5h/0sO6neh2jqRDVHOQ4o/LMea0tgCkbMgea5ip/e+MkWyg==", + "dev": true, + "requires": { + "spdx-exceptions": "2.1.0", + "spdx-license-ids": "3.0.0" + } + }, + "spdx-license-ids": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.0.tgz", + "integrity": "sha512-2+EPwgbnmOIl8HjGBXXMd9NAu02vLjOO1nWw4kmeRDFyHn+M/ETfHxQUK0oXg8ctgVnl9t3rosNVsZ1jG61nDA==", + "dev": true + }, + "stackframe": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/stackframe/-/stackframe-0.3.1.tgz", + "integrity": "sha1-M6qE8Rd6VUjIk1Uzy/6zQgl19aQ=" + }, + "statuses": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.4.0.tgz", + "integrity": "sha512-zhSCtt8v2NDrRlPQpCNtw/heZLtfUDqxBM1udqikb/Hbk52LK4nQSwr10u77iopCW5LsyHpuXS0GnEc48mLeew==" + }, + "string-width": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", + "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", + "dev": true, + "requires": { + "code-point-at": "1.1.0", + "is-fullwidth-code-point": "1.0.0", + "strip-ansi": "3.0.1" + } + }, + "string_decoder": { + "version": "0.10.31", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=", + "dev": true + }, + "stringstream": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/stringstream/-/stringstream-0.0.5.tgz", + "integrity": "sha1-TkhM1N5aC7vuGORjB3EKioFiGHg=", + "dev": true + }, + "strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "requires": { + "ansi-regex": "2.1.1" + } + }, + "strip-bom": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-2.0.0.tgz", + "integrity": "sha1-YhmoVhZSBJHzV4i9vxRHqZx+aw4=", + "dev": true, + "requires": { + "is-utf8": "0.2.1" + } + }, + "strip-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-1.0.1.tgz", + "integrity": "sha1-DHlipq3vp7vUrDZkYKY4VSrhoKI=", + "dev": true, + "requires": { + "get-stdin": "4.0.1" + } + }, + "supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=" + }, + "tldjs": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/tldjs/-/tldjs-1.6.2.tgz", + "integrity": "sha1-DYtxRWUfEFKsCzxoiEMpZjYYOxM=" + }, + "to-fast-properties": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-1.0.3.tgz", + "integrity": "sha1-uDVx+k2MJbguIxsG46MFXeTKGkc=" + }, + "to-iso-string": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/to-iso-string/-/to-iso-string-0.0.2.tgz", + "integrity": "sha1-TcGeZk38y+Jb2NtQiwDG2hWCVdE=", + "dev": true + }, + "tough-cookie": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.2.2.tgz", + "integrity": "sha1-yDoYMPTl7wuT7yo0iOck+N4Basc=", + "dev": true + }, + "trim-newlines": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-1.0.0.tgz", + "integrity": "sha1-WIeWa7WCpFA6QetST301ARgVphM=", + "dev": true + }, + "trim-right": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/trim-right/-/trim-right-1.0.1.tgz", + "integrity": "sha1-yy4SAwZ+DI3h9hQJS5/kVwTqYAM=" + }, + "tunnel-agent": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.4.3.tgz", + "integrity": "sha1-Y3PbdpCf5XDgjXNYM2Xtgop07us=", + "dev": true + }, + "type-is": { + "version": "1.6.16", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.16.tgz", + "integrity": "sha512-HRkVv/5qY2G6I8iab9cI7v1bOIdhm94dVjQCPFElW9W+3GeDOSHmy2EBYe4VTApuzolPcmgFTN3ftVJRKR2J9Q==", + "requires": { + "media-typer": "0.3.0", + "mime-types": "2.1.18" + } + }, + "ultron": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/ultron/-/ultron-1.0.2.tgz", + "integrity": "sha1-rOEWq1V80Zc4ak6I9GhTeMiy5Po=", + "dev": true + }, + "utf-8-validate": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/utf-8-validate/-/utf-8-validate-1.2.2.tgz", + "integrity": "sha1-i7hxpHQeCFxwSHynrNvX1tNgKes=", + "dev": true, + "optional": true, + "requires": { + "bindings": "1.2.1", + "nan": "2.4.0" + }, + "dependencies": { + "nan": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.4.0.tgz", + "integrity": "sha1-+zxZ1F/k7/4hXwuJD4rfbrMtIjI=", + "dev": true, + "optional": true + } + } + }, + "util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", + "dev": true + }, + "validate-npm-package-license": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.3.tgz", + "integrity": "sha512-63ZOUnL4SIXj4L0NixR3L1lcjO38crAbgrTpl28t8jjrfuiOBL5Iygm+60qPs/KsZGzPNg6Smnc/oY16QTjF0g==", + "dev": true, + "requires": { + "spdx-correct": "3.0.0", + "spdx-expression-parse": "3.0.0" + } + }, + "vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=" + }, + "which": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.0.tgz", + "integrity": "sha512-xcJpopdamTuY5duC/KnTTNBraPK54YwpenP4lzxU8H91GudWpFv38u0CKjclE1Wi2EH2EDz5LRcHcKbCIzqGyg==", + "dev": true, + "requires": { + "isexe": "2.0.0" + } + }, + "window-size": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/window-size/-/window-size-0.1.4.tgz", + "integrity": "sha1-+OGqHuWlPsW/FR/6CXQqatdpeHY=", + "dev": true + }, + "wordwrap": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.3.tgz", + "integrity": "sha1-o9XabNXAvAAI03I0u68b7WMFkQc=" + }, + "wrap-ansi": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz", + "integrity": "sha1-2Pw9KE3QV5T+hJc8rs3Rz4JP3YU=", + "dev": true, + "requires": { + "string-width": "1.0.2", + "strip-ansi": "3.0.1" + } + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" + }, + "ws": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-0.8.0.tgz", + "integrity": "sha1-rGDrrTEhIdAeFswzg9fsZ60PDx8=", + "dev": true, + "requires": { + "bufferutil": "1.2.1", + "options": "0.0.6", + "ultron": "1.0.2", + "utf-8-validate": "1.2.2" + } + }, + "xtend": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz", + "integrity": "sha1-pcbVMr5lbiPbgg77lDofBJmNY68=", + "dev": true + }, + "y18n": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-3.2.1.tgz", + "integrity": "sha1-bRX7qITAhnnA136I53WegR4H+kE=", + "dev": true + }, + "yargs": { + "version": "3.29.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-3.29.0.tgz", + "integrity": "sha1-GquWYOrnnYuPZ1vK7qtu40ws9pw=", + "dev": true, + "requires": { + "camelcase": "1.2.1", + "cliui": "3.2.0", + "decamelize": "1.2.0", + "os-locale": "1.4.0", + "window-size": "0.1.4", + "y18n": "3.2.1" + } + } + } +} diff --git a/server.js b/server.js index 5dd08b0..89defdc 100644 --- a/server.js +++ b/server.js @@ -10,11 +10,27 @@ import rand_id from './lib/rand_id'; const debug = Debug('localtunnel:server'); -function GetClientIdFromHostname(hostname) { - return tldjs.getSubdomain(hostname); +function GetClientIdFromHostname(hostname,opt) { + let subdomain = tldjs.getSubdomain(hostname); + console.log(opt,'option'); + console.log(hostname,'hostname'); + console.log(subdomain,'subdomain'); + if (subdomain && opt.subHost) { + const subHosts = Array.isArray(opt.subHost) + ? opt.subHost : [opt.subHost] + const subHost = subHosts.reduce((found, sub) => { + return found + || (subdomain.slice(-sub.length) == sub ? sub : '') + }, '') + subdomain = subHost + ? subdomain.slice(0, -(subHost.length + 1)) + : subdomain + } + return subdomain; } module.exports = function(opt) { + console.log(opt); opt = opt || {}; const manager = new ClientManager(opt); @@ -42,7 +58,6 @@ module.exports = function(opt) { // root endpoint app.use(async (ctx, next) => { const path = ctx.request.path; - // skip anything not on the root path if (path !== '/') { await next(); @@ -54,7 +69,6 @@ module.exports = function(opt) { const req_id = rand_id(); debug('making new client with id %s', req_id); const info = await manager.newClient(req_id); - const url = schema + '://' + info.id + '.' + ctx.request.host; info.url = url; ctx.body = info; @@ -81,7 +95,7 @@ module.exports = function(opt) { const req_id = parts[1]; // limit requested hostnames to 63 characters - if (! /^[a-z0-9]{4,63}$/.test(req_id)) { + if (! /^[a-z0-9-]{4,63}$/.test(req_id)) { const msg = 'Invalid subdomain. Subdomains must be lowercase and between 4 and 63 alphanumeric characters.'; ctx.status = 403; ctx.body = { @@ -111,7 +125,8 @@ module.exports = function(opt) { return; } - const clientId = GetClientIdFromHostname(hostname); + const clientId = GetClientIdFromHostname(hostname,opt); + console.log(clientId,'clientId response'); if (!clientId) { appCallback(req, res); return; @@ -133,7 +148,7 @@ module.exports = function(opt) { return; } - const clientId = GetClientIdFromHostname(hostname); + const clientId = GetClientIdFromHostname(hostname,opt); if (!clientId) { sock.destroy(); return; diff --git a/test/simple.js b/test/simple.js index 5dab5e2..b0c2362 100644 --- a/test/simple.js +++ b/test/simple.js @@ -4,7 +4,8 @@ var assert = require('assert'); var localtunnel = require('localtunnel'); var localtunnel_server = require('../server')({ - max_tcp_sockets: 2 + max_tcp_sockets: 2, + subHost: ['nested.sub','nested','sub'] }); var lt_server_port @@ -32,6 +33,7 @@ test('set up local http server', function(done) { test('set up localtunnel client', function(done) { var opt = { host: 'http://localhost:' + lt_server_port, + subdomain: 'txyz' }; localtunnel(test._fake_port, opt, function(err, tunnel) { @@ -66,3 +68,41 @@ test('should respond to request', function(done) { }); }); }); +test('set up localtunnel client (sub-host)', function (done) { + var opt = { + host: 'http://nested.sub.localhost:' + lt_server_port, + subdomain: 'txyz' + }; + + localtunnel(test._fake_port, opt, function (err, tunnel) { + console.log(url); + assert.ifError(err); + var url = tunnel.url; + assert.ok(new RegExp('^http:\/\/.*nested.sub.localhost:' + lt_server_port + '$').test(url)); + test._fake_url = url; + done(err); + }); +}); + +test('should respond to request (sub-host)', function (done) { + var hostname = url.parse(test._fake_url).hostname; + var opt = { + host: 'nested.sub.localhost', + port: lt_server_port, + headers: { + host: hostname + '.tld' + } + }; + http.get(opt, function (res) { + var body = ''; + res.setEncoding('utf-8'); + res.on('data', function (chunk) { + body += chunk; + }); + + res.on('end', function () { + assert.equal(body, 'hello world!'); + done(); + }); + }); +}); \ No newline at end of file diff --git a/test/websocket.js b/test/websocket.js index 2d70f72..e08ef9c 100644 --- a/test/websocket.js +++ b/test/websocket.js @@ -6,7 +6,8 @@ var WebSocket = require('ws'); var WebSocketServer = require('ws').Server; var localtunnel_server = require('../server')({ - max_tcp_sockets: 2 + max_tcp_sockets: 2, + subHost: ['sub'] }); var lt_server_port @@ -72,3 +73,34 @@ test('websocket server request', function(done) { ws.send('something'); }); }); +test('set up localtunnel client (sub host)', function (done) { + var opt = { + host: 'http://sub.localhost:' + lt_server_port + }; + + localtunnel(test._fake_port, opt, function (err, tunnel) { + assert.ifError(err); + var url = tunnel.url; + assert.ok(new RegExp('^http:\/\/.*sub.localhost:' + lt_server_port + '$').test(url)); + test._fake_url = url; + done(err); + }); +}); + +test('websocket server request (sub-host)', function (done) { + var hostname = url.parse(test._fake_url).hostname; + var ws = new WebSocket('http://sub.localhost:' + lt_server_port, { + headers: { + host: hostname + '.tld' + } + }); + + ws.on('message', function (msg) { + assert.equal(msg, 'something'); + done(); + }); + + ws.on('open', function open() { + ws.send('something'); + }); +}); \ No newline at end of file -- 2.54.0 From 96adea37d924ea97c4cecbb0b6bde70c3dad19c7 Mon Sep 17 00:00:00 2001 From: mgudesblatart Date: Thu, 1 Mar 2018 11:56:54 -0500 Subject: [PATCH 02/20] improved some functionality --- server.js | 28 +++++++++++++--------------- test/simple.js | 3 +-- 2 files changed, 14 insertions(+), 17 deletions(-) diff --git a/server.js b/server.js index 89defdc..a2080a6 100644 --- a/server.js +++ b/server.js @@ -12,25 +12,23 @@ const debug = Debug('localtunnel:server'); function GetClientIdFromHostname(hostname,opt) { let subdomain = tldjs.getSubdomain(hostname); - console.log(opt,'option'); - console.log(hostname,'hostname'); - console.log(subdomain,'subdomain'); - if (subdomain && opt.subHost) { + if (subdomain && opt.subHost) { + const _subdomain = subdomain.replace(/\./g,''); const subHosts = Array.isArray(opt.subHost) ? opt.subHost : [opt.subHost] - const subHost = subHosts.reduce((found, sub) => { - return found - || (subdomain.slice(-sub.length) == sub ? sub : '') - }, '') - subdomain = subHost - ? subdomain.slice(0, -(subHost.length + 1)) + const subHost = subHosts.reduce((found, sub) => { + sub = sub.replace(/\./g,''); + return found + (_subdomain.includes(sub) ? sub : ''); + },'') + subdomain = subHost.length > 0 + ? _subdomain.slice(0, -(subHost.length)) : subdomain } return subdomain; } module.exports = function(opt) { - console.log(opt); + opt = opt || {}; const manager = new ClientManager(opt); @@ -124,9 +122,9 @@ module.exports = function(opt) { res.end('Host header is required'); return; } - + const clientId = GetClientIdFromHostname(hostname,opt); - console.log(clientId,'clientId response'); + if (!clientId) { appCallback(req, res); return; @@ -144,13 +142,13 @@ module.exports = function(opt) { server.on('upgrade', (req, socket, head) => { const hostname = req.headers.host; if (!hostname) { - sock.destroy(); + socket.destroy(); return; } const clientId = GetClientIdFromHostname(hostname,opt); if (!clientId) { - sock.destroy(); + socket.destroy(); return; } diff --git a/test/simple.js b/test/simple.js index b0c2362..3a77883 100644 --- a/test/simple.js +++ b/test/simple.js @@ -5,7 +5,7 @@ var localtunnel = require('localtunnel'); var localtunnel_server = require('../server')({ max_tcp_sockets: 2, - subHost: ['nested.sub','nested','sub'] + subHost: ['nested','sub'] }); var lt_server_port @@ -75,7 +75,6 @@ test('set up localtunnel client (sub-host)', function (done) { }; localtunnel(test._fake_port, opt, function (err, tunnel) { - console.log(url); assert.ifError(err); var url = tunnel.url; assert.ok(new RegExp('^http:\/\/.*nested.sub.localhost:' + lt_server_port + '$').test(url)); -- 2.54.0 From b8664d2bc9e2e150ba18f4e55e4b4ee1d0b40910 Mon Sep 17 00:00:00 2001 From: mgudesblatart Date: Thu, 1 Mar 2018 13:16:13 -0500 Subject: [PATCH 03/20] minor tweaks, updated readme --- README.md | 16 +++++++++++++++- server.js | 1 - 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 534d26b..57b9ee9 100644 --- a/README.md +++ b/README.md @@ -29,6 +29,21 @@ 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** that to use sub-hosts you should pass `--sub-host` option during set up. Pass this argument for every sub-host level. + +```shell +# server set to run on port 1234 w/ a sub host to allow for this URL to work "http://sub.example.com" +bin/server --port 1234 \ +--sub-host sub +``` + +```shell +# server set to run on port 1234 w/ a sub host to allow for this URL to work "http://nested.sub.example.com" +bin/server --port 1234 \ +--sub-host nested \ +--sub-host sub +``` + ## use your server You can now use your domain with the `--host` flag for the `lt` client. @@ -41,7 +56,6 @@ You will be assigned a URL similar to `qdci.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. -Note that to use sub-hosts you should pass `--sub-host` option to server ## Deploy diff --git a/server.js b/server.js index a2080a6..3d93fbd 100644 --- a/server.js +++ b/server.js @@ -17,7 +17,6 @@ function GetClientIdFromHostname(hostname,opt) { const subHosts = Array.isArray(opt.subHost) ? opt.subHost : [opt.subHost] const subHost = subHosts.reduce((found, sub) => { - sub = sub.replace(/\./g,''); return found + (_subdomain.includes(sub) ? sub : ''); },'') subdomain = subHost.length > 0 -- 2.54.0 From dcddd2c0f7722addbfa0f1fc7b30f514babd31b6 Mon Sep 17 00:00:00 2001 From: mgudesblatart Date: Thu, 1 Mar 2018 13:21:09 -0500 Subject: [PATCH 04/20] updated simple test (sub-host) to include hyphen --- test/simple.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/simple.js b/test/simple.js index 3a77883..27f95c9 100644 --- a/test/simple.js +++ b/test/simple.js @@ -71,7 +71,7 @@ test('should respond to request', function(done) { test('set up localtunnel client (sub-host)', function (done) { var opt = { host: 'http://nested.sub.localhost:' + lt_server_port, - subdomain: 'txyz' + subdomain: 'txy-z' }; localtunnel(test._fake_port, opt, function (err, tunnel) { -- 2.54.0 From 0eb59d42f9e7d2b7686fad803b23ea77f7d677ca Mon Sep 17 00:00:00 2001 From: mgudesblatart Date: Thu, 1 Mar 2018 16:02:00 -0500 Subject: [PATCH 05/20] allowed for --sub-host to eqal 'sub.sub' --- .travis.yml | 5 +++++ server.js | 5 ++++- test/simple.js | 6 +++--- test/websocket.js | 6 +++--- 4 files changed, 15 insertions(+), 7 deletions(-) diff --git a/.travis.yml b/.travis.yml index f94a53e..37be4a9 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,3 +2,8 @@ language: node_js sudo: false node_js: - "9.2" +addons: + hosts: + - host + - sub.host + - nested.sub.host \ No newline at end of file diff --git a/server.js b/server.js index 3d93fbd..84a5b11 100644 --- a/server.js +++ b/server.js @@ -12,17 +12,20 @@ const debug = Debug('localtunnel:server'); function GetClientIdFromHostname(hostname,opt) { let subdomain = tldjs.getSubdomain(hostname); + if (subdomain && opt.subHost) { const _subdomain = subdomain.replace(/\./g,''); const subHosts = Array.isArray(opt.subHost) ? opt.subHost : [opt.subHost] const subHost = subHosts.reduce((found, sub) => { - return found + (_subdomain.includes(sub) ? sub : ''); + sub = sub.replace(/\./g,''); + return found + (_subdomain.includes(sub) ? sub : sub.includes(_subdomain) ? sub : ''); },'') subdomain = subHost.length > 0 ? _subdomain.slice(0, -(subHost.length)) : subdomain } + return subdomain; } diff --git a/test/simple.js b/test/simple.js index 27f95c9..05d68bc 100644 --- a/test/simple.js +++ b/test/simple.js @@ -70,14 +70,14 @@ test('should respond to request', function(done) { }); test('set up localtunnel client (sub-host)', function (done) { var opt = { - host: 'http://nested.sub.localhost:' + lt_server_port, + host: 'http://nested.sub.host:' + lt_server_port, subdomain: 'txy-z' }; localtunnel(test._fake_port, opt, function (err, tunnel) { assert.ifError(err); var url = tunnel.url; - assert.ok(new RegExp('^http:\/\/.*nested.sub.localhost:' + lt_server_port + '$').test(url)); + assert.ok(new RegExp('^http:\/\/.*nested.sub.host:' + lt_server_port + '$').test(url)); test._fake_url = url; done(err); }); @@ -86,7 +86,7 @@ test('set up localtunnel client (sub-host)', function (done) { test('should respond to request (sub-host)', function (done) { var hostname = url.parse(test._fake_url).hostname; var opt = { - host: 'nested.sub.localhost', + host: 'nested.sub.host', port: lt_server_port, headers: { host: hostname + '.tld' diff --git a/test/websocket.js b/test/websocket.js index e08ef9c..48b46f7 100644 --- a/test/websocket.js +++ b/test/websocket.js @@ -75,13 +75,13 @@ test('websocket server request', function(done) { }); test('set up localtunnel client (sub host)', function (done) { var opt = { - host: 'http://sub.localhost:' + lt_server_port + host: 'http://sub.host:' + lt_server_port }; localtunnel(test._fake_port, opt, function (err, tunnel) { assert.ifError(err); var url = tunnel.url; - assert.ok(new RegExp('^http:\/\/.*sub.localhost:' + lt_server_port + '$').test(url)); + assert.ok(new RegExp('^http:\/\/.*sub.host:' + lt_server_port + '$').test(url)); test._fake_url = url; done(err); }); @@ -89,7 +89,7 @@ test('set up localtunnel client (sub host)', function (done) { test('websocket server request (sub-host)', function (done) { var hostname = url.parse(test._fake_url).hostname; - var ws = new WebSocket('http://sub.localhost:' + lt_server_port, { + var ws = new WebSocket('http://sub.host:' + lt_server_port, { headers: { host: hostname + '.tld' } -- 2.54.0 From 09d9affa924ac062482f3c5223426ec55e916b40 Mon Sep 17 00:00:00 2001 From: mgudesblatart Date: Thu, 1 Mar 2018 16:45:52 -0500 Subject: [PATCH 06/20] reorganized tests. now allow for single string sub-host declaration --- README.md | 9 +++++- test/nested-sub.js | 68 +++++++++++++++++++++++++++++++++++++++ test/nested.sub.js | 68 +++++++++++++++++++++++++++++++++++++++ test/simple.js | 37 ---------------------- test/websocket.js | 31 ------------------ test/websocket.sub.js | 74 +++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 218 insertions(+), 69 deletions(-) create mode 100644 test/nested-sub.js create mode 100644 test/nested.sub.js create mode 100644 test/websocket.sub.js diff --git a/README.md b/README.md index 57b9ee9..88507ae 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # localtunnel-server -[![Build Status](https://travis-ci.org/localtunnel/server.svg?branch=master)](https://travis-ci.org/localtunnel/server) +[![Build Status](https://travis-ci.org/gojisystems/gojiconnect-server.svg?branch=master)](https://travis-ci.org/gojisystems/gojiconnect-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. @@ -30,6 +30,7 @@ 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** that to use sub-hosts you should pass `--sub-host` option during set up. Pass this argument for every sub-host level. +Alternatively, `--sub-host` may be passed in as a single argument with the all sub hosts in a string `"nested.sub"` ```shell # server set to run on port 1234 w/ a sub host to allow for this URL to work "http://sub.example.com" @@ -43,6 +44,12 @@ bin/server --port 1234 \ --sub-host nested \ --sub-host sub ``` +**Alternative** +```shell +# server set to run on port 1234 w/ a sub host to allow for this URL to work "http://nested.sub.example.com" +bin/server --port 1234 \ +--sub-host "nested.sub" +``` ## use your server diff --git a/test/nested-sub.js b/test/nested-sub.js new file mode 100644 index 0000000..21ed90f --- /dev/null +++ b/test/nested-sub.js @@ -0,0 +1,68 @@ +var http = require('http'); +var url = require('url'); +var assert = require('assert'); +var localtunnel = require('localtunnel'); + +var localtunnel_server = require('../server')({ + max_tcp_sockets: 2, + subHost: ['nested','sub'] +}); + +var lt_server_port + +suite('nested-sub'); + +test('set up localtunnel server', function(done) { + var server = localtunnel_server.listen(function() { + lt_server_port = server.address().port; + done(); + }); +}); + +test('set up local http server', function(done) { + var server = http.createServer(function(req, res) { + res.end('hello world!'); + }); + + server.listen(function() { + test._fake_port = server.address().port; + done(); + }); +}); +test('set up localtunnel client (sub-host)', function (done) { + var opt = { + host: 'http://nested.sub.host:' + lt_server_port, + subdomain: 'txy-z' + }; + + localtunnel(test._fake_port, opt, function (err, tunnel) { + assert.ifError(err); + var url = tunnel.url; + assert.ok(new RegExp('^http:\/\/.*nested.sub.host:' + lt_server_port + '$').test(url)); + test._fake_url = url; + done(err); + }); +}); + +test('should respond to request (sub-host)', function (done) { + var hostname = url.parse(test._fake_url).hostname; + var opt = { + host: 'nested.sub.host', + port: lt_server_port, + headers: { + host: hostname + '.tld' + } + }; + http.get(opt, function (res) { + var body = ''; + res.setEncoding('utf-8'); + res.on('data', function (chunk) { + body += chunk; + }); + + res.on('end', function () { + assert.equal(body, 'hello world!'); + done(); + }); + }); +}); \ No newline at end of file diff --git a/test/nested.sub.js b/test/nested.sub.js new file mode 100644 index 0000000..e9f243b --- /dev/null +++ b/test/nested.sub.js @@ -0,0 +1,68 @@ +var http = require('http'); +var url = require('url'); +var assert = require('assert'); +var localtunnel = require('localtunnel'); + +var localtunnel_server = require('../server')({ + max_tcp_sockets: 2, + subHost: ['nested.sub'] +}); + +var lt_server_port + +suite('nested.sub'); + +test('set up localtunnel server', function(done) { + var server = localtunnel_server.listen(function() { + lt_server_port = server.address().port; + done(); + }); +}); + +test('set up local http server', function(done) { + var server = http.createServer(function(req, res) { + res.end('hello world!'); + }); + + server.listen(function() { + test._fake_port = server.address().port; + done(); + }); +}); +test('set up localtunnel client (sub-host)', function (done) { + var opt = { + host: 'http://nested.sub.host:' + lt_server_port, + subdomain: 'txy-z' + }; + + localtunnel(test._fake_port, opt, function (err, tunnel) { + assert.ifError(err); + var url = tunnel.url; + assert.ok(new RegExp('^http:\/\/.*nested.sub.host:' + lt_server_port + '$').test(url)); + test._fake_url = url; + done(err); + }); +}); + +test('should respond to request (sub-host)', function (done) { + var hostname = url.parse(test._fake_url).hostname; + var opt = { + host: 'nested.sub.host', + port: lt_server_port, + headers: { + host: hostname + '.tld' + } + }; + http.get(opt, function (res) { + var body = ''; + res.setEncoding('utf-8'); + res.on('data', function (chunk) { + body += chunk; + }); + + res.on('end', function () { + assert.equal(body, 'hello world!'); + done(); + }); + }); +}); \ No newline at end of file diff --git a/test/simple.js b/test/simple.js index 05d68bc..100f95b 100644 --- a/test/simple.js +++ b/test/simple.js @@ -67,41 +67,4 @@ test('should respond to request', function(done) { done(); }); }); -}); -test('set up localtunnel client (sub-host)', function (done) { - var opt = { - host: 'http://nested.sub.host:' + lt_server_port, - subdomain: 'txy-z' - }; - - localtunnel(test._fake_port, opt, function (err, tunnel) { - assert.ifError(err); - var url = tunnel.url; - assert.ok(new RegExp('^http:\/\/.*nested.sub.host:' + lt_server_port + '$').test(url)); - test._fake_url = url; - done(err); - }); -}); - -test('should respond to request (sub-host)', function (done) { - var hostname = url.parse(test._fake_url).hostname; - var opt = { - host: 'nested.sub.host', - port: lt_server_port, - headers: { - host: hostname + '.tld' - } - }; - http.get(opt, function (res) { - var body = ''; - res.setEncoding('utf-8'); - res.on('data', function (chunk) { - body += chunk; - }); - - res.on('end', function () { - assert.equal(body, 'hello world!'); - done(); - }); - }); }); \ No newline at end of file diff --git a/test/websocket.js b/test/websocket.js index 48b46f7..ae4b6a4 100644 --- a/test/websocket.js +++ b/test/websocket.js @@ -69,37 +69,6 @@ test('websocket server request', function(done) { done(); }); - ws.on('open', function open() { - ws.send('something'); - }); -}); -test('set up localtunnel client (sub host)', function (done) { - var opt = { - host: 'http://sub.host:' + lt_server_port - }; - - localtunnel(test._fake_port, opt, function (err, tunnel) { - assert.ifError(err); - var url = tunnel.url; - assert.ok(new RegExp('^http:\/\/.*sub.host:' + lt_server_port + '$').test(url)); - test._fake_url = url; - done(err); - }); -}); - -test('websocket server request (sub-host)', function (done) { - var hostname = url.parse(test._fake_url).hostname; - var ws = new WebSocket('http://sub.host:' + lt_server_port, { - headers: { - host: hostname + '.tld' - } - }); - - ws.on('message', function (msg) { - assert.equal(msg, 'something'); - done(); - }); - ws.on('open', function open() { ws.send('something'); }); diff --git a/test/websocket.sub.js b/test/websocket.sub.js new file mode 100644 index 0000000..f9c0f35 --- /dev/null +++ b/test/websocket.sub.js @@ -0,0 +1,74 @@ +var http = require('http'); +var url = require('url'); +var assert = require('assert'); +var localtunnel = require('localtunnel'); +var WebSocket = require('ws'); +var WebSocketServer = require('ws').Server; + +var localtunnel_server = require('../server')({ + max_tcp_sockets: 2, + subHost: ['sub'] +}); + +var lt_server_port + +suite('websocket.sub'); + +before('set up localtunnel server', function(done) { + var server = localtunnel_server.listen(function() { + lt_server_port = server.address().port; + done(); + }); +}); + +before('set up local websocket server', function(done) { + var wss = new WebSocketServer({ port: 0 }, function() { + test._fake_port = wss._server.address().port; + done(); + }); + + wss.on('error', function(err) { + done(err); + }); + + wss.on('connection', function connection(ws) { + ws.on('error', function(err) { + done(err); + }); + + ws.on('message', function incoming(message) { + ws.send(message); + }); + }); +}); +test('set up localtunnel client (sub host)', function (done) { + var opt = { + host: 'http://sub.host:' + lt_server_port + }; + + localtunnel(test._fake_port, opt, function (err, tunnel) { + assert.ifError(err); + var url = tunnel.url; + assert.ok(new RegExp('^http:\/\/.*sub.host:' + lt_server_port + '$').test(url)); + test._fake_url = url; + done(err); + }); +}); + +test('websocket server request (sub-host)', function (done) { + var hostname = url.parse(test._fake_url).hostname; + var ws = new WebSocket('http://sub.host:' + lt_server_port, { + headers: { + host: hostname + '.tld' + } + }); + + ws.on('message', function (msg) { + assert.equal(msg, 'something'); + done(); + }); + + ws.on('open', function open() { + ws.send('something'); + }); +}); \ No newline at end of file -- 2.54.0 From c195ba129623562462ed42837677558883bd9bbc Mon Sep 17 00:00:00 2001 From: Goji Systems <23219957+gojisystems@users.noreply.github.com> Date: Sat, 3 Mar 2018 12:00:27 -0500 Subject: [PATCH 07/20] fixed reduce tertiary in GetClientIdFromHostname --- server.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server.js b/server.js index 84a5b11..d5bc369 100644 --- a/server.js +++ b/server.js @@ -19,7 +19,7 @@ function GetClientIdFromHostname(hostname,opt) { ? opt.subHost : [opt.subHost] const subHost = subHosts.reduce((found, sub) => { sub = sub.replace(/\./g,''); - return found + (_subdomain.includes(sub) ? sub : sub.includes(_subdomain) ? sub : ''); + return found + (_subdomain.includes(sub) ? sub : sub.includes(_subdomain) ? _subdomain : ''); },'') subdomain = subHost.length > 0 ? _subdomain.slice(0, -(subHost.length)) -- 2.54.0 From d7852a3dd1b7987d2c805f87cb38d0078352073e Mon Sep 17 00:00:00 2001 From: Alex Huang Date: Mon, 2 Apr 2018 10:37:45 +1000 Subject: [PATCH 08/20] Add option to set server address (#80) Since this server may run behind a reverse proxy like Nginx, it should be able to bind to localhost only rather than listening to the whole network. --- bin/server | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/bin/server b/bin/server index 16a943d..ecf1d4f 100755 --- a/bin/server +++ b/bin/server @@ -17,6 +17,10 @@ const argv = optimist default: '80', describe: 'listen on this port for outside requests' }) + .options('address', { + default: '0.0.0.0', + describe: 'IP address to bind to' + }) .options('max-sockets', { default: 10, describe: 'maximum number of tcp sockets each client is allowed to establish at one time (the tunnels)' @@ -33,7 +37,7 @@ const server = require('../server')({ secure: argv.secure }); -server.listen(argv.port, () => { +server.listen(argv.port, argv.address, () => { debug('server listening on port: %d', server.address().port); }); -- 2.54.0 From b1c296a409afe5040149beac39dd2583d91afae1 Mon Sep 17 00:00:00 2001 From: Ajay Gandhi Date: Sun, 1 Apr 2018 18:00:21 -0700 Subject: [PATCH 09/20] Generate human readable IDs for client URLs (#77) Based on @FranzSkuffka's suggested enhancement in PR 207 on localtunnel/localtunnel (https://github.com/localtunnel/localtunnel/pull/207), this PR uses the "human-readable-ids" package to generate IDs of the form "adjective-noun-number". These IDs are easier to remember and arguably more useful in testing since they can be retyped without copying and pasting. Some sample IDs generated by this package are: * heavy-puma-90 * grump-walrus-64 * purple-starfish-58 * nasty-seahorse-9 --- README.md | 2 +- lib/ClientManager.js | 4 ++-- lib/rand_id.js | 13 ------------- package.json | 1 + server.js | 4 ++-- yarn.lock | 10 ++++++++++ 6 files changed, 16 insertions(+), 18 deletions(-) delete mode 100644 lib/rand_id.js diff --git a/README.md b/README.md index c557c94..8918376 100644 --- a/README.md +++ b/README.md @@ -37,7 +37,7 @@ You can now use your domain with the `--host` flag for the `lt` client. lt --host http://sub.example.tld:1234 --port 9000 ``` -You will be assigned a URL similar to `qdci.sub.example.com:1234`. +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. diff --git a/lib/ClientManager.js b/lib/ClientManager.js index 2d798a1..a4199f9 100644 --- a/lib/ClientManager.js +++ b/lib/ClientManager.js @@ -4,8 +4,8 @@ import Proxy from './Proxy'; import on_finished from 'on-finished'; import http from 'http'; import pump from 'pump'; +import { hri } from "human-readable-ids"; -import rand_id from './rand_id'; import BindingAgent from './BindingAgent'; const NoOp = () => {}; @@ -36,7 +36,7 @@ class ClientManager { // can't ask for id already is use if (clients[id]) { - id = rand_id(); + id = hri.random(); } const popt = { diff --git a/lib/rand_id.js b/lib/rand_id.js deleted file mode 100644 index f76f6d8..0000000 --- a/lib/rand_id.js +++ /dev/null @@ -1,13 +0,0 @@ -// all url safe -// can't use uppercase because hostnames are lowercased -const chars = 'abcdefghijklmnopqrstuvwxyz'; - -export default function rand_id() { - let randomstring = ''; - for (var i=0; i<10; ++i) { - const rnum = Math.floor(Math.random() * chars.length); - randomstring += chars[rnum]; - } - - return randomstring; -} diff --git a/package.json b/package.json index 0ba80a0..0908e04 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,7 @@ "bluebird": "3.5.1", "book": "1.3.3", "debug": "3.1.0", + "human-readable-ids": "1.0.3", "koa": "2.4.1", "localenv": "0.2.2", "on-finished": "2.3.0", diff --git a/server.js b/server.js index 5dd08b0..7ba2106 100644 --- a/server.js +++ b/server.js @@ -4,9 +4,9 @@ import tldjs from 'tldjs'; import Debug from 'debug'; import http from 'http'; import Promise from 'bluebird'; +import { hri } from 'human-readable-ids'; import ClientManager from './lib/ClientManager'; -import rand_id from './lib/rand_id'; const debug = Debug('localtunnel:server'); @@ -51,7 +51,7 @@ module.exports = function(opt) { const isNewClientRequest = ctx.query['new'] !== undefined; if (isNewClientRequest) { - const req_id = rand_id(); + const req_id = hri.random(); debug('making new client with id %s', req_id); const info = await manager.newClient(req_id); diff --git a/yarn.lock b/yarn.lock index 265f847..156462e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -615,6 +615,12 @@ http-signature@~0.11.0: assert-plus "^0.1.5" ctype "0.5.3" +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" @@ -733,6 +739,10 @@ 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" -- 2.54.0 From 37df802cc06e9cde69c59085419a6f46db817da5 Mon Sep 17 00:00:00 2001 From: Roman Shtylman Date: Sun, 1 Apr 2018 18:04:48 -0700 Subject: [PATCH 10/20] Allow dash characters in subdomain. Thank you @zackslash via https://github.com/localtunnel/server/pull/47 Rebased to latest master. --- server.js | 2 +- test/basic.js | 15 +++++++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/server.js b/server.js index 7ba2106..495b5a9 100644 --- a/server.js +++ b/server.js @@ -81,7 +81,7 @@ module.exports = function(opt) { const req_id = parts[1]; // limit requested hostnames to 63 characters - if (! /^[a-z0-9]{4,63}$/.test(req_id)) { + if (! /^(?:[a-z0-9][a-z0-9\-]{4,63}[a-z0-9]|[a-z0-9]{4,63})$/.test(req_id)) { const msg = 'Invalid subdomain. Subdomains must be lowercase and between 4 and 63 alphanumeric characters.'; ctx.status = 403; ctx.body = { diff --git a/test/basic.js b/test/basic.js index f1c829d..786e931 100644 --- a/test/basic.js +++ b/test/basic.js @@ -118,6 +118,21 @@ test('request specific domain', function(done) { }); }); +test('request domain with dash', function(done) { + var opt = { + host: 'http://localhost:' + lt_server_port, + subdomain: 'abcd-1234' + }; + + localtunnel(test._fake_port, opt, function(err, tunnel) { + assert.ifError(err); + var url = tunnel.url; + assert.ok(new RegExp('^http:\/\/.*localhost:' + lt_server_port + '$').test(url)); + test._fake_url = url; + done(err); + }); +}); + test('request domain that is too long', function(done) { var opt = { host: 'http://localhost:' + lt_server_port, -- 2.54.0 From 6b07a86ec14e3461027cffb980fa94dbb506e5cd Mon Sep 17 00:00:00 2001 From: Roman Shtylman Date: Sun, 1 Apr 2018 18:24:49 -0700 Subject: [PATCH 11/20] switch babel-register to esm --- bin/server | 17 +-- package.json | 4 +- server.js | 3 +- test/basic.js | 12 ++- test/mocha.opts | 2 +- test/queue.js | 12 ++- test/simple.js | 12 ++- test/websocket.js | 16 +-- yarn.lock | 264 ++-------------------------------------------- 9 files changed, 49 insertions(+), 293 deletions(-) diff --git a/bin/server b/bin/server index ecf1d4f..d5e3fa3 100755 --- a/bin/server +++ b/bin/server @@ -1,11 +1,14 @@ -#!/usr/bin/env node +#!/usr/bin/env node -r esm -require('localenv'); -require('babel-register'); +import 'localenv'; +import optimist from 'optimist'; -const log = require('book'); -const debug = require('debug')('localtunnel'); -const optimist = require('optimist'); +import log from 'book'; +import Debug from 'debug'; + +import CreateServer from '../server'; + +const debug = Debug('localtunnel'); const argv = optimist .usage('Usage: $0 --port [num]') @@ -32,7 +35,7 @@ if (argv.help) { process.exit(); } -const server = require('../server')({ +const server = CreateServer({ max_tcp_sockets: argv['max-sockets'], secure: argv.secure }); diff --git a/package.json b/package.json index 0908e04..e64988d 100644 --- a/package.json +++ b/package.json @@ -9,11 +9,9 @@ "url": "git://github.com/localtunnel/server.git" }, "dependencies": { - "babel-plugin-transform-es2015-modules-commonjs": "6.26.0", - "babel-register": "6.26.0", - "bluebird": "3.5.1", "book": "1.3.3", "debug": "3.1.0", + "esm": "3.0.14", "human-readable-ids": "1.0.3", "koa": "2.4.1", "localenv": "0.2.2", diff --git a/server.js b/server.js index 495b5a9..ebe22c2 100644 --- a/server.js +++ b/server.js @@ -3,7 +3,6 @@ import Koa from 'koa'; import tldjs from 'tldjs'; import Debug from 'debug'; import http from 'http'; -import Promise from 'bluebird'; import { hri } from 'human-readable-ids'; import ClientManager from './lib/ClientManager'; @@ -14,7 +13,7 @@ function GetClientIdFromHostname(hostname) { return tldjs.getSubdomain(hostname); } -module.exports = function(opt) { +export default function(opt) { opt = opt || {}; const manager = new ClientManager(opt); diff --git a/test/basic.js b/test/basic.js index 786e931..c50b7e2 100644 --- a/test/basic.js +++ b/test/basic.js @@ -1,9 +1,11 @@ -var http = require('http'); -var url = require('url'); -var assert = require('assert'); -var localtunnel = require('localtunnel'); +import http from 'http'; +import url from 'url'; +import assert from 'assert'; +import localtunnel from 'localtunnel'; -var localtunnel_server = require('../server')(); +import CreateServer from '../server'; + +const localtunnel_server = CreateServer(); process.on('uncaughtException', (err) => { console.error(err); diff --git a/test/mocha.opts b/test/mocha.opts index 40f1956..ce1c914 100644 --- a/test/mocha.opts +++ b/test/mocha.opts @@ -1,4 +1,4 @@ --check-leaks --reporter spec --ui qunit ---compilers js:babel-register +--require esm diff --git a/test/queue.js b/test/queue.js index 103891c..b945586 100644 --- a/test/queue.js +++ b/test/queue.js @@ -1,11 +1,13 @@ -var http = require('http'); -var url = require('url'); -var assert = require('assert'); -var localtunnel = require('localtunnel'); +import http from 'http'; +import url from 'url'; +import assert from 'assert'; +import localtunnel from 'localtunnel'; + +import CreateServer from '../server'; suite('queue'); -var localtunnel_server = require('../server')({ +var localtunnel_server = CreateServer({ max_tcp_sockets: 1 }); diff --git a/test/simple.js b/test/simple.js index 5dab5e2..da3472c 100644 --- a/test/simple.js +++ b/test/simple.js @@ -1,9 +1,11 @@ -var http = require('http'); -var url = require('url'); -var assert = require('assert'); -var localtunnel = require('localtunnel'); +import http from 'http'; +import url from 'url'; +import assert from 'assert'; +import localtunnel from 'localtunnel'; -var localtunnel_server = require('../server')({ +import CreateServer from '../server'; + +const localtunnel_server = CreateServer({ max_tcp_sockets: 2 }); diff --git a/test/websocket.js b/test/websocket.js index 2d70f72..6c6db98 100644 --- a/test/websocket.js +++ b/test/websocket.js @@ -1,11 +1,13 @@ -var http = require('http'); -var url = require('url'); -var assert = require('assert'); -var localtunnel = require('localtunnel'); -var WebSocket = require('ws'); -var WebSocketServer = require('ws').Server; +import http from 'http'; +import url from 'url'; +import assert from 'assert'; +import localtunnel from 'localtunnel'; +import WebSocket from 'ws'; +import { Server as WebSocketServer } from 'ws'; -var localtunnel_server = require('../server')({ +import CreateServer from '../server'; + +const localtunnel_server = CreateServer({ max_tcp_sockets: 2 }); diff --git a/yarn.lock b/yarn.lock index 156462e..7f84082 100644 --- a/yarn.lock +++ b/yarn.lock @@ -47,140 +47,6 @@ aws-sign2@~0.6.0: version "0.6.0" resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.6.0.tgz#14342dd38dbcc94d0e5b87d763cd63612c0e794f" -babel-code-frame@^6.26.0: - version "6.26.0" - resolved "https://registry.yarnpkg.com/babel-code-frame/-/babel-code-frame-6.26.0.tgz#63fd43f7dc1e3bb7ce35947db8fe369a3f58c74b" - dependencies: - chalk "^1.1.3" - esutils "^2.0.2" - js-tokens "^3.0.2" - -babel-core@^6.26.0: - version "6.26.0" - resolved "https://registry.yarnpkg.com/babel-core/-/babel-core-6.26.0.tgz#af32f78b31a6fcef119c87b0fd8d9753f03a0bb8" - dependencies: - babel-code-frame "^6.26.0" - babel-generator "^6.26.0" - babel-helpers "^6.24.1" - babel-messages "^6.23.0" - babel-register "^6.26.0" - babel-runtime "^6.26.0" - babel-template "^6.26.0" - babel-traverse "^6.26.0" - babel-types "^6.26.0" - babylon "^6.18.0" - convert-source-map "^1.5.0" - debug "^2.6.8" - json5 "^0.5.1" - lodash "^4.17.4" - minimatch "^3.0.4" - path-is-absolute "^1.0.1" - private "^0.1.7" - slash "^1.0.0" - source-map "^0.5.6" - -babel-generator@^6.26.0: - version "6.26.0" - resolved "https://registry.yarnpkg.com/babel-generator/-/babel-generator-6.26.0.tgz#ac1ae20070b79f6e3ca1d3269613053774f20dc5" - dependencies: - babel-messages "^6.23.0" - babel-runtime "^6.26.0" - babel-types "^6.26.0" - detect-indent "^4.0.0" - jsesc "^1.3.0" - lodash "^4.17.4" - source-map "^0.5.6" - trim-right "^1.0.1" - -babel-helpers@^6.24.1: - version "6.24.1" - resolved "https://registry.yarnpkg.com/babel-helpers/-/babel-helpers-6.24.1.tgz#3471de9caec388e5c850e597e58a26ddf37602b2" - dependencies: - babel-runtime "^6.22.0" - babel-template "^6.24.1" - -babel-messages@^6.23.0: - version "6.23.0" - resolved "https://registry.yarnpkg.com/babel-messages/-/babel-messages-6.23.0.tgz#f3cdf4703858035b2a2951c6ec5edf6c62f2630e" - dependencies: - babel-runtime "^6.22.0" - -babel-plugin-transform-es2015-modules-commonjs@6.26.0: - version "6.26.0" - resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-modules-commonjs/-/babel-plugin-transform-es2015-modules-commonjs-6.26.0.tgz#0d8394029b7dc6abe1a97ef181e00758dd2e5d8a" - dependencies: - babel-plugin-transform-strict-mode "^6.24.1" - babel-runtime "^6.26.0" - babel-template "^6.26.0" - babel-types "^6.26.0" - -babel-plugin-transform-strict-mode@^6.24.1: - version "6.24.1" - resolved "https://registry.yarnpkg.com/babel-plugin-transform-strict-mode/-/babel-plugin-transform-strict-mode-6.24.1.tgz#d5faf7aa578a65bbe591cf5edae04a0c67020758" - dependencies: - babel-runtime "^6.22.0" - babel-types "^6.24.1" - -babel-register@6.26.0, babel-register@^6.26.0: - version "6.26.0" - resolved "https://registry.yarnpkg.com/babel-register/-/babel-register-6.26.0.tgz#6ed021173e2fcb486d7acb45c6009a856f647071" - dependencies: - babel-core "^6.26.0" - babel-runtime "^6.26.0" - core-js "^2.5.0" - home-or-tmp "^2.0.0" - lodash "^4.17.4" - mkdirp "^0.5.1" - source-map-support "^0.4.15" - -babel-runtime@^6.22.0, babel-runtime@^6.26.0: - version "6.26.0" - resolved "https://registry.yarnpkg.com/babel-runtime/-/babel-runtime-6.26.0.tgz#965c7058668e82b55d7bfe04ff2337bc8b5647fe" - dependencies: - core-js "^2.4.0" - regenerator-runtime "^0.11.0" - -babel-template@^6.24.1, babel-template@^6.26.0: - version "6.26.0" - resolved "https://registry.yarnpkg.com/babel-template/-/babel-template-6.26.0.tgz#de03e2d16396b069f46dd9fff8521fb1a0e35e02" - dependencies: - babel-runtime "^6.26.0" - babel-traverse "^6.26.0" - babel-types "^6.26.0" - babylon "^6.18.0" - lodash "^4.17.4" - -babel-traverse@^6.26.0: - version "6.26.0" - resolved "https://registry.yarnpkg.com/babel-traverse/-/babel-traverse-6.26.0.tgz#46a9cbd7edcc62c8e5c064e2d2d8d0f4035766ee" - dependencies: - babel-code-frame "^6.26.0" - babel-messages "^6.23.0" - babel-runtime "^6.26.0" - babel-types "^6.26.0" - babylon "^6.18.0" - debug "^2.6.8" - globals "^9.18.0" - invariant "^2.2.2" - lodash "^4.17.4" - -babel-types@^6.24.1, babel-types@^6.26.0: - version "6.26.0" - resolved "https://registry.yarnpkg.com/babel-types/-/babel-types-6.26.0.tgz#a3b073f94ab49eb6fa55cd65227a334380632497" - dependencies: - babel-runtime "^6.26.0" - esutils "^2.0.2" - lodash "^4.17.4" - to-fast-properties "^1.0.3" - -babylon@^6.18.0: - version "6.18.0" - resolved "https://registry.yarnpkg.com/babylon/-/babylon-6.18.0.tgz#af2f3b88fa6f5c1e4c634d1a0f8eac4f55b395e3" - -balanced-match@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" - bindings@1.2.x, bindings@~1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/bindings/-/bindings-1.2.1.tgz#14ad6113812d2d37d72e67b4cacb4bb726505f11" @@ -191,10 +57,6 @@ bl@~1.0.0: dependencies: readable-stream "~2.0.5" -bluebird@3.5.1: - version "3.5.1" - resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.5.1.tgz#d9551f9de98f1fcda1e683d17ee91a0602ee2eb9" - book@1.3.3: version "1.3.3" resolved "https://registry.yarnpkg.com/book/-/book-1.3.3.tgz#53654254bfe083db102e461544ae9f84be199985" @@ -207,13 +69,6 @@ boom@2.x.x: dependencies: hoek "2.x.x" -brace-expansion@^1.1.7: - version "1.1.8" - resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.8.tgz#c07b211c7c952ec1f8efd51a77ef0d1d3990a292" - dependencies: - balanced-match "^1.0.0" - concat-map "0.0.1" - bufferutil@1.2.x: version "1.2.1" resolved "https://registry.yarnpkg.com/bufferutil/-/bufferutil-1.2.1.tgz#37be5d36e1e06492221e68d474b1ac58e510cbd7" @@ -316,10 +171,6 @@ commander@^2.9.0: version "2.12.2" resolved "https://registry.yarnpkg.com/commander/-/commander-2.12.2.tgz#0f5946c427ed9ec0d91a46bb9def53e54650e555" -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" @@ -328,10 +179,6 @@ content-type@^1.0.0: version "1.0.4" resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b" -convert-source-map@^1.5.0: - version "1.5.1" - resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.5.1.tgz#b8278097b9bc229365de5c62cf5fcaed8b5599e5" - cookies@~0.7.0: version "0.7.1" resolved "https://registry.yarnpkg.com/cookies/-/cookies-0.7.1.tgz#7c8a615f5481c61ab9f16c833731bcb8f663b99b" @@ -339,10 +186,6 @@ cookies@~0.7.0: depd "~1.1.1" keygrip "~1.0.2" -core-js@^2.4.0, core-js@^2.5.0: - version "2.5.2" - resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.5.2.tgz#bc4648656e7dc9dc80d7d3c7bbc172d96e744e63" - 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" @@ -386,12 +229,6 @@ debug@2.2.0: dependencies: ms "0.7.1" -debug@^2.6.8: - version "2.6.9" - resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" - dependencies: - ms "2.0.0" - decamelize@^1.0.0, decamelize@^1.1.2: version "1.2.0" resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" @@ -416,12 +253,6 @@ destroy@^1.0.3: version "1.0.4" resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.0.4.tgz#978857442c44749e4206613e37946205826abd80" -detect-indent@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/detect-indent/-/detect-indent-4.0.0.tgz#f76d064352cdf43a1cb6ce619c4ee3a9475de208" - dependencies: - repeating "^2.0.0" - diff@1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/diff/-/diff-1.4.0.tgz#7f28d2eb9ee7b15a97efd89ce63dcfdaa3ccbabf" @@ -470,14 +301,14 @@ 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.14: + version "3.0.14" + resolved "https://registry.yarnpkg.com/esm/-/esm-3.0.14.tgz#a9a79b7348c8708050ff42499b5ecbec019d08dc" + esprima@~3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/esprima/-/esprima-3.0.0.tgz#53cf247acda77313e551c3aa2e73342d3fb4f7d9" -esutils@^2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.2.tgz#0abf4f1caa5bcb1f7a9d8acc6dea4faaa04bac9b" - extend@~3.0.0: version "3.0.1" resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.1.tgz#a755ea7bc1adfcc5a31ce7e762dbaadc5e636444" @@ -536,10 +367,6 @@ glob@3.2.11: inherits "2" minimatch "0.3" -globals@^9.18.0: - version "9.18.0" - resolved "https://registry.yarnpkg.com/globals/-/globals-9.18.0.tgz#aa3896b3e69b487f17e31ed2143d69a8e30c2d8a" - graceful-fs@^4.1.2: version "4.1.11" resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.1.11.tgz#0e8bdfe4d1ddb8854d64e04ea7c00e2a026e5658" @@ -580,13 +407,6 @@ hoek@2.x.x: version "2.16.3" resolved "https://registry.yarnpkg.com/hoek/-/hoek-2.16.3.tgz#20bb7403d3cea398e91dc4710a8ff1b8274a25ed" -home-or-tmp@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/home-or-tmp/-/home-or-tmp-2.0.0.tgz#e36c3f2d2cae7d746a857e38d18d5f32a7882db8" - dependencies: - os-homedir "^1.0.0" - os-tmpdir "^1.0.1" - hosted-git-info@^2.1.4: version "2.5.0" resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.5.0.tgz#6d60e34b3abbc8313062c3b798ef8d901a07af3c" @@ -635,12 +455,6 @@ inherits@2, inherits@2.0.3, inherits@~2.0.1: version "2.0.3" resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" -invariant@^2.2.2: - version "2.2.2" - resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.2.tgz#9e1f56ac0acdb6bf303306f338be3b204ae60360" - dependencies: - loose-envify "^1.0.0" - invert-kv@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/invert-kv/-/invert-kv-1.0.0.tgz#104a8e4aaca6d3d8cd157a8ef8bfab2d7a3ffdb6" @@ -715,22 +529,10 @@ jade@0.26.3: commander "0.6.1" mkdirp "0.3.0" -js-tokens@^3.0.0, js-tokens@^3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-3.0.2.tgz#9866df395102130e38f7f996bceb65443209c25b" - -jsesc@^1.3.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-1.3.0.tgz#46c3fec8c1892b12b0833db9bc7622176dbab34b" - json-stringify-safe@~5.0.1: version "5.0.1" resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb" -json5@^0.5.1: - version "0.5.1" - resolved "https://registry.yarnpkg.com/json5/-/json5-0.5.1.tgz#1eade7acc012034ad84e2396767ead9fa5495821" - jsonpointer@^4.0.0: version "4.0.1" resolved "https://registry.yarnpkg.com/jsonpointer/-/jsonpointer-4.0.1.tgz#4fd92cb34e0e9db3c89c8622ecf51f9b978c6cb9" @@ -897,16 +699,10 @@ lodash.toarray@^4.4.0: version "4.4.0" resolved "https://registry.yarnpkg.com/lodash.toarray/-/lodash.toarray-4.4.0.tgz#24c4bfcd6b2fba38bfd0594db1179d8e9b656561" -lodash@^4.14.0, lodash@^4.17.4: +lodash@^4.14.0: version "4.17.4" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.4.tgz#78203a4d1c328ae1d86dca6460e369b57f4055ae" -loose-envify@^1.0.0: - version "1.3.1" - resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.3.1.tgz#d1a8ad33fa9ce0e713d65fdd0ac8b748d478c848" - dependencies: - js-tokens "^3.0.0" - loud-rejection@^1.0.0: version "1.6.0" resolved "https://registry.yarnpkg.com/loud-rejection/-/loud-rejection-1.6.0.tgz#5b46f80147edee578870f086d04821cf998e551f" @@ -972,12 +768,6 @@ minimatch@0.3: lru-cache "2" sigmund "~1.0.0" -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" @@ -994,7 +784,7 @@ mkdirp@0.3.0: version "0.3.0" resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.3.0.tgz#1bbf5ab1ba827af23575143490426455f481fe1e" -mkdirp@0.5.1, mkdirp@^0.5.1: +mkdirp@0.5.1: version "0.5.1" resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903" dependencies: @@ -1128,20 +918,12 @@ options@>=0.0.5: version "0.0.6" resolved "https://registry.yarnpkg.com/options/-/options-0.0.6.tgz#ec22d312806bb53e731773e7cdaefcf1c643128f" -os-homedir@^1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/os-homedir/-/os-homedir-1.0.2.tgz#ffbc4988336e0e833de0c168c7ef152121aa7fb3" - os-locale@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/os-locale/-/os-locale-1.4.0.tgz#20f9f17ae29ed345e8bde583b13d2009803c14d9" dependencies: lcid "^1.0.0" -os-tmpdir@^1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274" - parse-json@^2.2.0: version "2.2.0" resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-2.2.0.tgz#f480f40434ef80741f8469099f8dea18f55a4dc9" @@ -1158,10 +940,6 @@ path-exists@^2.0.0: dependencies: pinkie-promise "^2.0.0" -path-is-absolute@^1.0.1: - 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" @@ -1188,10 +966,6 @@ pinkie@^2.0.0: version "2.0.4" resolved "https://registry.yarnpkg.com/pinkie/-/pinkie-2.0.4.tgz#72556b80cfa0d48a974e80e77248e80ed4f7f870" -private@^0.1.7: - version "0.1.8" - resolved "https://registry.yarnpkg.com/private/-/private-0.1.8.tgz#2381edb3689f7a53d653190060fcf822d2f368ff" - process-nextick-args@~1.0.6: version "1.0.7" resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-1.0.7.tgz#150e20b756590ad3f91093f25a4f2ad8bff30ba3" @@ -1246,10 +1020,6 @@ redeyed@~1.0.0: dependencies: esprima "~3.0.0" -regenerator-runtime@^0.11.0: - version "0.11.1" - resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz#be05ad7f9bf7d22e056f9726cee5017fbf19e2e9" - repeating@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/repeating/-/repeating-2.0.1.tgz#5214c53a926d3552707527fbab415dbc08d06dda" @@ -1306,26 +1076,12 @@ signal-exit@^3.0.0: version "3.0.2" resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.2.tgz#b5fdc08f1287ea1178628e415e25132b73646c6d" -slash@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/slash/-/slash-1.0.0.tgz#c41f2f6c39fc16d1cd17ad4b5d896114ae470d55" - sntp@1.x.x: version "1.0.9" resolved "https://registry.yarnpkg.com/sntp/-/sntp-1.0.9.tgz#6541184cc90aeea6c6e7b35e2659082443c66198" dependencies: hoek "2.x.x" -source-map-support@^0.4.15: - version "0.4.18" - resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.4.18.tgz#0286a6de8be42641338594e97ccea75f0a2c585f" - dependencies: - source-map "^0.5.6" - -source-map@^0.5.6: - version "0.5.7" - resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc" - spdx-correct@~1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-1.0.2.tgz#4b3073d933ff51f3912f03ac5519498a4150db40" @@ -1394,10 +1150,6 @@ tldjs@1.6.2: version "1.6.2" resolved "https://registry.yarnpkg.com/tldjs/-/tldjs-1.6.2.tgz#0d8b7145651f1052ac0b3c688843296636183b13" -to-fast-properties@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-1.0.3.tgz#b83571fa4d8c25b82e231b06e3a3055de4ca1a47" - to-iso-string@0.0.2: version "0.0.2" resolved "https://registry.yarnpkg.com/to-iso-string/-/to-iso-string-0.0.2.tgz#4dc19e664dfccbe25bd8db508b00c6da158255d1" @@ -1410,10 +1162,6 @@ trim-newlines@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/trim-newlines/-/trim-newlines-1.0.0.tgz#5887966bb582a4503a41eb524f7d35011815a613" -trim-right@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/trim-right/-/trim-right-1.0.1.tgz#cb2e1203067e0c8de1f614094b9fe45704ea6003" - tunnel-agent@~0.4.1: version "0.4.3" resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.4.3.tgz#6373db76909fe570e08d73583365ed828a74eeeb" -- 2.54.0 From 86dce49dd1d40a64a2f121ff3aa271719417810f Mon Sep 17 00:00:00 2001 From: Roman Shtylman Date: Sun, 1 Apr 2018 19:57:48 -0700 Subject: [PATCH 12/20] add --domain option to enable localtunnel servers on subdomains --- README.md | 2 ++ bin/server | 6 +++++- package.json | 2 +- server.js | 11 +++++++---- test/domain.js | 52 ++++++++++++++++++++++++++++++++++++++++++++++++++ yarn.lock | 12 +++++++++--- 6 files changed, 76 insertions(+), 9 deletions(-) create mode 100644 test/domain.js diff --git a/README.md b/README.md index 8918376..5b46d5d 100644 --- a/README.md +++ b/README.md @@ -29,6 +29,8 @@ 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. diff --git a/bin/server b/bin/server index d5e3fa3..a6bba9d 100755 --- a/bin/server +++ b/bin/server @@ -24,6 +24,9 @@ const argv = optimist 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)' @@ -37,7 +40,8 @@ if (argv.help) { const server = CreateServer({ max_tcp_sockets: argv['max-sockets'], - secure: argv.secure + secure: argv.secure, + domain: argv.domain, }); server.listen(argv.port, argv.address, () => { diff --git a/package.json b/package.json index e64988d..aad8c63 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,7 @@ "on-finished": "2.3.0", "optimist": "0.6.1", "pump": "2.0.0", - "tldjs": "1.6.2" + "tldjs": "2.3.1" }, "devDependencies": { "localtunnel": "1.8.0", diff --git a/server.js b/server.js index ebe22c2..86b2d57 100644 --- a/server.js +++ b/server.js @@ -9,13 +9,16 @@ import ClientManager from './lib/ClientManager'; const debug = Debug('localtunnel:server'); -function GetClientIdFromHostname(hostname) { - return tldjs.getSubdomain(hostname); -} - export default function(opt) { opt = opt || {}; + const validHosts = (opt.domain) ? [opt.domain] : undefined; + const myTldjs = tldjs.fromUserSettings({ validHosts }); + + function GetClientIdFromHostname(hostname) { + return myTldjs.getSubdomain(hostname); + } + const manager = new ClientManager(opt); const schema = opt.secure ? 'https' : 'http'; diff --git a/test/domain.js b/test/domain.js new file mode 100644 index 0000000..6fa2b02 --- /dev/null +++ b/test/domain.js @@ -0,0 +1,52 @@ +import http from 'http'; +import url from 'url'; +import assert from 'assert'; +import localtunnel from 'localtunnel'; + +import CreateServer from '../server'; + +const localtunnel_server = CreateServer({ + domain: 'domain.example.com', +}); + +process.on('uncaughtException', (err) => { + console.error(err); +}); + +process.on('unhandledRejection', (reason, promise) => { + console.error(reason); +}); + +suite('domain'); + +var lt_server_port; + +before('set up localtunnel server', function(done) { + var server = localtunnel_server.listen(function() { + lt_server_port = server.address().port; + done(); + }); +}); + +test('landing page', function(done) { + var opt = { + host: 'localhost', + port: lt_server_port, + headers: { + host: 'domain.example.com' + }, + path: '/' + } + + var req = http.request(opt, function(res) { + res.setEncoding('utf8'); + assert.equal(res.headers.location, 'https://localtunnel.github.io/www/') + done(); + }); + + req.end(); +}); + +after('shutdown', function() { + localtunnel_server.close(); +}); diff --git a/yarn.lock b/yarn.lock index 7f84082..d6628aa 100644 --- a/yarn.lock +++ b/yarn.lock @@ -977,6 +977,10 @@ pump@2.0.0: 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@~5.2.0: version "5.2.1" resolved "https://registry.yarnpkg.com/qs/-/qs-5.2.1.tgz#801fee030e0b9450d6385adc48a4cc55b44aedfc" @@ -1146,9 +1150,11 @@ supports-color@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-2.0.0.tgz#535d045ce6b6363fa40117084629995e9df324c7" -tldjs@1.6.2: - version "1.6.2" - resolved "https://registry.yarnpkg.com/tldjs/-/tldjs-1.6.2.tgz#0d8b7145651f1052ac0b3c688843296636183b13" +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" to-iso-string@0.0.2: version "0.0.2" -- 2.54.0 From 423327d0ac43dfd5cfccb79014c96c210387ac22 Mon Sep 17 00:00:00 2001 From: Roman Shtylman Date: Sun, 1 Apr 2018 20:24:55 -0700 Subject: [PATCH 13/20] update dockerfile entrypoint to use esm Does not work using #! in bin/server without this. --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index eb56d30..fb8ac3c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -10,4 +10,4 @@ RUN yarn install --production && yarn cache clean COPY . /app ENV NODE_ENV production -ENTRYPOINT ["bin/server"] +ENTRYPOINT ["node", "-r", "esm", "./bin/server"] -- 2.54.0 From 30fd566c3aab6e0bc048f77dc77151d4e0236792 Mon Sep 17 00:00:00 2001 From: Roman Shtylman Date: Thu, 5 Apr 2018 18:06:44 -0700 Subject: [PATCH 14/20] remove stale .babelrc --- .babelrc | 5 ----- 1 file changed, 5 deletions(-) delete mode 100644 .babelrc diff --git a/.babelrc b/.babelrc deleted file mode 100644 index 2ce79c5..0000000 --- a/.babelrc +++ /dev/null @@ -1,5 +0,0 @@ -{ - "plugins": [ - "transform-es2015-modules-commonjs", - ] -} -- 2.54.0 From c27100b98ea4f5309b8da14b94714b53b1c62aac Mon Sep 17 00:00:00 2001 From: Roman Shtylman Date: Tue, 15 May 2018 14:49:19 -0400 Subject: [PATCH 15/20] refactor to use http agent interface for tunnels --- README.md | 10 + lib/BindingAgent.js | 23 -- lib/Client.js | 69 ++++ lib/Client.test.js | 158 +++++++++ lib/ClientManager.js | 216 ++++-------- lib/ClientManager.test.js | 51 +++ lib/Proxy.js | 193 ----------- lib/TunnelAgent.js | 171 +++++++++ lib/TunnelAgent.test.js | 176 ++++++++++ package.json | 14 +- server.js | 34 +- server.test.js | 85 +++++ test/basic.js | 166 --------- test/domain.js | 52 --- test/mocha.opts | 4 - test/queue.js | 105 ------ test/simple.js | 70 ---- test/websocket.js | 76 ---- yarn.lock | 713 +++++++++++++------------------------- 19 files changed, 1041 insertions(+), 1345 deletions(-) delete mode 100644 lib/BindingAgent.js create mode 100644 lib/Client.js create mode 100644 lib/Client.test.js create mode 100644 lib/ClientManager.test.js delete mode 100644 lib/Proxy.js create mode 100644 lib/TunnelAgent.js create mode 100644 lib/TunnelAgent.test.js create mode 100644 server.test.js delete mode 100644 test/basic.js delete mode 100644 test/domain.js delete mode 100644 test/mocha.opts delete mode 100644 test/queue.js delete mode 100644 test/simple.js delete mode 100644 test/websocket.js diff --git a/README.md b/README.md index 5b46d5d..9c829f0 100644 --- a/README.md +++ b/README.md @@ -43,6 +43,16 @@ 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. diff --git a/lib/BindingAgent.js b/lib/BindingAgent.js deleted file mode 100644 index c0d7ad9..0000000 --- a/lib/BindingAgent.js +++ /dev/null @@ -1,23 +0,0 @@ -import http from 'http'; -import util from 'util'; -import assert from 'assert'; - -// binding agent will return a given options.socket as the socket for the agent -// this is useful if you already have a socket established and want the request -// to use that socket instead of making a new one -function BindingAgent(options) { - options = options || {}; - http.Agent.call(this, options); - - this.socket = options.socket; - assert(this.socket, 'socket is required for BindingAgent'); - this.createConnection = create_connection; -} - -util.inherits(BindingAgent, http.Agent); - -function create_connection(port, host, options) { - return this.socket; -} - -export default BindingAgent; diff --git a/lib/Client.js b/lib/Client.js new file mode 100644 index 0000000..aae3df0 --- /dev/null +++ b/lib/Client.js @@ -0,0 +1,69 @@ +import http from 'http'; + +import TunnelAgent from './TunnelAgent'; + +// 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 { + constructor(options) { + this.agent = options.agent; + } + + handleRequest(req, res) { + const opt = { + path: req.url, + agent: this.agent, + method: req.method, + headers: req.headers + }; + + const clientReq = http.request(opt, (clientRes) => { + // write response code and headers + res.writeHead(clientRes.statusCode, clientRes.headers); + clientRes.pipe(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) => { + + }); + + req.pipe(clientReq); + } + + handleUpgrade(req, socket) { + this.agent.createConnection({}, (err, conn) => { + // 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) { + 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(''); + + conn.pipe(socket).pipe(conn); + conn.write(arr.join('\r\n')); + }); + } +} + +export default Client; \ No newline at end of file diff --git a/lib/Client.test.js b/lib/Client.test.js new file mode 100644 index 0000000..8f53be1 --- /dev/null +++ b/lib/Client.test.js @@ -0,0 +1,158 @@ +import assert from 'assert'; +import http from 'http'; +import { Duplex } from 'stream'; +import EventEmitter from 'events'; +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.js b/lib/ClientManager.js index a4199f9..b41270f 100644 --- a/lib/ClientManager.js +++ b/lib/ClientManager.js @@ -1,36 +1,31 @@ -import Proxy from './Proxy'; +import { hri } from 'human-readable-ids'; +import Debug from 'debug'; -// maybe remove? -import on_finished from 'on-finished'; -import http from 'http'; -import pump from 'pump'; -import { hri } from "human-readable-ids"; - -import BindingAgent from './BindingAgent'; - -const NoOp = () => {}; +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; - - this.reqId = 0; + this.opt = opt || {}; // id -> client instance - this.clients = Object.create(null); + 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 - async newClient (id) { + // if the tunnel could not be created, throws an error + async newClient(id) { const clients = this.clients; const stats = this.stats; @@ -39,161 +34,68 @@ class ClientManager { id = hri.random(); } - const popt = { - id: id, - max_tcp_sockets: this.opt.max_tcp_sockets - }; + const maxSockets = this.opt.max_tcp_sockets; + const agent = new TunnelAgent({ + maxSockets: 10, + }); - const client = Proxy(popt); + agent.on('online', () => { + this.debug('client online %s', id); + }); + + agent.on('offline', () => { + // TODO(roman): grace period for re-connecting + // this period is short as the client is expected to maintain connections actively + // if they client does not reconnect on a dropped connection they need to re-establish + this.debug('client offline %s', id); + this.removeClient(id); + }); + + // 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.removeClient(id); + }); + + const client = new Client({ agent }); // add to clients map immediately // avoiding races with other clients requesting same id clients[id] = client; - client.on('end', () => { - --stats.tunnels; - delete clients[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; + } + } - return new Promise((resolve, reject) => { - // each local client has a tcp server to link with the remove localtunnel client - // this starts the server and waits until it is listening - client.start((err, info) => { - if (err) { - // clear the reserved client id - delete clients[id]; - reject(err); - return; - } - - ++stats.tunnels; - info.id = id; - resolve(info); - }); - }); + removeClient(id) { + const client = this.clients[id]; + if (!client) { + return; + } + --this.stats.tunnels; + delete this.clients[id]; + client.agent.destroy(); } hasClient(id) { return this.clients[id]; } - // handle http request - handleRequest(clientId, req, res) { - const client = this.clients[clientId]; - if (!client) { - return; - } - - const reqId = this.reqId; - this.reqId = this.reqId + 1; - - let endRes = () => { - endRes = NoOp; - res.end(); - }; - - on_finished(res, () => { - endRes = NoOp; - }); - - client.nextSocket((clientSocket) => { - // response ended before we even got a socket to respond on - if (endRes === NoOp) { - return; - } - - // happens when client upstream is disconnected (or disconnects) - // and the proxy iterates the waiting list and clears the callbacks - // we gracefully inform the user and kill their conn - // without this, the browser will leave some connections open - // and try to use them again for new requests - // TODO(roman) we could instead have a timeout above - // if no socket becomes available within some time, - // we just tell the user no resource available to service request - if (!clientSocket) { - endRes(); - return; - } - - const agent = new BindingAgent({ - socket: clientSocket, - }); - - const opt = { - path: req.url, - agent: agent, - method: req.method, - headers: req.headers - }; - - return new Promise((resolve) => { - // what if error making this request? - const clientReq = http.request(opt, (clientRes) => { - // write response code and headers - res.writeHead(clientRes.statusCode, clientRes.headers); - - // when this pump is done, we end our response - pump(clientRes, res, (err) => { - endRes(); - resolve(); - }); - }); - - // we don't care about when this ends, only if there is error - pump(req, clientReq, (err) => { - if (err) { - endRes(); - resolve(); - } - }); - }); - }); - } - - // handle http upgrade - handleUpgrade(clientId, req, sock) { - const client = this.clients[clientId]; - if (!client) { - return; - } - - client.nextSocket(async (clientSocket) => { - if (!sock.readable || !sock.writable) { - sock.end(); - return; - } - - // happens when client upstream is disconnected (or disconnects) - // and the proxy iterates the waiting list and clears the callbacks - // we gracefully inform the user and kill their conn - // without this, the browser will leave some connections open - // and try to use them again for new requests - // TODO(roman) we could instead have a timeout above - // if no socket becomes available within some time, - // we just tell the user no resource available to service request - if (!clientSocket) { - sock.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(''); - - clientSocket.pipe(sock).pipe(clientSocket); - clientSocket.write(arr.join('\r\n')); - - await new Promise((resolve) => { - sock.once('end', resolve); - }); - }); + getClient(id) { + return this.clients[id]; } } diff --git a/lib/ClientManager.test.js b/lib/ClientManager.test.js new file mode 100644 index 0000000..63eb60e --- /dev/null +++ b/lib/ClientManager.test.js @@ -0,0 +1,51 @@ +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; + assert(!manager.hasClient('foobar')); + }); +}); diff --git a/lib/Proxy.js b/lib/Proxy.js deleted file mode 100644 index ccd0ebf..0000000 --- a/lib/Proxy.js +++ /dev/null @@ -1,193 +0,0 @@ -import net from 'net'; -import EventEmitter from 'events'; -import log from 'book'; -import Debug from 'debug'; - -const debug = Debug('localtunnel:server'); - -const Proxy = function(opt) { - if (!(this instanceof Proxy)) { - return new Proxy(opt); - } - - const self = this; - - self.sockets = []; - self.waiting = []; - self.id = opt.id; - - self.activeSockets = 0; - - // default max is 10 - self.max_tcp_sockets = opt.max_tcp_sockets || 10; - - // new tcp server to service requests for this client - self.server = net.createServer(); - - // track initial user connection setup - self.conn_timeout = undefined; - - self.debug = Debug(`localtunnel:server:${self.id}`); -}; - -Proxy.prototype.__proto__ = EventEmitter.prototype; - -Proxy.prototype.start = function(cb) { - const self = this; - const server = self.server; - - if (self.started) { - cb(new Error('already started')); - return; - } - self.started = true; - - server.on('close', self._cleanup.bind(self)); - server.on('connection', self._handle_socket.bind(self)); - - server.on('error', function(err) { - // where do these errors come from? - // other side creates a connection and then is killed? - if (err.code == 'ECONNRESET' || err.code == 'ETIMEDOUT') { - return; - } - - log.error(err); - }); - - server.listen(function() { - const port = server.address().port; - self.debug('tcp server listening on port: %d', port); - - cb(null, { - // port for lt client tcp connections - port: port, - // maximum number of tcp connections allowed by lt client - max_conn_count: self.max_tcp_sockets - }); - }); - - self._maybe_destroy(); -}; - -Proxy.prototype._maybe_destroy = function() { - const self = this; - - clearTimeout(self.conn_timeout); - - // After last socket is gone, we give opportunity to connect again quickly - self.conn_timeout = setTimeout(function() { - // sometimes the server is already closed but the event has not fired? - try { - clearTimeout(self.conn_timeout); - self.server.close(); - } - catch (err) { - self._cleanup(); - } - }, 1000); -} - -// new socket connection from client for tunneling requests to client -Proxy.prototype._handle_socket = function(socket) { - const self = this; - - // no more socket connections allowed - if (self.activeSockets >= self.max_tcp_sockets) { - return socket.end(); - } - - self.activeSockets = self.activeSockets + 1; - - self.debug('new connection from: %s:%s', socket.address().address, socket.address().port); - - // a single connection is enough to keep client id slot open - clearTimeout(self.conn_timeout); - - socket.once('close', function(had_error) { - self.activeSockets = self.activeSockets - 1; - self.debug('closed socket (error: %s)', had_error); - - // what if socket was servicing a request at this time? - // then it will be put back in available after right? - // we need a list of sockets servicing requests? - - // remove this socket - const idx = self.sockets.indexOf(socket); - if (idx >= 0) { - self.sockets.splice(idx, 1); - } - - // need to track total sockets, not just active available - self.debug('remaining client sockets: %s', self.sockets.length); - - // no more sockets for this ident - if (self.sockets.length === 0) { - self.debug('all sockets disconnected'); - self._maybe_destroy(); - } - }); - - // close will be emitted after this - socket.on('error', function(err) { - // we don't log here to avoid logging crap for misbehaving clients - socket.destroy(); - }); - - self.sockets.push(socket); - self._process_waiting(); -}; - -Proxy.prototype._process_waiting = function() { - const self = this; - const fn = self.waiting.shift(); - if (fn) { - self.debug('handling queued request'); - self.nextSocket(fn); - } -}; - -Proxy.prototype._cleanup = function() { - const self = this; - self.debug('closed tcp socket for client(%s)', self.id); - - clearTimeout(self.conn_timeout); - - // clear waiting by ending responses, (requests?) - self.waiting.forEach(handler => handler(null)); - - self.emit('end'); -}; - -Proxy.prototype.nextSocket = async function(fn) { - const self = this; - - // socket is a tcp connection back to the user hosting the site - const sock = self.sockets.shift(); - if (!sock) { - self.debug('no more clients, queue callback'); - self.waiting.push(fn); - return; - } - - self.debug('processing request'); - await fn(sock); - - if (!sock.destroyed) { - self.debug('retuning socket'); - self.sockets.push(sock); - } - - // no sockets left to process waiting requests - if (self.sockets.length === 0) { - return; - } - - self._process_waiting(); -}; - -Proxy.prototype._done = function() { - const self = this; -}; - -export default Proxy; diff --git a/lib/TunnelAgent.js b/lib/TunnelAgent.js new file mode 100644 index 0000000..215b1f8 --- /dev/null +++ b/lib/TunnelAgent.js @@ -0,0 +1,171 @@ +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'); + + // track maximum allowed sockets + this.activeSockets = 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; + } + + 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) => { + // where do these errors come from? + // other side creates a connection and then is killed? + 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.debug('closed tcp socket'); + clearTimeout(this.connTimeout); + // we will not invoke these callbacks? + // TODO(roman): we could invoke these with errors...? + // this makes downstream have to handle this + this.waitingCreateConn = []; + this.emit('end'); + } + + // new socket connection from client for tunneling requests to client + _onConnection(socket) { + // no more socket connections allowed + if (this.activeSockets >= this.maxTcpSockets) { + this.debug('no more sockets allowed'); + socket.destroy(); + return false; + } + + // a new socket becomes available + if (this.activeSockets == 0) { + this.emit('online'); + } + + this.activeSockets += 1; + this.debug('new connection from: %s:%s', socket.address().address, socket.address().port); + + // a single connection is enough to keep client id slot open + clearTimeout(this.connTimeout); + + socket.once('close', (had_error) => { + this.debug('closed socket (error: %s)', had_error); + this.debug('removing socket'); + this.activeSockets -= 1; + // remove the socket from available list + const idx = this.availableSockets.indexOf(socket); + if (idx >= 0) { + this.availableSockets.splice(idx, 1); + } + // need to track total sockets, not just active available + this.debug('remaining client sockets: %s', this.availableSockets.length); + // no more sockets for this session + // the session will become inactive if client does not reconnect + if (this.availableSockets.length <= 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(); + }); + + // make socket available for those waiting on sockets + this.availableSockets.push(socket); + + // flush anyone waiting on sockets + this._callWaitingCreateConn(); + } + + // invoke when a new socket is available and there may be waiting createConnection calls + _callWaitingCreateConn() { + const fn = this.waitingCreateConn.shift(); + if (!fn) { + return; + } + + this.debug('handling queued request'); + this.createConnection({}, fn); + } + + // fetch a socket from the available socket pool for the agent + // if no socket is available, queue + // cb(err, socket) + createConnection(options, cb) { + // 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'); + return; + } + + this.debug('socket given'); + cb(null, sock); + } + + destroy() { + this.server.close(); + super.destroy(); + } +} + +export default TunnelAgent; diff --git a/lib/TunnelAgent.test.js b/lib/TunnelAgent.test.js new file mode 100644 index 0000000..62839c0 --- /dev/null +++ b/lib/TunnelAgent.test.js @@ -0,0 +1,176 @@ +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'); + }); +}); diff --git a/package.json b/package.json index aad8c63..1624eef 100644 --- a/package.json +++ b/package.json @@ -11,23 +11,21 @@ "dependencies": { "book": "1.3.3", "debug": "3.1.0", - "esm": "3.0.14", + "esm": "3.0.34", "human-readable-ids": "1.0.3", - "koa": "2.4.1", + "koa": "2.5.1", "localenv": "0.2.2", - "on-finished": "2.3.0", "optimist": "0.6.1", - "pump": "2.0.0", "tldjs": "2.3.1" }, "devDependencies": { - "localtunnel": "1.8.0", - "mocha": "2.5.3", + "mocha": "5.1.1", "node-dev": "3.1.3", - "ws": "0.8.0" + "supertest": "3.1.0", + "ws": "5.1.1" }, "scripts": { - "test": "mocha --ui qunit --reporter spec", + "test": "mocha --check-leaks --require esm './**/*.test.js'", "start": "./bin/server", "dev": "node-dev bin/server --port 3000" } diff --git a/server.js b/server.js index 86b2d57..c5c395a 100644 --- a/server.js +++ b/server.js @@ -14,6 +14,7 @@ export default function(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); @@ -53,9 +54,9 @@ export default function(opt) { const isNewClientRequest = ctx.query['new'] !== undefined; if (isNewClientRequest) { - const req_id = hri.random(); - debug('making new client with id %s', req_id); - const info = await manager.newClient(req_id); + 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; @@ -64,7 +65,7 @@ export default function(opt) { } // no new client request, send to landing page - ctx.redirect('https://localtunnel.github.io/www/'); + ctx.redirect(landingPage); }); // anything after the / path is a request for a specific client name @@ -80,10 +81,10 @@ export default function(opt) { return; } - const req_id = parts[1]; + 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(req_id)) { + 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 = { @@ -92,8 +93,8 @@ export default function(opt) { return; } - debug('making new client with id %s', req_id); - const info = await manager.newClient(req_id); + 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; @@ -104,6 +105,7 @@ export default function(opt) { 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; @@ -119,13 +121,14 @@ export default function(opt) { return; } - if (manager.hasClient(clientId)) { - manager.handleRequest(clientId, req, res); + const client = manager.getClient(clientId); + if (!client) { + res.statusCode = 404; + res.end('404'); return; } - res.statusCode = 404; - res.end('404'); + client.handleRequest(req, res); }); server.on('upgrade', (req, socket, head) => { @@ -141,12 +144,13 @@ export default function(opt) { return; } - if (manager.hasClient(clientId)) { - manager.handleUpgrade(clientId, req, socket); + const client = manager.getClient(clientId); + if (!client) { + sock.destroy(); return; } - socket.destroy(); + client.handleUpgrade(req, socket); }); return server; diff --git a/server.test.js b/server.test.js new file mode 100644 index 0000000..3f72f2d --- /dev/null +++ b/server.test.js @@ -0,0 +1,85 @@ +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)); + }); +}); \ No newline at end of file diff --git a/test/basic.js b/test/basic.js deleted file mode 100644 index c50b7e2..0000000 --- a/test/basic.js +++ /dev/null @@ -1,166 +0,0 @@ -import http from 'http'; -import url from 'url'; -import assert from 'assert'; -import localtunnel from 'localtunnel'; - -import CreateServer from '../server'; - -const localtunnel_server = CreateServer(); - -process.on('uncaughtException', (err) => { - console.error(err); -}); - -process.on('unhandledRejection', (reason, promise) => { - console.error(reason); -}); - -suite('basic'); - -var lt_server_port; - -before('set up localtunnel server', function(done) { - var server = localtunnel_server.listen(function() { - lt_server_port = server.address().port; - done(); - }); -}); - -test('landing page', function(done) { - var opt = { - host: 'localhost', - port: lt_server_port, - headers: { - host: 'example.com' - }, - path: '/' - } - - var req = http.request(opt, function(res) { - res.setEncoding('utf8'); - assert.equal(res.headers.location, 'https://localtunnel.github.io/www/') - done(); - }); - - req.end(); -}); - -before('set up local http server', function(done) { - var server = http.createServer(); - server.on('request', function(req, res) { - res.write('foo'); - res.end(); - }); - server.listen(function() { - var port = server.address().port; - - test._fake_port = port; - done(); - }); -}); - -before('set up localtunnel client', function(done) { - var opt = { - host: 'http://localhost:' + lt_server_port, - }; - - localtunnel(test._fake_port, opt, function(err, tunnel) { - assert.ifError(err); - var url = tunnel.url; - assert.ok(new RegExp('^http:\/\/.*localhost:' + lt_server_port + '$').test(url)); - test._fake_url = url; - done(err); - }); -}); - -test('query localtunnel server w/ ident', function(done) { - var uri = test._fake_url; - var hostname = url.parse(uri).hostname; - - var opt = { - host: 'localhost', - port: lt_server_port, - headers: { - host: hostname + '.tld' - }, - path: '/' - } - - var req = http.request(opt, function(res) { - res.setEncoding('utf8'); - var body = ''; - - res.on('data', function(chunk) { - body += chunk; - }); - - res.on('end', function() { - assert.equal('foo', body); - - // TODO(shtylman) shutdown client - done(); - }); - }); - - req.end(); -}); - -test('request specific domain', function(done) { - var opt = { - host: 'http://localhost:' + lt_server_port, - subdomain: 'abcd' - }; - - localtunnel(test._fake_port, opt, function(err, tunnel) { - assert.ifError(err); - var url = tunnel.url; - assert.ok(new RegExp('^http:\/\/.*localhost:' + lt_server_port + '$').test(url)); - test._fake_url = url; - done(err); - }); -}); - -test('request domain with dash', function(done) { - var opt = { - host: 'http://localhost:' + lt_server_port, - subdomain: 'abcd-1234' - }; - - localtunnel(test._fake_port, opt, function(err, tunnel) { - assert.ifError(err); - var url = tunnel.url; - assert.ok(new RegExp('^http:\/\/.*localhost:' + lt_server_port + '$').test(url)); - test._fake_url = url; - done(err); - }); -}); - -test('request domain that is too long', function(done) { - var opt = { - host: 'http://localhost:' + lt_server_port, - subdomain: 'thisdomainisoutsidethesizeofwhatweallowwhichissixtythreecharacters' - }; - - localtunnel(test._fake_port, opt, function(err, tunnel) { - assert(err); - assert.equal(err.message, 'Invalid subdomain. Subdomains must be lowercase and between 4 and 63 alphanumeric characters.'); - done(); - }); -}); - -test('request uppercase domain', function(done) { - var opt = { - host: 'http://localhost:' + lt_server_port, - subdomain: 'ABCD' - }; - - localtunnel(test._fake_port, opt, function(err, tunnel) { - assert(err); - assert.equal(err.message, 'Invalid subdomain. Subdomains must be lowercase and between 4 and 63 alphanumeric characters.'); - done(); - }); -}); - -after('shutdown', function() { - localtunnel_server.close(); -}); diff --git a/test/domain.js b/test/domain.js deleted file mode 100644 index 6fa2b02..0000000 --- a/test/domain.js +++ /dev/null @@ -1,52 +0,0 @@ -import http from 'http'; -import url from 'url'; -import assert from 'assert'; -import localtunnel from 'localtunnel'; - -import CreateServer from '../server'; - -const localtunnel_server = CreateServer({ - domain: 'domain.example.com', -}); - -process.on('uncaughtException', (err) => { - console.error(err); -}); - -process.on('unhandledRejection', (reason, promise) => { - console.error(reason); -}); - -suite('domain'); - -var lt_server_port; - -before('set up localtunnel server', function(done) { - var server = localtunnel_server.listen(function() { - lt_server_port = server.address().port; - done(); - }); -}); - -test('landing page', function(done) { - var opt = { - host: 'localhost', - port: lt_server_port, - headers: { - host: 'domain.example.com' - }, - path: '/' - } - - var req = http.request(opt, function(res) { - res.setEncoding('utf8'); - assert.equal(res.headers.location, 'https://localtunnel.github.io/www/') - done(); - }); - - req.end(); -}); - -after('shutdown', function() { - localtunnel_server.close(); -}); diff --git a/test/mocha.opts b/test/mocha.opts deleted file mode 100644 index ce1c914..0000000 --- a/test/mocha.opts +++ /dev/null @@ -1,4 +0,0 @@ ---check-leaks ---reporter spec ---ui qunit ---require esm diff --git a/test/queue.js b/test/queue.js deleted file mode 100644 index b945586..0000000 --- a/test/queue.js +++ /dev/null @@ -1,105 +0,0 @@ -import http from 'http'; -import url from 'url'; -import assert from 'assert'; -import localtunnel from 'localtunnel'; - -import CreateServer from '../server'; - -suite('queue'); - -var localtunnel_server = CreateServer({ - max_tcp_sockets: 1 -}); - -var server; -var lt_server_port; - -before('set up localtunnel server', function(done) { - var lt_server = localtunnel_server.listen(function() { - lt_server_port = lt_server.address().port; - done(); - }); -}); - -before('set up local http server', function(done) { - server = http.createServer(); - server.on('request', function(req, res) { - // respond sometime later - setTimeout(function() { - res.setHeader('x-count', req.headers['x-count']); - res.end('foo'); - }, 500); - }); - - server.listen(function() { - var port = server.address().port; - - test._fake_port = port; - done(); - }); -}); - -before('set up localtunnel client', function(done) { - var opt = { - host: 'http://localhost:' + lt_server_port, - }; - - localtunnel(test._fake_port, opt, function(err, tunnel) { - assert.ifError(err); - var url = tunnel.url; - assert.ok(new RegExp('^http:\/\/.*localhost:' + lt_server_port + '$').test(url)); - test._fake_url = url; - done(err); - }); -}); - -test('query localtunnel server w/ ident', function(done) { - var uri = test._fake_url; - var hostname = url.parse(uri).hostname; - - var count = 0; - var opt = { - host: 'localhost', - port: lt_server_port, - agent: false, - headers: { - host: hostname + '.tld' - }, - path: '/' - } - - var num_requests = 2; - var responses = 0; - - function maybe_done() { - if (++responses >= num_requests) { - done(); - } - } - - function make_req() { - opt.headers['x-count'] = count++; - http.get(opt, function(res) { - res.setEncoding('utf8'); - var body = ''; - - res.on('data', function(chunk) { - body += chunk; - }); - - res.on('end', function() { - assert.equal('foo', body); - maybe_done(); - }); - }); - } - - for (var i=0 ; i= 1.3.1 < 2" - -http-signature@~0.11.0: - version "0.11.0" - resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-0.11.0.tgz#1796cf67a001ad5cd6849dca0991485f09089fe6" - dependencies: - asn1 "0.1.11" - assert-plus "^0.1.5" - ctype "0.5.3" + setprototypeof "1.1.0" + statuses ">= 1.4.0 < 2" human-readable-ids@1.0.3: version "1.0.3" @@ -451,14 +367,17 @@ indexof@~0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/indexof/-/indexof-0.0.1.tgz#82dc336d232b9062179d05ab3293a66059fd435d" -inherits@2, inherits@2.0.3, inherits@~2.0.1: +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" -invert-kv@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/invert-kv/-/invert-kv-1.0.0.tgz#104a8e4aaca6d3d8cd157a8ef8bfab2d7a3ffdb6" - is-arrayish@^0.2.1: version "0.2.1" resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" @@ -475,33 +394,14 @@ is-finite@^1.0.0: dependencies: number-is-nan "^1.0.0" -is-fullwidth-code-point@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz#ef9e31386f031a7f0d643af82fde50c457ef00cb" - dependencies: - number-is-nan "^1.0.0" - is-generator-function@^1.0.3: - version "1.0.6" - resolved "https://registry.yarnpkg.com/is-generator-function/-/is-generator-function-1.0.6.tgz#9e71653cd15fff341c79c4151460a131d31e9fc4" - -is-my-json-valid@^2.12.4: - version "2.16.1" - resolved "https://registry.yarnpkg.com/is-my-json-valid/-/is-my-json-valid-2.16.1.tgz#5a846777e2c2620d1e69104e5d3a03b1f6088f11" - dependencies: - generate-function "^2.0.0" - generate-object-property "^1.1.0" - jsonpointer "^4.0.0" - xtend "^4.0.0" + 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-property@^1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/is-property/-/is-property-1.0.2.tgz#57fe1c4e48474edd65b09911f26b1cd4095dda84" - is-utf8@^0.2.0: version "0.2.1" resolved "https://registry.yarnpkg.com/is-utf8/-/is-utf8-0.2.1.tgz#4b0da1442104d1b336340e80797e865cf39f7d72" @@ -518,25 +418,6 @@ isexe@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" -isstream@~0.1.2: - version "0.1.2" - resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a" - -jade@0.26.3: - version "0.26.3" - resolved "https://registry.yarnpkg.com/jade/-/jade-0.26.3.tgz#8f10d7977d8d79f2f6ff862a81b0513ccb25686c" - dependencies: - commander "0.6.1" - mkdirp "0.3.0" - -json-stringify-safe@~5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb" - -jsonpointer@^4.0.0: - version "4.0.1" - resolved "https://registry.yarnpkg.com/jsonpointer/-/jsonpointer-4.0.1.tgz#4fd92cb34e0e9db3c89c8622ecf51f9b978c6cb9" - keygrip@~1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/keygrip/-/keygrip-1.0.2.tgz#ad3297c557069dea8bcfe7a4fa491b75c5ddeb91" @@ -566,9 +447,9 @@ 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@2.4.1: - version "2.4.1" - resolved "https://registry.yarnpkg.com/koa/-/koa-2.4.1.tgz#d449cfb970a7e9da571f699eda40bb9e32eb1484" +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" @@ -595,12 +476,6 @@ koa@2.4.1: type-is "^1.5.5" vary "^1.0.0" -lcid@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/lcid/-/lcid-1.0.0.tgz#308accafa0bc483a3867b4b6f2b9506251d1b835" - dependencies: - invert-kv "^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" @@ -617,15 +492,6 @@ localenv@0.2.2: dependencies: commander "2.5.0" -localtunnel@1.8.0: - version "1.8.0" - resolved "https://registry.yarnpkg.com/localtunnel/-/localtunnel-1.8.0.tgz#e8feba5fa015bfa2cc1c5daff0973eaf3adf3337" - dependencies: - debug "2.2.0" - openurl "1.1.0" - request "2.65.0" - yargs "3.29.0" - lodash._arraycopy@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/lodash._arraycopy/-/lodash._arraycopy-3.0.0.tgz#76e7b7c1f1fb92547374878a562ed06a3e50f6e1" @@ -699,10 +565,6 @@ lodash.toarray@^4.4.0: version "4.4.0" resolved "https://registry.yarnpkg.com/lodash.toarray/-/lodash.toarray-4.4.0.tgz#24c4bfcd6b2fba38bfd0594db1179d8e9b656561" -lodash@^4.14.0: - version "4.17.4" - resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.4.tgz#78203a4d1c328ae1d86dca6460e369b57f4055ae" - loud-rejection@^1.0.0: version "1.6.0" resolved "https://registry.yarnpkg.com/loud-rejection/-/loud-rejection-1.6.0.tgz#5b46f80147edee578870f086d04821cf998e551f" @@ -710,17 +572,13 @@ loud-rejection@^1.0.0: currently-unhandled "^0.4.1" signal-exit "^3.0.0" -lru-cache@2: - version "2.7.3" - resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-2.7.3.tgz#6d4524e8b955f95d4f5b58851ce21dd72fb4e952" - 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@^1.6.2: - version "1.7.0" - resolved "https://registry.yarnpkg.com/marked-terminal/-/marked-terminal-1.7.0.tgz#c8c460881c772c7604b64367007ee5f77f125904" +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" @@ -728,9 +586,9 @@ marked-terminal@^1.6.2: lodash.assign "^4.2.0" node-emoji "^1.4.1" -marked@^0.3.6: - version "0.3.7" - resolved "https://registry.yarnpkg.com/marked/-/marked-0.3.7.tgz#80ef3bbf1bd00d1c9cfebe42ba1b8c85da258d0d" +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" @@ -751,22 +609,29 @@ meow@^3.3.0: redent "^1.0.0" trim-newlines "^1.0.0" -mime-db@~1.30.0: - version "1.30.0" - resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.30.0.tgz#74c643da2dd9d6a45399963465b26d5ca7d71f01" +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-types@^2.0.7, mime-types@^2.1.11, mime-types@~2.1.15, mime-types@~2.1.16, mime-types@~2.1.7: - version "2.1.17" - resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.17.tgz#09d7a393f03e995a79f8af857b70a9e0ab16557a" - dependencies: - mime-db "~1.30.0" +mime-db@~1.33.0: + version "1.33.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.33.0.tgz#a3492050a5cb9b63450541e39d9788d2272783db" -minimatch@0.3: - version "0.3.0" - resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-0.3.0.tgz#275d8edaac4f1bb3326472089e7949c8394699dd" +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: - lru-cache "2" - sigmund "~1.0.0" + 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" @@ -780,47 +645,32 @@ minimist@~0.0.1: version "0.0.10" resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.10.tgz#de3f98543dbf96082be48ad1a0c7cda836301dcf" -mkdirp@0.3.0: - version "0.3.0" - resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.3.0.tgz#1bbf5ab1ba827af23575143490426455f481fe1e" - 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@2.5.3: - version "2.5.3" - resolved "https://registry.yarnpkg.com/mocha/-/mocha-2.5.3.tgz#161be5bdeb496771eb9b35745050b622b5aefc58" +mocha@5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/mocha/-/mocha-5.1.1.tgz#b774c75609dac05eb48f4d9ba1d827b97fde8a7b" dependencies: - commander "2.3.0" - debug "2.2.0" - diff "1.4.0" - escape-string-regexp "1.0.2" - glob "3.2.11" - growl "1.9.2" - jade "0.26.3" + 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 "1.2.0" - to-iso-string "0.0.2" - -ms@0.7.1: - version "0.7.1" - resolved "https://registry.yarnpkg.com/ms/-/ms-0.7.1.tgz#9cd13c03adbff25b65effde7ce864ee952017098" + 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" -nan@^2.0.5: - version "2.8.0" - resolved "https://registry.yarnpkg.com/nan/-/nan-2.8.0.tgz#ed715f3fe9de02b57a5e6252d90a96675e1f085a" - -nan@~2.4.0: - version "2.4.0" - resolved "https://registry.yarnpkg.com/nan/-/nan-2.4.0.tgz#fb3c59d45fe4effe215f0b890f8adf6eb32d2232" - negotiator@0.6.1: version "0.6.1" resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.1.tgz#2b327184e8992101177b28563fb5e7102acd0ca9" @@ -854,10 +704,6 @@ node-notifier@^4.0.2: shellwords "^0.1.0" which "^1.0.5" -node-uuid@~1.4.3: - version "1.4.8" - resolved "https://registry.yarnpkg.com/node-uuid/-/node-uuid-1.4.8.tgz#b040eb0923968afabf8d32fb1f17f1167fdab907" - 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" @@ -871,10 +717,6 @@ 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" -oauth-sign@~0.8.0: - version "0.8.2" - resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.8.2.tgz#46a6ab7f0aead8deae9ec0565780b7d4efeb9d43" - object-assign@^4.0.1: version "4.1.1" resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" @@ -887,13 +729,13 @@ object-keys@~0.2.0: indexof "~0.0.1" is "~0.2.6" -on-finished@2.3.0, on-finished@^2.1.0: +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.1, once@^1.4.0: +once@^1.3.0: version "1.4.0" resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" dependencies: @@ -903,10 +745,6 @@ only@0.0.2: version "0.0.2" resolved "https://registry.yarnpkg.com/only/-/only-0.0.2.tgz#2afde84d03e50b9a8edc444e30610a70295edfb4" -openurl@1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/openurl/-/openurl-1.1.0.tgz#e2f2189d999c04823201f083f0f1a7cd8903187a" - optimist@0.6.1: version "0.6.1" resolved "https://registry.yarnpkg.com/optimist/-/optimist-0.6.1.tgz#da3ea74686fa21a19a111c326e90eb15a0196686" @@ -914,16 +752,6 @@ optimist@0.6.1: minimist "~0.0.1" wordwrap "~0.0.2" -options@>=0.0.5: - version "0.0.6" - resolved "https://registry.yarnpkg.com/options/-/options-0.0.6.tgz#ec22d312806bb53e731773e7cdaefcf1c643128f" - -os-locale@^1.4.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/os-locale/-/os-locale-1.4.0.tgz#20f9f17ae29ed345e8bde583b13d2009803c14d9" - dependencies: - lcid "^1.0.0" - parse-json@^2.2.0: version "2.2.0" resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-2.2.0.tgz#f480f40434ef80741f8469099f8dea18f55a4dc9" @@ -940,6 +768,10 @@ path-exists@^2.0.0: 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" @@ -966,24 +798,17 @@ pinkie@^2.0.0: version "2.0.4" resolved "https://registry.yarnpkg.com/pinkie/-/pinkie-2.0.4.tgz#72556b80cfa0d48a974e80e77248e80ed4f7f870" -process-nextick-args@~1.0.6: - version "1.0.7" - resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-1.0.7.tgz#150e20b756590ad3f91093f25a4f2ad8bff30ba3" - -pump@2.0.0: +process-nextick-args@~2.0.0: version "2.0.0" - resolved "https://registry.yarnpkg.com/pump/-/pump-2.0.0.tgz#7946da1c8d622b098e2ceb2d3476582470829c9d" - dependencies: - end-of-stream "^1.1.0" - once "^1.3.1" + resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.0.tgz#a37d732f4271b4ab1ad070d35508e8290788ffaa" punycode@^1.4.1: version "1.4.1" resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e" -qs@~5.2.0: - version "5.2.1" - resolved "https://registry.yarnpkg.com/qs/-/qs-5.2.1.tgz#801fee030e0b9450d6385adc48a4cc55b44aedfc" +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" @@ -1000,15 +825,16 @@ read-pkg@^1.0.0: normalize-package-data "^2.3.2" path-type "^1.0.0" -readable-stream@~2.0.5: - version "2.0.6" - resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.0.6.tgz#8f90341e68a53ccc928788dacfcd11b36eb9b78e" +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.1" + inherits "~2.0.3" isarray "~1.0.0" - process-nextick-args "~1.0.6" - string_decoder "~0.10.x" + 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: @@ -1030,101 +856,69 @@ repeating@^2.0.0: dependencies: is-finite "^1.0.0" -request@2.65.0: - version "2.65.0" - resolved "https://registry.yarnpkg.com/request/-/request-2.65.0.tgz#cc1a3bc72b96254734fc34296da322f9486ddeba" - dependencies: - aws-sign2 "~0.6.0" - bl "~1.0.0" - caseless "~0.11.0" - combined-stream "~1.0.5" - extend "~3.0.0" - forever-agent "~0.6.1" - form-data "~1.0.0-rc3" - har-validator "~2.0.2" - hawk "~3.1.0" - http-signature "~0.11.0" - isstream "~0.1.2" - json-stringify-safe "~5.0.1" - mime-types "~2.1.7" - node-uuid "~1.4.3" - oauth-sign "~0.8.0" - qs "~5.2.0" - stringstream "~0.0.4" - tough-cookie "~2.2.0" - tunnel-agent "~0.4.1" - resolve@^1.0.0: - version "1.5.0" - resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.5.0.tgz#1f09acce796c9a762579f31b2c1cc4c3cddf9f36" + version "1.7.1" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.7.1.tgz#aadd656374fd298aee895bc026b8297418677fd3" dependencies: path-parse "^1.0.5" -"semver@2 || 3 || 4 || 5", semver@^5.1.0: - version "5.4.1" - resolved "https://registry.yarnpkg.com/semver/-/semver-5.4.1.tgz#e059c09d8571f0540823733433505d3a2f00b18e" +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" -setprototypeof@1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.0.3.tgz#66567e37043eeb4f04d91bd658c0cbefb55b8e04" +"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" -sigmund@~1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/sigmund/-/sigmund-1.0.1.tgz#3ff21f198cad2175f9f3b781853fd94d0d19b590" - signal-exit@^3.0.0: version "3.0.2" resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.2.tgz#b5fdc08f1287ea1178628e415e25132b73646c6d" -sntp@1.x.x: - version "1.0.9" - resolved "https://registry.yarnpkg.com/sntp/-/sntp-1.0.9.tgz#6541184cc90aeea6c6e7b35e2659082443c66198" +spdx-correct@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-3.0.0.tgz#05a5b4d7153a195bc92c3c425b69f3b2a9524c82" dependencies: - hoek "2.x.x" + spdx-expression-parse "^3.0.0" + spdx-license-ids "^3.0.0" -spdx-correct@~1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-1.0.2.tgz#4b3073d933ff51f3912f03ac5519498a4150db40" +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-license-ids "^1.0.2" + spdx-exceptions "^2.1.0" + spdx-license-ids "^3.0.0" -spdx-expression-parse@~1.0.0: - version "1.0.4" - resolved "https://registry.yarnpkg.com/spdx-expression-parse/-/spdx-expression-parse-1.0.4.tgz#9bdf2f20e1f40ed447fbe273266191fced51626c" - -spdx-license-ids@^1.0.2: - version "1.2.2" - resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-1.2.2.tgz#c9df7a3424594ade6bd11900d596696dc06bac57" +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.3.1 < 2", statuses@^1.2.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.4.0.tgz#bb73d446da2796106efcc1b601a253d6c46bd087" +"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-width@^1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-1.0.2.tgz#118bdf5b8cdc51a2a7e70d211e07e2b0b9b107d3" +string_decoder@~1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8" dependencies: - code-point-at "^1.0.0" - is-fullwidth-code-point "^1.0.0" - strip-ansi "^3.0.0" + safe-buffer "~5.1.0" -string_decoder@~0.10.x: - version "0.10.31" - resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-0.10.31.tgz#62e203bc41766c6c28c9fc84301dab1c5310fa94" - -stringstream@~0.0.4: - version "0.0.5" - resolved "https://registry.yarnpkg.com/stringstream/-/stringstream-0.0.5.tgz#4e484cd4de5a0bbbee18e46307710a8a81621878" - -strip-ansi@^3.0.0, strip-ansi@^3.0.1: +strip-ansi@^3.0.0: version "3.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-3.0.1.tgz#6a385fb8853d952d5ff05d0e8aaf94278dc63dcf" dependencies: @@ -1142,9 +936,33 @@ strip-indent@^1.0.1: dependencies: get-stdin "^4.0.1" -supports-color@1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-1.2.0.tgz#ff1ed1e61169d06b3cf2d588e188b18d8847e17e" +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" @@ -1156,50 +974,27 @@ tldjs@2.3.1: dependencies: punycode "^1.4.1" -to-iso-string@0.0.2: - version "0.0.2" - resolved "https://registry.yarnpkg.com/to-iso-string/-/to-iso-string-0.0.2.tgz#4dc19e664dfccbe25bd8db508b00c6da158255d1" - -tough-cookie@~2.2.0: - version "2.2.2" - resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.2.2.tgz#c83a1830f4e5ef0b93ef2a3488e724f8de016ac7" - trim-newlines@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/trim-newlines/-/trim-newlines-1.0.0.tgz#5887966bb582a4503a41eb524f7d35011815a613" -tunnel-agent@~0.4.1: - version "0.4.3" - resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.4.3.tgz#6373db76909fe570e08d73583365ed828a74eeeb" - type-is@^1.5.5: - version "1.6.15" - resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.15.tgz#cab10fb4909e441c82842eafe1ad646c81804410" + 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.15" - -ultron@1.0.x: - version "1.0.2" - resolved "https://registry.yarnpkg.com/ultron/-/ultron-1.0.2.tgz#ace116ab557cd197386a4e88f4685378c8b2e4fa" - -utf-8-validate@1.2.x: - version "1.2.2" - resolved "https://registry.yarnpkg.com/utf-8-validate/-/utf-8-validate-1.2.2.tgz#8bb871a4741e085c70487ca7acdbd7d6d36029eb" - dependencies: - bindings "~1.2.1" - nan "~2.4.0" + mime-types "~2.1.18" 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.1" - resolved "https://registry.yarnpkg.com/validate-npm-package-license/-/validate-npm-package-license-3.0.1.tgz#2804babe712ad3379459acfbe24746ab2c303fbc" + 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 "~1.0.0" - spdx-expression-parse "~1.0.0" + spdx-correct "^3.0.0" + spdx-expression-parse "^3.0.0" vary@^1.0.0: version "1.1.2" @@ -1211,38 +1006,19 @@ which@^1.0.5: dependencies: isexe "^2.0.0" -window-size@^0.1.2: - version "0.1.4" - resolved "https://registry.yarnpkg.com/window-size/-/window-size-0.1.4.tgz#f8e1aa1ee5a53ec5bf151ffa09742a6ad7697876" - wordwrap@~0.0.2: version "0.0.3" resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-0.0.3.tgz#a3d5da6cd5c0bc0008d37234bbaf1bed63059107" -wrap-ansi@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-2.1.0.tgz#d8fc3d284dd05794fe84973caecdd1cf824fdd85" - dependencies: - string-width "^1.0.1" - strip-ansi "^3.0.1" - wrappy@1: version "1.0.2" resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" -ws@0.8.0: - version "0.8.0" - resolved "https://registry.yarnpkg.com/ws/-/ws-0.8.0.tgz#ac60ebad312121d01e16cc3383d7ec67ad0f0f1f" +ws@5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/ws/-/ws-5.1.1.tgz#1d43704689711ac1942fd2f283e38f825c4b8b95" dependencies: - options ">=0.0.5" - ultron "1.0.x" - optionalDependencies: - bufferutil "1.2.x" - utf-8-validate "1.2.x" - -xtend@^4.0.0: - version "4.0.1" - resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.1.tgz#a5c6d532be656e23db820efb943a1f04998d63af" + async-limiter "~1.0.0" xtend@~2.0.6: version "2.0.6" @@ -1250,18 +1026,3 @@ xtend@~2.0.6: dependencies: is-object "~0.1.2" object-keys "~0.2.0" - -y18n@^3.2.0: - version "3.2.1" - resolved "https://registry.yarnpkg.com/y18n/-/y18n-3.2.1.tgz#6d15fba884c08679c0d77e88e7759e811e07fa41" - -yargs@3.29.0: - version "3.29.0" - resolved "https://registry.yarnpkg.com/yargs/-/yargs-3.29.0.tgz#1aab9660eae79d8b8f675bcaeeab6ee34c2cf69c" - dependencies: - camelcase "^1.2.1" - cliui "^3.0.3" - decamelize "^1.0.0" - os-locale "^1.4.0" - window-size "^0.1.2" - y18n "^3.2.0" -- 2.54.0 From 743895720c0517d16a83b4e52a524ed5f6dac512 Mon Sep 17 00:00:00 2001 From: Roman Shtylman Date: Tue, 15 May 2018 19:18:12 -0400 Subject: [PATCH 16/20] Dockerfile: update to node v10 --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index fb8ac3c..ddda30b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM node:9.2.1-alpine +FROM node:10.1.0-alpine WORKDIR /app -- 2.54.0 From 317db73bdcdcfb23f64056b64da9ba4034374775 Mon Sep 17 00:00:00 2001 From: Roman Shtylman Date: Wed, 16 May 2018 10:21:56 -0400 Subject: [PATCH 17/20] use pump to pipe sockets Ensures that destination socket close or destroy also does the same for the source socket. --- lib/Client.js | 32 +++++++++++++--- lib/ClientManager.js | 11 +++++- lib/ClientManager.test.js | 8 +++- lib/TunnelAgent.js | 78 +++++++++++++++++++-------------------- package.json | 1 + server.js | 2 +- yarn.lock | 15 +++++++- 7 files changed, 97 insertions(+), 50 deletions(-) diff --git a/lib/Client.js b/lib/Client.js index aae3df0..ccb7274 100644 --- a/lib/Client.js +++ b/lib/Client.js @@ -1,6 +1,6 @@ import http from 'http'; - -import TunnelAgent from './TunnelAgent'; +import Debug from 'debug'; +import pump from 'pump'; // A client encapsulates req/res handling using an agent // @@ -9,9 +9,11 @@ import TunnelAgent from './TunnelAgent'; class Client { constructor(options) { this.agent = options.agent; + this.debug = Debug('lt:Client'); } handleRequest(req, res) { + this.debug('> %s', req.url); const opt = { path: req.url, agent: this.agent, @@ -20,23 +22,38 @@ class Client { }; const clientReq = http.request(opt, (clientRes) => { + this.debug('< %s', req.url); // write response code and headers res.writeHead(clientRes.statusCode, clientRes.headers); - clientRes.pipe(res); + + // 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 }); - req.pipe(clientReq); + // 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(); @@ -45,6 +62,7 @@ class Client { // socket met have disconnected while we waiting for a socket if (!socket.readable || !socket.writable) { + conn.destroy(); socket.end(); return; } @@ -60,7 +78,9 @@ class Client { arr.push(''); arr.push(''); - conn.pipe(socket).pipe(conn); + // 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/ClientManager.js b/lib/ClientManager.js index b41270f..6279982 100644 --- a/lib/ClientManager.js +++ b/lib/ClientManager.js @@ -20,6 +20,8 @@ class ClientManager { }; this.debug = Debug('lt:ClientManager'); + + this.graceTimeout = null; } // create a new tunnel with `id` @@ -36,11 +38,13 @@ class ClientManager { const maxSockets = this.opt.max_tcp_sockets; const agent = new TunnelAgent({ + clientId: id, maxSockets: 10, }); agent.on('online', () => { this.debug('client online %s', id); + clearTimeout(this.graceTimeout); }); agent.on('offline', () => { @@ -48,7 +52,11 @@ class ClientManager { // this period is short as the client is expected to maintain connections actively // if they client does not reconnect on a dropped connection they need to re-establish this.debug('client offline %s', id); - this.removeClient(id); + + // client is given a grace period in which they can re-connect before they are _removed_ + this.graceTimeout = setTimeout(() => { + this.removeClient(id); + }, 1000); }); // TODO(roman): an agent error removes the client, the user needs to re-connect? @@ -81,6 +89,7 @@ class ClientManager { } removeClient(id) { + this.debug('removing client: %s', id); const client = this.clients[id]; if (!client) { return; diff --git a/lib/ClientManager.test.js b/lib/ClientManager.test.js index 63eb60e..3282553 100644 --- a/lib/ClientManager.test.js +++ b/lib/ClientManager.test.js @@ -46,6 +46,12 @@ describe('ClientManager', () => { 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); }); diff --git a/lib/TunnelAgent.js b/lib/TunnelAgent.js index 215b1f8..f940aa5 100644 --- a/lib/TunnelAgent.js +++ b/lib/TunnelAgent.js @@ -25,10 +25,10 @@ class TunnelAgent extends Agent { // once a socket is available it is handed out to the next callback this.waitingCreateConn = []; - this.debug = Debug('lt:TunnelAgent'); + this.debug = Debug(`lt:TunnelAgent[${options.clientId}]`); // track maximum allowed sockets - this.activeSockets = 0; + this.connectedSockets = 0; this.maxTcpSockets = options.maxTcpSockets || DEFAULT_MAX_SOCKETS; // new tcp server to service requests for this client @@ -36,6 +36,7 @@ class TunnelAgent extends Agent { // flag to avoid double starts this.started = false; + this.closed = false; } listen() { @@ -48,8 +49,7 @@ class TunnelAgent extends Agent { server.on('close', this._onClose.bind(this)); server.on('connection', this._onConnection.bind(this)); server.on('error', (err) => { - // where do these errors come from? - // other side creates a connection and then is killed? + // These errors happen from killed connections, we don't worry about them if (err.code == 'ECONNRESET' || err.code == 'ETIMEDOUT') { return; } @@ -70,11 +70,12 @@ class TunnelAgent extends Agent { } _onClose() { + this.closed = true; this.debug('closed tcp socket'); - clearTimeout(this.connTimeout); - // we will not invoke these callbacks? - // TODO(roman): we could invoke these with errors...? - // this makes downstream have to handle this + // flush any waiting connections + for (const conn of this.waitingCreateConn) { + conn(new Error('closed'), null); + } this.waitingCreateConn = []; this.emit('end'); } @@ -82,37 +83,23 @@ class TunnelAgent extends Agent { // new socket connection from client for tunneling requests to client _onConnection(socket) { // no more socket connections allowed - if (this.activeSockets >= this.maxTcpSockets) { + if (this.connectedSockets >= this.maxTcpSockets) { this.debug('no more sockets allowed'); socket.destroy(); return false; } - // a new socket becomes available - if (this.activeSockets == 0) { - this.emit('online'); - } - - this.activeSockets += 1; - this.debug('new connection from: %s:%s', socket.address().address, socket.address().port); - - // a single connection is enough to keep client id slot open - clearTimeout(this.connTimeout); - - socket.once('close', (had_error) => { - this.debug('closed socket (error: %s)', had_error); - this.debug('removing socket'); - this.activeSockets -= 1; + 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); } - // need to track total sockets, not just active available - this.debug('remaining client sockets: %s', this.availableSockets.length); - // no more sockets for this session - // the session will become inactive if client does not reconnect - if (this.availableSockets.length <= 0) { + + this.debug('connected sockets: %s', this.connectedSockets); + if (this.connectedSockets <= 0) { this.debug('all sockets disconnected'); this.emit('offline'); } @@ -125,28 +112,38 @@ class TunnelAgent extends Agent { socket.destroy(); }); - // make socket available for those waiting on sockets - this.availableSockets.push(socket); + if (this.connectedSockets === 0) { + this.emit('online'); + } - // flush anyone waiting on sockets - this._callWaitingCreateConn(); - } + this.connectedSockets += 1; + this.debug('new connection from: %s:%s', socket.address().address, socket.address().port); - // invoke when a new socket is available and there may be waiting createConnection calls - _callWaitingCreateConn() { + // if there are queued callbacks, give this socket now and don't queue into available const fn = this.waitingCreateConn.shift(); - if (!fn) { + if (fn) { + this.debug('giving socket to queued conn request'); + setTimeout(() => { + fn(null, socket); + }, 0); return; } - this.debug('handling queued request'); - this.createConnection({}, fn); + // 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(); @@ -154,7 +151,8 @@ class TunnelAgent extends Agent { // wait until we have one if (!sock) { this.waitingCreateConn.push(cb); - this.debug('waiting'); + this.debug('waiting connected: %s', this.connectedSockets); + this.debug('waiting available: %s', this.availableSockets.length); return; } diff --git a/package.json b/package.json index 1624eef..047eb25 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,7 @@ "koa": "2.5.1", "localenv": "0.2.2", "optimist": "0.6.1", + "pump": "3.0.0", "tldjs": "2.3.1" }, "devDependencies": { diff --git a/server.js b/server.js index c5c395a..fb5124b 100644 --- a/server.js +++ b/server.js @@ -146,7 +146,7 @@ export default function(opt) { const client = manager.getClient(clientId); if (!client) { - sock.destroy(); + socket.destroy(); return; } diff --git a/yarn.lock b/yarn.lock index 9c33483..eba9339 100644 --- a/yarn.lock +++ b/yarn.lock @@ -217,6 +217,12 @@ 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" @@ -735,7 +741,7 @@ on-finished@^2.1.0: dependencies: ee-first "1.1.1" -once@^1.3.0: +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: @@ -802,6 +808,13 @@ 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" -- 2.54.0 From cf070d4ce99852cf110ccbca8b0ed23515b984b7 Mon Sep 17 00:00:00 2001 From: Roman Shtylman Date: Wed, 16 May 2018 16:41:05 -0400 Subject: [PATCH 18/20] add /api/tunnels/:id/status endpoint Provides connected_sockets information for a tunnel. --- lib/Client.js | 4 ++++ lib/TunnelAgent.js | 6 ++++++ lib/TunnelAgent.test.js | 7 +++++++ package.json | 1 + server.js | 29 ++++++++++++++++++++--------- server.test.js | 24 ++++++++++++++++++++++++ yarn.lock | 29 +++++++++++++++++++++++++++-- 7 files changed, 89 insertions(+), 11 deletions(-) diff --git a/lib/Client.js b/lib/Client.js index ccb7274..16b522e 100644 --- a/lib/Client.js +++ b/lib/Client.js @@ -12,6 +12,10 @@ class Client { this.debug = Debug('lt:Client'); } + stats() { + return this.agent.stats(); + } + handleRequest(req, res) { this.debug('> %s', req.url); const opt = { diff --git a/lib/TunnelAgent.js b/lib/TunnelAgent.js index f940aa5..efc2231 100644 --- a/lib/TunnelAgent.js +++ b/lib/TunnelAgent.js @@ -39,6 +39,12 @@ class TunnelAgent extends Agent { this.closed = false; } + stats() { + return { + connectedSockets: this.connectedSockets, + }; + } + listen() { const server = this.server; if (this.started) { diff --git a/lib/TunnelAgent.test.js b/lib/TunnelAgent.test.js index 62839c0..9505f35 100644 --- a/lib/TunnelAgent.test.js +++ b/lib/TunnelAgent.test.js @@ -173,4 +173,11 @@ describe('TunnelAgent', () => { }); assert.equal(err.message, 'foo'); }); + + it('should return stats', async () => { + const agent = new TunnelAgent(); + assert.deepEqual(agent.stats(), { + connectedSockets: 0, + }); + }); }); diff --git a/package.json b/package.json index 047eb25..0693b8d 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,7 @@ "esm": "3.0.34", "human-readable-ids": "1.0.3", "koa": "2.5.1", + "koa-router": "7.4.0", "localenv": "0.2.2", "optimist": "0.6.1", "pump": "3.0.0", diff --git a/server.js b/server.js index fb5124b..5a53c47 100644 --- a/server.js +++ b/server.js @@ -4,6 +4,7 @@ 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'; @@ -25,23 +26,33 @@ export default function(opt) { const schema = opt.secure ? 'https' : 'http'; const app = new Koa(); + const router = new Router(); - // api status endpoint - app.use(async (ctx, next) => { - const path = ctx.request.path; - if (path !== '/api/status') { - await next(); - return; - } - + 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; diff --git a/server.test.js b/server.test.js index 3f72f2d..10bf8bc 100644 --- a/server.test.js +++ b/server.test.js @@ -82,4 +82,28 @@ describe('Server', () => { 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/yarn.lock b/yarn.lock index eba9339..9ad744b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -348,7 +348,7 @@ http-assert@^1.1.0: deep-equal "~1.0.1" http-errors "~1.6.1" -http-errors@^1.2.8, 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: @@ -416,6 +416,10 @@ 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" @@ -453,6 +457,17 @@ 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" @@ -615,7 +630,7 @@ meow@^3.3.0: redent "^1.0.0" trim-newlines "^1.0.0" -methods@^1.1.1, methods@~1.1.2: +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" @@ -782,6 +797,12 @@ 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" @@ -998,6 +1019,10 @@ type-is@^1.5.5: 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" -- 2.54.0 From d9fca62c17c10f24a1fa15d5130e05e61267a44e Mon Sep 17 00:00:00 2001 From: Roman Shtylman Date: Wed, 16 May 2018 17:07:58 -0400 Subject: [PATCH 19/20] grace period disconnect should be per-client --- lib/Client.js | 45 ++++++++++++++++++++++++++++++++++++--- lib/Client.test.js | 1 - lib/ClientManager.js | 35 +++++++++--------------------- lib/ClientManager.test.js | 33 ++++++++++++++++++++++++++++ 4 files changed, 85 insertions(+), 29 deletions(-) diff --git a/lib/Client.js b/lib/Client.js index 16b522e..369eb07 100644 --- a/lib/Client.js +++ b/lib/Client.js @@ -1,21 +1,60 @@ 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 { +class Client extends EventEmitter { constructor(options) { - this.agent = options.agent; - this.debug = Debug('lt:Client'); + 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 = { diff --git a/lib/Client.test.js b/lib/Client.test.js index 8f53be1..5a0afd0 100644 --- a/lib/Client.test.js +++ b/lib/Client.test.js @@ -1,7 +1,6 @@ import assert from 'assert'; import http from 'http'; import { Duplex } from 'stream'; -import EventEmitter from 'events'; import WebSocket from 'ws'; import net from 'net'; diff --git a/lib/ClientManager.js b/lib/ClientManager.js index 6279982..e1a7838 100644 --- a/lib/ClientManager.js +++ b/lib/ClientManager.js @@ -21,6 +21,7 @@ class ClientManager { this.debug = Debug('lt:ClientManager'); + // This is totally wrong :facepalm: this needs to be per-client... this.graceTimeout = null; } @@ -42,35 +43,19 @@ class ClientManager { maxSockets: 10, }); - agent.on('online', () => { - this.debug('client online %s', id); - clearTimeout(this.graceTimeout); + const client = new Client({ + id, + agent, }); - agent.on('offline', () => { - // TODO(roman): grace period for re-connecting - // this period is short as the client is expected to maintain connections actively - // if they client does not reconnect on a dropped connection they need to re-establish - this.debug('client offline %s', id); - - // client is given a grace period in which they can re-connect before they are _removed_ - this.graceTimeout = setTimeout(() => { - this.removeClient(id); - }, 1000); - }); - - // 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.removeClient(id); - }); - - const client = new Client({ 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(); @@ -96,11 +81,11 @@ class ClientManager { } --this.stats.tunnels; delete this.clients[id]; - client.agent.destroy(); + client.close(); } hasClient(id) { - return this.clients[id]; + return !!this.clients[id]; } getClient(id) { diff --git a/lib/ClientManager.test.js b/lib/ClientManager.test.js index 3282553..d85abe5 100644 --- a/lib/ClientManager.test.js +++ b/lib/ClientManager.test.js @@ -54,4 +54,37 @@ describe('ClientManager', () => { 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); }); -- 2.54.0 From 78c06f8fc43ce0eacea2aeb88f2d1d63afaf4e19 Mon Sep 17 00:00:00 2001 From: Jo Hyun Woo Date: Wed, 27 Mar 2019 00:56:17 +0900 Subject: [PATCH 20/20] fix typo in server.js (#109) `sock` is not defined in the scope. --- server.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server.js b/server.js index 5a53c47..21a4af6 100644 --- a/server.js +++ b/server.js @@ -145,13 +145,13 @@ export default function(opt) { server.on('upgrade', (req, socket, head) => { const hostname = req.headers.host; if (!hostname) { - sock.destroy(); + socket.destroy(); return; } const clientId = GetClientIdFromHostname(hostname); if (!clientId) { - sock.destroy(); + socket.destroy(); return; } -- 2.54.0