mirror of
https://github.com/bitinflow/localtunnel.git
synced 2026-03-14 14:05:54 +00:00
Compare commits
133 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b7e450177b | ||
|
|
24475fef1b | ||
|
|
2a74d6be9f | ||
|
|
d7330a7121 | ||
|
|
57549598f3 | ||
|
|
e9d92039bb | ||
|
|
be3dc1a4cc | ||
|
|
4d6db16235 | ||
|
|
41270db62f | ||
|
|
42ea8ecfa5 | ||
|
|
5b313bdfaa | ||
|
|
ef78c1978e | ||
|
|
d0fc61ca97 | ||
|
|
a4bf6d16e4 | ||
|
|
9a38b8de0f | ||
|
|
c524872323 | ||
|
|
2372ec22cc | ||
|
|
db22a4efe0 | ||
|
|
610484b9d7 | ||
|
|
86deca52f2 | ||
|
|
cb3441a339 | ||
|
|
32fd1fdcbd | ||
|
|
14cac6f6c8 | ||
|
|
1f33d4992d | ||
|
|
578dc9aaae | ||
|
|
4c136a265c | ||
|
|
627cfe4783 | ||
|
|
dbe0c16024 | ||
|
|
c87bbe82e7 | ||
|
|
c71ba81972 | ||
|
|
8efcb3a294 | ||
|
|
b9c1901d60 | ||
|
|
edc182125f | ||
|
|
81c28d4d68 | ||
|
|
9a1d48764a | ||
|
|
371db2870a | ||
|
|
4ae493ae44 | ||
|
|
f487effe3a | ||
|
|
333af2b08f | ||
|
|
b32041d8aa | ||
|
|
649de1b840 | ||
|
|
f791217756 | ||
|
|
cce9d1490a | ||
|
|
86cd2d3c58 | ||
|
|
d70c743014 | ||
|
|
4940043378 | ||
|
|
b4a22bff64 | ||
|
|
176ec0479d | ||
|
|
fe316de3e0 | ||
|
|
4f97434a69 | ||
|
|
a46cd02fcb | ||
|
|
792d9f19bd | ||
|
|
d0b483b92b | ||
|
|
59d96a3cc6 | ||
|
|
b516ecccfa | ||
|
|
f68b1f06d9 | ||
|
|
4d09875163 | ||
|
|
2773fe6923 | ||
|
|
457bd64ecc | ||
|
|
eb31659345 | ||
|
|
3ee8b1b884 | ||
|
|
15aac729bb | ||
|
|
e73cfe3e45 | ||
|
|
790a642a83 | ||
|
|
e6539e1225 | ||
|
|
4c0a5dc4eb | ||
|
|
809262cf3d | ||
|
|
ddb47d2f90 | ||
|
|
a6845ec63b | ||
|
|
adecf03f41 | ||
|
|
9bdb40e97c | ||
|
|
006ce7733b | ||
|
|
624d279c26 | ||
|
|
c2b8f2b7ab | ||
|
|
4d9dcc1711 | ||
|
|
7b1fef982f | ||
|
|
b1ebef2b0b | ||
|
|
9ad43778b7 | ||
|
|
887c444543 | ||
|
|
828cb2afcb | ||
|
|
8768329fdd | ||
|
|
3026d6a42c | ||
|
|
abd461f83a | ||
|
|
2acea3d77f | ||
|
|
5d0eb3382a | ||
|
|
3b67c8a8ce | ||
|
|
71552a336e | ||
|
|
87a23bf28c | ||
|
|
3d54de851f | ||
|
|
92bb807908 | ||
|
|
afbdc3697e | ||
|
|
0049f21b55 | ||
|
|
509841104b | ||
|
|
92caf2f204 | ||
|
|
9487797e02 | ||
|
|
a42f6a8d8d | ||
|
|
14b4bcb96f | ||
|
|
4aa65002eb | ||
|
|
08676ba81d | ||
|
|
174e7f3982 | ||
|
|
44be55cd7b | ||
|
|
5c6cd2359c | ||
|
|
2f6f9459ad | ||
|
|
7217a08a05 | ||
|
|
fbfc923a7e | ||
|
|
d9bc11b520 | ||
|
|
ad64611bd1 | ||
|
|
ac70515143 | ||
|
|
8d7ccccf21 | ||
|
|
77091b3d93 | ||
|
|
4f4a147b45 | ||
|
|
f1d809a84d | ||
|
|
eba003bd26 | ||
|
|
3354c4c6e3 | ||
|
|
1c2757e604 | ||
|
|
790e55e881 | ||
|
|
a9b0274ff4 | ||
|
|
83ecb29eff | ||
|
|
21df257d16 | ||
|
|
18ada0854a | ||
|
|
34afd6537d | ||
|
|
2c38aefb9d | ||
|
|
aa488f6e76 | ||
|
|
f6618953f9 | ||
|
|
092d050fa0 | ||
|
|
0334ace20b | ||
|
|
13afcff1ae | ||
|
|
ed5aa3f16b | ||
|
|
2fcac1336c | ||
|
|
0568ae0bef | ||
|
|
585a8afad7 | ||
|
|
fbe841a1c5 | ||
|
|
929473913f |
21
.eslintrc.js
Normal file
21
.eslintrc.js
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
module.exports = {
|
||||||
|
parser: 'babel-eslint',
|
||||||
|
extends: ['airbnb', 'prettier', 'plugin:jest/recommended'],
|
||||||
|
plugins: ['prettier', 'jest'],
|
||||||
|
env: {
|
||||||
|
'jest/globals': true,
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
'prettier/prettier': [
|
||||||
|
'warn',
|
||||||
|
{
|
||||||
|
printWidth: 100,
|
||||||
|
tabWidth: 2,
|
||||||
|
bracketSpacing: true,
|
||||||
|
trailingComma: 'es5',
|
||||||
|
singleQuote: true,
|
||||||
|
jsxBracketSameLine: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}
|
||||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -1 +1 @@
|
|||||||
node_modules
|
/node_modules
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
dist: xenial
|
||||||
language: node_js
|
language: node_js
|
||||||
node_js:
|
node_js:
|
||||||
- 0.8
|
- "8"
|
||||||
|
- "10"
|
||||||
|
- "12"
|
||||||
|
|||||||
73
CHANGELOG.md
Normal file
73
CHANGELOG.md
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
# 2.0.0 (2019-09-16)
|
||||||
|
|
||||||
|
- Add support for tunneling a local HTTPS server
|
||||||
|
- Add support for localtunnel server with IP-based tunnel URLs
|
||||||
|
- Node.js client API is now Promise-based, with backwards compatibility to callback
|
||||||
|
- Major refactor of entire codebase using modern ES syntax (requires Node.js v8.3.0 or above)
|
||||||
|
|
||||||
|
# 1.9.2 (2019-06-01)
|
||||||
|
|
||||||
|
- Update debug to 4.1.1
|
||||||
|
- Update axios to 0.19.0
|
||||||
|
|
||||||
|
# 1.9.1 (2018-09-08)
|
||||||
|
|
||||||
|
- Update debug to 2.6.9
|
||||||
|
|
||||||
|
# 1.9.0 (2018-04-03)
|
||||||
|
|
||||||
|
- Add _request_ event to Tunnel emitter
|
||||||
|
- Update yargs to support config via environment variables
|
||||||
|
- Add basic request logging when --print-requests argument is used
|
||||||
|
|
||||||
|
# 1.8.3 (2017-06-11)
|
||||||
|
|
||||||
|
- update request dependency
|
||||||
|
- update debug dependency
|
||||||
|
- update openurl dependency
|
||||||
|
|
||||||
|
# 1.8.2 (2016-11-17)
|
||||||
|
|
||||||
|
- fix host header transform
|
||||||
|
- update request dependency
|
||||||
|
|
||||||
|
# 1.8.1 (2016-01-20)
|
||||||
|
|
||||||
|
- fix bug w/ HostHeaderTransformer and binary data
|
||||||
|
|
||||||
|
# 1.8.0 (2015-11-04)
|
||||||
|
|
||||||
|
- pass socket errors up to top level
|
||||||
|
|
||||||
|
# 1.7.0 (2015-07-22)
|
||||||
|
|
||||||
|
- add short arg options
|
||||||
|
|
||||||
|
# 1.6.0 (2015-05-15)
|
||||||
|
|
||||||
|
- keep sockets alive after connecting
|
||||||
|
- add --open param to CLI
|
||||||
|
|
||||||
|
# 1.5.0 (2014-10-25)
|
||||||
|
|
||||||
|
- capture all errors on remote socket and restart the tunnel
|
||||||
|
|
||||||
|
# 1.4.0 (2014-08-31)
|
||||||
|
|
||||||
|
- don't emit errors for ETIMEDOUT
|
||||||
|
|
||||||
|
# 1.2.0 / 2014-04-28
|
||||||
|
|
||||||
|
- return `client` from `localtunnel` API instantiation
|
||||||
|
|
||||||
|
# 1.1.0 / 2014-02-24
|
||||||
|
|
||||||
|
- add a host header transform to change the 'Host' header in requests
|
||||||
|
|
||||||
|
# 1.0.0 / 2014-02-14
|
||||||
|
|
||||||
|
- default to localltunnel.me for host
|
||||||
|
- remove exported `connect` method (just export one function that does the same thing)
|
||||||
|
- change localtunnel signature to (port, opt, fn)
|
||||||
|
|
||||||
|
# 0.2.2 / 2014-01-09
|
||||||
21
LICENSE
Normal file
21
LICENSE
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2018 Roman Shtylman
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
120
README.md
120
README.md
@@ -1,18 +1,32 @@
|
|||||||
# localtunnel [](http://travis-ci.org/shtylman/localtunnel) #
|
# localtunnel
|
||||||
|
|
||||||
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.
|
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.
|
||||||
|
|
||||||
Great for working with browser testing tools like browserling or external api callback services like twilio which require a public url for callbacks.
|
Great for working with browser testing tools like browserling or external api callback services like twilio which require a public url for callbacks.
|
||||||
|
|
||||||
## installation ##
|
## Quickstart
|
||||||
|
|
||||||
|
```
|
||||||
|
npx localtunnel --port 8000
|
||||||
|
```
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
### Globally
|
||||||
|
|
||||||
```
|
```
|
||||||
npm install -g localtunnel
|
npm install -g localtunnel
|
||||||
```
|
```
|
||||||
|
|
||||||
## use ##
|
### As a dependency in your project
|
||||||
|
|
||||||
Super Easy! Assuming your local server is running on port 8000, just use the ```lt``` command to start the tunnel.
|
```
|
||||||
|
yarn add localtunnel
|
||||||
|
```
|
||||||
|
|
||||||
|
## CLI usage
|
||||||
|
|
||||||
|
When localtunnel is installed globally, just use the `lt` command to start the tunnel.
|
||||||
|
|
||||||
```
|
```
|
||||||
lt --port 8000
|
lt --port 8000
|
||||||
@@ -20,29 +34,87 @@ lt --port 8000
|
|||||||
|
|
||||||
Thats it! It will connect to the tunnel server, setup the tunnel, and tell you what url to use for your testing. This url will remain active for the duration of your session; so feel free to share it with others for happy fun time!
|
Thats it! It will connect to the tunnel server, setup the tunnel, and tell you what url to use for your testing. This url will remain active for the duration of your session; so feel free to share it with others for happy fun time!
|
||||||
|
|
||||||
You can restart your local server all you want, ```lt``` is smart enough to detect this and reconnect once it is back.
|
You can restart your local server all you want, `lt` is smart enough to detect this and reconnect once it is back.
|
||||||
|
|
||||||
## API ##
|
### Arguments
|
||||||
|
|
||||||
The localtunnel client is also usable through an API (test integration, automation, etc)
|
Below are some common arguments. See `lt --help` for additional arguments
|
||||||
|
|
||||||
```javascript
|
- `--subdomain` request a named subdomain on the localtunnel server (default is random characters)
|
||||||
var lt_client = require('localtunnel').client;
|
- `--local-host` proxy to a hostname other than localhost
|
||||||
|
|
||||||
var client = lt_client.connect({
|
You may also specify arguments via env variables. E.x.
|
||||||
// the localtunnel server
|
|
||||||
host: 'http://localtunnel.me',
|
|
||||||
// your local application port
|
|
||||||
port: 12345
|
|
||||||
});
|
|
||||||
|
|
||||||
// when your are assigned a url
|
|
||||||
client.on('url', function(url) {
|
|
||||||
// you can now make http requests to the url
|
|
||||||
// they will be proxied to your local server on port [12345]
|
|
||||||
});
|
|
||||||
|
|
||||||
client.on('error', function(err) {
|
|
||||||
// uh oh!
|
|
||||||
});
|
|
||||||
```
|
```
|
||||||
|
PORT=3000 lt
|
||||||
|
```
|
||||||
|
|
||||||
|
## API
|
||||||
|
|
||||||
|
The localtunnel client is also usable through an API (for test integration, automation, etc)
|
||||||
|
|
||||||
|
### localtunnel(port [,options][,callback])
|
||||||
|
|
||||||
|
Creates a new localtunnel to the specified local `port`. Will return a Promise that resolves once you have been assigned a public localtunnel url. `options` can be used to request a specific `subdomain`. A `callback` function can be passed, in which case it won't return a Promise. This exists for backwards compatibility with the old Node-style callback API. You may also pass a single options object with `port` as a property.
|
||||||
|
|
||||||
|
```js
|
||||||
|
const localtunnel = require('localtunnel');
|
||||||
|
|
||||||
|
(async () => {
|
||||||
|
const tunnel = await localtunnel({ port: 3000 });
|
||||||
|
|
||||||
|
// the assigned public url for your tunnel
|
||||||
|
// i.e. https://abcdefgjhij.localtunnel.me
|
||||||
|
tunnel.url;
|
||||||
|
|
||||||
|
tunnel.on('close', () => {
|
||||||
|
// tunnels are closed
|
||||||
|
});
|
||||||
|
})();
|
||||||
|
```
|
||||||
|
|
||||||
|
#### options
|
||||||
|
|
||||||
|
- `port` (number) [required] The local port number to expose through localtunnel.
|
||||||
|
- `subdomain` (string) Request a specific subdomain on the proxy server. **Note** You may not actually receive this name depending on availability.
|
||||||
|
- `host` (string) URL for the upstream proxy server. Defaults to `https://localtunnel.me`.
|
||||||
|
- `local_host` (string) Proxy to this hostname instead of `localhost`. This will also cause the `Host` header to be re-written to this value in proxied requests.
|
||||||
|
- `local_https` (boolean) Enable tunneling to local HTTPS server.
|
||||||
|
- `local_cert` (string) Path to certificate PEM file for local HTTPS server.
|
||||||
|
- `local_key` (string) Path to certificate key file for local HTTPS server.
|
||||||
|
- `local_ca` (string) Path to certificate authority file for self-signed certificates.
|
||||||
|
- `allow_invalid_cert` (boolean) Disable certificate checks for your local HTTPS server (ignore cert/key/ca options).
|
||||||
|
|
||||||
|
Refer to [tls.createSecureContext](https://nodejs.org/api/tls.html#tls_tls_createsecurecontext_options) for details on the certificate options.
|
||||||
|
|
||||||
|
### Tunnel
|
||||||
|
|
||||||
|
The `tunnel` instance returned to your callback emits the following events
|
||||||
|
|
||||||
|
| event | args | description |
|
||||||
|
| ------- | ---- | ------------------------------------------------------------------------------------ |
|
||||||
|
| request | info | fires when a request is processed by the tunnel, contains _method_ and _path_ fields |
|
||||||
|
| error | err | fires when an error happens on the tunnel |
|
||||||
|
| close | | fires when the tunnel has closed |
|
||||||
|
|
||||||
|
The `tunnel` instance has the following methods
|
||||||
|
|
||||||
|
| method | args | description |
|
||||||
|
| ------ | ---- | ---------------- |
|
||||||
|
| close | | close the tunnel |
|
||||||
|
|
||||||
|
## other clients
|
||||||
|
|
||||||
|
Clients in other languages
|
||||||
|
|
||||||
|
_go_ [gotunnelme](https://github.com/NoahShen/gotunnelme)
|
||||||
|
|
||||||
|
_go_ [go-localtunnel](https://github.com/localtunnel/go-localtunnel)
|
||||||
|
|
||||||
|
## server
|
||||||
|
|
||||||
|
See [localtunnel/server](//github.com/localtunnel/server) for details on the server that powers localtunnel.
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
MIT
|
||||||
|
|||||||
34
bin/client
34
bin/client
@@ -1,34 +0,0 @@
|
|||||||
#!/usr/bin/env node
|
|
||||||
var lt_client = require(__dirname + '/../client');
|
|
||||||
|
|
||||||
var argv = require('optimist')
|
|
||||||
.usage('Usage: $0 --port [num]')
|
|
||||||
.demand(['port'])
|
|
||||||
.options('host', {
|
|
||||||
default: 'http://localtunnel.me',
|
|
||||||
describe: 'upstream server providing forwarding'
|
|
||||||
})
|
|
||||||
.options('subdomain', {
|
|
||||||
describe: 'request this subdomain'
|
|
||||||
})
|
|
||||||
.describe('port', 'internal http server port')
|
|
||||||
.argv;
|
|
||||||
|
|
||||||
var opt = {
|
|
||||||
host: argv.host,
|
|
||||||
port: argv.port,
|
|
||||||
subdomain: argv.subdomain,
|
|
||||||
}
|
|
||||||
|
|
||||||
var client = lt_client.connect(opt);
|
|
||||||
|
|
||||||
// only emitted when the url changes
|
|
||||||
client.on('url', function(url) {
|
|
||||||
console.log('your url is: %s', url);
|
|
||||||
});
|
|
||||||
|
|
||||||
client.on('error', function(err) {
|
|
||||||
console.error(err);
|
|
||||||
});
|
|
||||||
|
|
||||||
// vim: ft=javascript
|
|
||||||
104
bin/lt.js
Executable file
104
bin/lt.js
Executable file
@@ -0,0 +1,104 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
/* eslint-disable no-console */
|
||||||
|
|
||||||
|
const openurl = require('openurl');
|
||||||
|
const yargs = require('yargs');
|
||||||
|
|
||||||
|
const localtunnel = require('../localtunnel');
|
||||||
|
const { version } = require('../package');
|
||||||
|
|
||||||
|
const { argv } = yargs
|
||||||
|
.usage('Usage: lt --port [num] <options>')
|
||||||
|
.env(true)
|
||||||
|
.option('p', {
|
||||||
|
alias: 'port',
|
||||||
|
describe: 'Internal HTTP server port',
|
||||||
|
})
|
||||||
|
.option('h', {
|
||||||
|
alias: 'host',
|
||||||
|
describe: 'Upstream server providing forwarding',
|
||||||
|
default: 'https://localtunnel.me',
|
||||||
|
})
|
||||||
|
.option('s', {
|
||||||
|
alias: 'subdomain',
|
||||||
|
describe: 'Request this subdomain',
|
||||||
|
})
|
||||||
|
.option('l', {
|
||||||
|
alias: 'local-host',
|
||||||
|
describe: 'Tunnel traffic to this host instead of localhost, override Host header to this host',
|
||||||
|
})
|
||||||
|
.option('local-https', {
|
||||||
|
describe: 'Tunnel traffic to a local HTTPS server',
|
||||||
|
})
|
||||||
|
.option('local-cert', {
|
||||||
|
describe: 'Path to certificate PEM file for local HTTPS server',
|
||||||
|
})
|
||||||
|
.option('local-key', {
|
||||||
|
describe: 'Path to certificate key file for local HTTPS server',
|
||||||
|
})
|
||||||
|
.option('local-ca', {
|
||||||
|
describe: 'Path to certificate authority file for self-signed certificates',
|
||||||
|
})
|
||||||
|
.option('allow-invalid-cert', {
|
||||||
|
describe: 'Disable certificate checks for your local HTTPS server (ignore cert/key/ca options)',
|
||||||
|
})
|
||||||
|
.options('o', {
|
||||||
|
alias: 'open',
|
||||||
|
describe: 'Opens the tunnel URL in your browser',
|
||||||
|
})
|
||||||
|
.option('print-requests', {
|
||||||
|
describe: 'Print basic request info',
|
||||||
|
})
|
||||||
|
.require('port')
|
||||||
|
.boolean('local-https')
|
||||||
|
.boolean('allow-invalid-cert')
|
||||||
|
.boolean('print-requests')
|
||||||
|
.help('help', 'Show this help and exit')
|
||||||
|
.version(version);
|
||||||
|
|
||||||
|
if (typeof argv.port !== 'number') {
|
||||||
|
yargs.showHelp();
|
||||||
|
console.error('\nInvalid argument: `port` must be a number');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
(async () => {
|
||||||
|
const tunnel = await localtunnel({
|
||||||
|
port: argv.port,
|
||||||
|
host: argv.host,
|
||||||
|
subdomain: argv.subdomain,
|
||||||
|
local_host: argv.localHost,
|
||||||
|
local_https: argv.localHttps,
|
||||||
|
local_cert: argv.localCert,
|
||||||
|
local_key: argv.localKey,
|
||||||
|
local_ca: argv.localCa,
|
||||||
|
allow_invalid_cert: argv.allowInvalidCert,
|
||||||
|
}).catch(err => {
|
||||||
|
throw err;
|
||||||
|
});
|
||||||
|
|
||||||
|
tunnel.on('error', err => {
|
||||||
|
throw err;
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('your url is: %s', tunnel.url);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* `cachedUrl` is set when using a proxy server that support resource caching.
|
||||||
|
* This URL generally remains available after the tunnel itself has closed.
|
||||||
|
* @see https://github.com/localtunnel/localtunnel/pull/319#discussion_r319846289
|
||||||
|
*/
|
||||||
|
if (tunnel.cachedUrl) {
|
||||||
|
console.log('your cachedUrl is: %s', tunnel.cachedUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (argv.open) {
|
||||||
|
openurl.open(tunnel.url);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (argv['print-requests']) {
|
||||||
|
tunnel.on('request', info => {
|
||||||
|
console.log(new Date().toString(), info.method, info.path);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})();
|
||||||
35
bin/server
35
bin/server
@@ -1,35 +0,0 @@
|
|||||||
#!/usr/bin/env node
|
|
||||||
|
|
||||||
// vendor
|
|
||||||
var log = require('book');
|
|
||||||
var optimist = require('optimist');
|
|
||||||
|
|
||||||
var argv = optimist
|
|
||||||
.usage('Usage: $0 --port [num]')
|
|
||||||
.options('port', {
|
|
||||||
default: '80',
|
|
||||||
describe: 'listen on this port for outside requests'
|
|
||||||
})
|
|
||||||
.argv;
|
|
||||||
|
|
||||||
if (argv.help) {
|
|
||||||
optimist.showHelp();
|
|
||||||
process.exit();
|
|
||||||
}
|
|
||||||
|
|
||||||
process.once('uncaughtException', function(err) {
|
|
||||||
log.panic(err);
|
|
||||||
process.exit(-1);
|
|
||||||
return;
|
|
||||||
});
|
|
||||||
|
|
||||||
var server = require('../server')({
|
|
||||||
max_tcp_sockets: 5
|
|
||||||
});
|
|
||||||
|
|
||||||
server.listen(argv.port, function() {
|
|
||||||
log.info('server listening on port: %d', server.address().port);
|
|
||||||
});
|
|
||||||
|
|
||||||
// vim: ft=javascript
|
|
||||||
|
|
||||||
142
client.js
142
client.js
@@ -1,142 +0,0 @@
|
|||||||
// builtin
|
|
||||||
var net = require('net');
|
|
||||||
var url = require('url');
|
|
||||||
var request = require('request');
|
|
||||||
var EventEmitter = require('events').EventEmitter;
|
|
||||||
|
|
||||||
// request upstream url and connection info
|
|
||||||
var request_url = function(params, cb) {
|
|
||||||
request(params, function(err, res, body) {
|
|
||||||
if (err) {
|
|
||||||
cb(err);
|
|
||||||
}
|
|
||||||
|
|
||||||
cb(null, body);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
var connect = function(opt) {
|
|
||||||
var ev = new EventEmitter();
|
|
||||||
|
|
||||||
// local port
|
|
||||||
var local_port = opt.port;
|
|
||||||
|
|
||||||
var base_uri = opt.host + '/';
|
|
||||||
|
|
||||||
// optionally override the upstream server
|
|
||||||
var upstream = url.parse(opt.host);
|
|
||||||
|
|
||||||
// no subdomain at first, maybe use requested domain
|
|
||||||
var assigned_domain = opt.subdomain;
|
|
||||||
|
|
||||||
// connect to upstream given connection parameters
|
|
||||||
var tunnel = function (remote_host, remote_port) {
|
|
||||||
|
|
||||||
var remote_opt = {
|
|
||||||
host: remote_host,
|
|
||||||
port: remote_port
|
|
||||||
};
|
|
||||||
|
|
||||||
var local_opt = {
|
|
||||||
host: 'localhost',
|
|
||||||
port: local_port
|
|
||||||
};
|
|
||||||
|
|
||||||
var remote_attempts = 0;
|
|
||||||
|
|
||||||
(function conn(conn_had_error) {
|
|
||||||
if (conn_had_error) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (++remote_attempts >= 3) {
|
|
||||||
console.error('localtunnel server offline - try again');
|
|
||||||
process.exit(-1);
|
|
||||||
}
|
|
||||||
|
|
||||||
// connection to localtunnel server
|
|
||||||
var remote = net.connect(remote_opt);
|
|
||||||
|
|
||||||
remote.once('error', function(err) {
|
|
||||||
if (err.code !== 'ECONNREFUSED') {
|
|
||||||
local.emit('error', err);
|
|
||||||
}
|
|
||||||
|
|
||||||
// retrying connection to local server
|
|
||||||
setTimeout(conn, 1000);
|
|
||||||
});
|
|
||||||
|
|
||||||
function recon_local() {
|
|
||||||
remote.pause();
|
|
||||||
remote_attempts = 0;
|
|
||||||
|
|
||||||
// connection to local http server
|
|
||||||
var local = net.connect(local_opt);
|
|
||||||
|
|
||||||
local.once('error', function(err) {
|
|
||||||
if (err.code !== 'ECONNREFUSED') {
|
|
||||||
local.emit('error', err);
|
|
||||||
}
|
|
||||||
|
|
||||||
// retrying connection to local server
|
|
||||||
setTimeout(recon_local, 1000);
|
|
||||||
});
|
|
||||||
|
|
||||||
local.once('connect', function() {
|
|
||||||
remote.resume();
|
|
||||||
remote.pipe(local).pipe(remote, {end: false});
|
|
||||||
});
|
|
||||||
|
|
||||||
local.once('close', function(had_error) {
|
|
||||||
if (had_error) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
recon_local();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
remote.once('close', conn);
|
|
||||||
remote.once('connect', recon_local);
|
|
||||||
})();
|
|
||||||
};
|
|
||||||
|
|
||||||
var params = {
|
|
||||||
path: '/',
|
|
||||||
json: true
|
|
||||||
};
|
|
||||||
|
|
||||||
// where to quest
|
|
||||||
params.uri = base_uri + ((assigned_domain) ? assigned_domain : '?new');
|
|
||||||
|
|
||||||
// get an id from lt server and setup forwarding tcp connections
|
|
||||||
request_url(params, function(err, body) {
|
|
||||||
|
|
||||||
if (err) {
|
|
||||||
ev.emit('error', new Error('tunnel server not available: %s, retry 1s', err.message));
|
|
||||||
|
|
||||||
// retry interval for id request
|
|
||||||
return setTimeout(function() {
|
|
||||||
connect_proxy(opt);
|
|
||||||
}, 1000);
|
|
||||||
}
|
|
||||||
|
|
||||||
// our assigned hostname and tcp port
|
|
||||||
var port = body.port;
|
|
||||||
var host = upstream.hostname;
|
|
||||||
|
|
||||||
// store the id so we can try to get the same one
|
|
||||||
assigned_domain = body.id;
|
|
||||||
|
|
||||||
var max_conn = body.max_conn_count || 1;
|
|
||||||
for (var count = 0 ; count < max_conn ; ++count) {
|
|
||||||
tunnel(host, port);
|
|
||||||
}
|
|
||||||
|
|
||||||
ev.emit('url', body.url);
|
|
||||||
});
|
|
||||||
|
|
||||||
return ev;
|
|
||||||
};
|
|
||||||
|
|
||||||
module.exports.connect = connect;
|
|
||||||
|
|
||||||
2
index.js
2
index.js
@@ -1,2 +0,0 @@
|
|||||||
module.exports.client = require('./client');
|
|
||||||
module.exports.server = require('./server');
|
|
||||||
23
lib/HeaderHostTransformer.js
Normal file
23
lib/HeaderHostTransformer.js
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
const { Transform } = require('stream');
|
||||||
|
|
||||||
|
class HeaderHostTransformer extends Transform {
|
||||||
|
constructor(opts = {}) {
|
||||||
|
super(opts);
|
||||||
|
this.host = opts.host || 'localhost';
|
||||||
|
this.replaced = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
_transform(data, encoding, callback) {
|
||||||
|
callback(
|
||||||
|
null,
|
||||||
|
this.replaced // after replacing the first instance of the Host header we just become a regular passthrough
|
||||||
|
? data
|
||||||
|
: data.toString().replace(/(\r\n[Hh]ost: )\S+/, (match, $1) => {
|
||||||
|
this.replaced = true;
|
||||||
|
return $1 + this.host;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = HeaderHostTransformer;
|
||||||
163
lib/Tunnel.js
Normal file
163
lib/Tunnel.js
Normal file
@@ -0,0 +1,163 @@
|
|||||||
|
/* eslint-disable consistent-return, no-underscore-dangle */
|
||||||
|
|
||||||
|
const { parse } = require('url');
|
||||||
|
const { EventEmitter } = require('events');
|
||||||
|
const axios = require('axios');
|
||||||
|
const debug = require('debug')('localtunnel:client');
|
||||||
|
|
||||||
|
const TunnelCluster = require('./TunnelCluster');
|
||||||
|
|
||||||
|
module.exports = class Tunnel extends EventEmitter {
|
||||||
|
constructor(opts = {}) {
|
||||||
|
super(opts);
|
||||||
|
this.opts = opts;
|
||||||
|
this.closed = false;
|
||||||
|
if (!this.opts.host) {
|
||||||
|
this.opts.host = 'https://localtunnel.me';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_getInfo(body) {
|
||||||
|
/* eslint-disable camelcase */
|
||||||
|
const { id, ip, port, url, cached_url, max_conn_count } = body;
|
||||||
|
const { host, port: local_port, local_host } = this.opts;
|
||||||
|
const { local_https, local_cert, local_key, local_ca, allow_invalid_cert } = this.opts;
|
||||||
|
return {
|
||||||
|
name: id,
|
||||||
|
url,
|
||||||
|
cached_url,
|
||||||
|
max_conn: max_conn_count || 1,
|
||||||
|
remote_host: parse(host).hostname,
|
||||||
|
remote_ip: ip,
|
||||||
|
remote_port: port,
|
||||||
|
local_port,
|
||||||
|
local_host,
|
||||||
|
local_https,
|
||||||
|
local_cert,
|
||||||
|
local_key,
|
||||||
|
local_ca,
|
||||||
|
allow_invalid_cert,
|
||||||
|
};
|
||||||
|
/* eslint-enable camelcase */
|
||||||
|
}
|
||||||
|
|
||||||
|
// initialize connection
|
||||||
|
// callback with connection info
|
||||||
|
_init(cb) {
|
||||||
|
const opt = this.opts;
|
||||||
|
const getInfo = this._getInfo.bind(this);
|
||||||
|
|
||||||
|
const params = {
|
||||||
|
responseType: 'json',
|
||||||
|
};
|
||||||
|
|
||||||
|
const baseUri = `${opt.host}/`;
|
||||||
|
// no subdomain at first, maybe use requested domain
|
||||||
|
const assignedDomain = opt.subdomain;
|
||||||
|
// where to quest
|
||||||
|
const uri = baseUri + (assignedDomain || '?new');
|
||||||
|
|
||||||
|
(function getUrl() {
|
||||||
|
axios
|
||||||
|
.get(uri, params)
|
||||||
|
.then(res => {
|
||||||
|
const body = res.data;
|
||||||
|
debug('got tunnel information', res.data);
|
||||||
|
if (res.status !== 200) {
|
||||||
|
const err = new Error(
|
||||||
|
(body && body.message) || 'localtunnel server returned an error, please try again'
|
||||||
|
);
|
||||||
|
return cb(err);
|
||||||
|
}
|
||||||
|
cb(null, getInfo(body));
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
debug(`tunnel server offline: ${err.message}, retry 1s`);
|
||||||
|
return setTimeout(getUrl, 1000);
|
||||||
|
});
|
||||||
|
})();
|
||||||
|
}
|
||||||
|
|
||||||
|
_establish(info) {
|
||||||
|
// increase max event listeners so that localtunnel consumers don't get
|
||||||
|
// warning messages as soon as they setup even one listener. See #71
|
||||||
|
this.setMaxListeners(info.max_conn + (EventEmitter.defaultMaxListeners || 10));
|
||||||
|
|
||||||
|
this.tunnelCluster = new TunnelCluster(info);
|
||||||
|
|
||||||
|
// only emit the url the first time
|
||||||
|
this.tunnelCluster.once('open', () => {
|
||||||
|
this.emit('url', info.url);
|
||||||
|
});
|
||||||
|
|
||||||
|
// re-emit socket error
|
||||||
|
this.tunnelCluster.on('error', err => {
|
||||||
|
debug('got socket error', err.message);
|
||||||
|
this.emit('error', err);
|
||||||
|
});
|
||||||
|
|
||||||
|
let tunnelCount = 0;
|
||||||
|
|
||||||
|
// track open count
|
||||||
|
this.tunnelCluster.on('open', tunnel => {
|
||||||
|
tunnelCount++;
|
||||||
|
debug('tunnel open [total: %d]', tunnelCount);
|
||||||
|
|
||||||
|
const closeHandler = () => {
|
||||||
|
tunnel.destroy();
|
||||||
|
};
|
||||||
|
|
||||||
|
if (this.closed) {
|
||||||
|
return closeHandler();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.once('close', closeHandler);
|
||||||
|
tunnel.once('close', () => {
|
||||||
|
this.removeListener('close', closeHandler);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// when a tunnel dies, open a new one
|
||||||
|
this.tunnelCluster.on('dead', () => {
|
||||||
|
tunnelCount--;
|
||||||
|
debug('tunnel dead [total: %d]', tunnelCount);
|
||||||
|
if (this.closed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.tunnelCluster.open();
|
||||||
|
});
|
||||||
|
|
||||||
|
this.tunnelCluster.on('request', req => {
|
||||||
|
this.emit('request', req);
|
||||||
|
});
|
||||||
|
|
||||||
|
// establish as many tunnels as allowed
|
||||||
|
for (let count = 0; count < info.max_conn; ++count) {
|
||||||
|
this.tunnelCluster.open();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
open(cb) {
|
||||||
|
this._init((err, info) => {
|
||||||
|
if (err) {
|
||||||
|
return cb(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.clientId = info.name;
|
||||||
|
this.url = info.url;
|
||||||
|
|
||||||
|
// `cached_url` is only returned by proxy servers that support resource caching.
|
||||||
|
if (info.cached_url) {
|
||||||
|
this.cachedUrl = info.cached_url;
|
||||||
|
}
|
||||||
|
|
||||||
|
this._establish(info);
|
||||||
|
cb();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
close() {
|
||||||
|
this.closed = true;
|
||||||
|
this.emit('close');
|
||||||
|
}
|
||||||
|
};
|
||||||
152
lib/TunnelCluster.js
Normal file
152
lib/TunnelCluster.js
Normal file
@@ -0,0 +1,152 @@
|
|||||||
|
const { EventEmitter } = require('events');
|
||||||
|
const debug = require('debug')('localtunnel:client');
|
||||||
|
const fs = require('fs');
|
||||||
|
const net = require('net');
|
||||||
|
const tls = require('tls');
|
||||||
|
|
||||||
|
const HeaderHostTransformer = require('./HeaderHostTransformer');
|
||||||
|
|
||||||
|
// manages groups of tunnels
|
||||||
|
module.exports = class TunnelCluster extends EventEmitter {
|
||||||
|
constructor(opts = {}) {
|
||||||
|
super(opts);
|
||||||
|
this.opts = opts;
|
||||||
|
}
|
||||||
|
|
||||||
|
open() {
|
||||||
|
const opt = this.opts;
|
||||||
|
|
||||||
|
// Prefer IP if returned by the server
|
||||||
|
const remoteHostOrIp = opt.remote_ip || opt.remote_host;
|
||||||
|
const remotePort = opt.remote_port;
|
||||||
|
const localHost = opt.local_host || 'localhost';
|
||||||
|
const localPort = opt.local_port;
|
||||||
|
const localProtocol = opt.local_https ? 'https' : 'http';
|
||||||
|
const allowInvalidCert = opt.allow_invalid_cert;
|
||||||
|
|
||||||
|
debug(
|
||||||
|
'establishing tunnel %s://%s:%s <> %s:%s',
|
||||||
|
localProtocol,
|
||||||
|
localHost,
|
||||||
|
localPort,
|
||||||
|
remoteHostOrIp,
|
||||||
|
remotePort
|
||||||
|
);
|
||||||
|
|
||||||
|
// connection to localtunnel server
|
||||||
|
const remote = net.connect({
|
||||||
|
host: remoteHostOrIp,
|
||||||
|
port: remotePort,
|
||||||
|
});
|
||||||
|
|
||||||
|
remote.setKeepAlive(true);
|
||||||
|
|
||||||
|
remote.on('error', err => {
|
||||||
|
debug('got remote connection error', err.message);
|
||||||
|
|
||||||
|
// emit connection refused errors immediately, because they
|
||||||
|
// indicate that the tunnel can't be established.
|
||||||
|
if (err.code === 'ECONNREFUSED') {
|
||||||
|
this.emit(
|
||||||
|
'error',
|
||||||
|
new Error(
|
||||||
|
`connection refused: ${remoteHostOrIp}:${remotePort} (check your firewall settings)`
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
remote.end();
|
||||||
|
});
|
||||||
|
|
||||||
|
const connLocal = () => {
|
||||||
|
if (remote.destroyed) {
|
||||||
|
debug('remote destroyed');
|
||||||
|
this.emit('dead');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
debug('connecting locally to %s://%s:%d', localProtocol, localHost, localPort);
|
||||||
|
remote.pause();
|
||||||
|
|
||||||
|
if (allowInvalidCert) {
|
||||||
|
debug('allowing invalid certificates');
|
||||||
|
}
|
||||||
|
|
||||||
|
const getLocalCertOpts = () =>
|
||||||
|
allowInvalidCert
|
||||||
|
? { rejectUnauthorized: false }
|
||||||
|
: {
|
||||||
|
cert: fs.readFileSync(opt.local_cert),
|
||||||
|
key: fs.readFileSync(opt.local_key),
|
||||||
|
ca: opt.local_ca ? [fs.readFileSync(opt.local_ca)] : undefined,
|
||||||
|
};
|
||||||
|
|
||||||
|
// connection to local http server
|
||||||
|
const local = opt.local_https
|
||||||
|
? tls.connect({ host: localHost, port: localPort, ...getLocalCertOpts() })
|
||||||
|
: net.connect({ host: localHost, port: localPort });
|
||||||
|
|
||||||
|
const remoteClose = () => {
|
||||||
|
debug('remote close');
|
||||||
|
this.emit('dead');
|
||||||
|
local.end();
|
||||||
|
};
|
||||||
|
|
||||||
|
remote.once('close', remoteClose);
|
||||||
|
|
||||||
|
// TODO some languages have single threaded servers which makes opening up
|
||||||
|
// multiple local connections impossible. We need a smarter way to scale
|
||||||
|
// and adjust for such instances to avoid beating on the door of the server
|
||||||
|
local.once('error', err => {
|
||||||
|
debug('local error %s', err.message);
|
||||||
|
local.end();
|
||||||
|
|
||||||
|
remote.removeListener('close', remoteClose);
|
||||||
|
|
||||||
|
if (err.code !== 'ECONNREFUSED') {
|
||||||
|
return remote.end();
|
||||||
|
}
|
||||||
|
|
||||||
|
// retrying connection to local server
|
||||||
|
setTimeout(connLocal, 1000);
|
||||||
|
});
|
||||||
|
|
||||||
|
local.once('connect', () => {
|
||||||
|
debug('connected locally');
|
||||||
|
remote.resume();
|
||||||
|
|
||||||
|
let stream = remote;
|
||||||
|
|
||||||
|
// if user requested specific local host
|
||||||
|
// then we use host header transform to replace the host header
|
||||||
|
if (opt.local_host) {
|
||||||
|
debug('transform Host header to %s', opt.local_host);
|
||||||
|
stream = remote.pipe(new HeaderHostTransformer({ host: opt.local_host }));
|
||||||
|
}
|
||||||
|
|
||||||
|
stream.pipe(local).pipe(remote);
|
||||||
|
|
||||||
|
// when local closes, also get a new remote
|
||||||
|
local.once('close', hadError => {
|
||||||
|
debug('local connection closed [%s]', hadError);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
remote.on('data', data => {
|
||||||
|
const match = data.toString().match(/^(\w+) (\S+)/);
|
||||||
|
if (match) {
|
||||||
|
this.emit('request', {
|
||||||
|
method: match[1],
|
||||||
|
path: match[2],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// tunnel is considered open when remote connects
|
||||||
|
remote.once('connect', () => {
|
||||||
|
this.emit('open', remote);
|
||||||
|
connLocal();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
|
|
||||||
var chars = 'abcdefghiklmnopqrstuvwxyz';
|
|
||||||
module.exports = function rand_id() {
|
|
||||||
var randomstring = '';
|
|
||||||
for (var i=0; i<4; ++i) {
|
|
||||||
var rnum = Math.floor(Math.random() * chars.length);
|
|
||||||
randomstring += chars[rnum];
|
|
||||||
}
|
|
||||||
|
|
||||||
return randomstring;
|
|
||||||
}
|
|
||||||
|
|
||||||
14
localtunnel.js
Normal file
14
localtunnel.js
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
const Tunnel = require('./lib/Tunnel');
|
||||||
|
|
||||||
|
module.exports = function localtunnel(arg1, arg2, arg3) {
|
||||||
|
const options = typeof arg1 === 'object' ? arg1 : { ...arg2, port: arg1 };
|
||||||
|
const callback = typeof arg1 === 'object' ? arg2 : arg3;
|
||||||
|
const client = new Tunnel(options);
|
||||||
|
if (callback) {
|
||||||
|
client.open(err => (err ? callback(err) : callback(null, client)));
|
||||||
|
return client;
|
||||||
|
}
|
||||||
|
return new Promise((resolve, reject) =>
|
||||||
|
client.open(err => (err ? reject(err) : resolve(client)))
|
||||||
|
);
|
||||||
|
};
|
||||||
162
localtunnel.spec.js
Normal file
162
localtunnel.spec.js
Normal file
@@ -0,0 +1,162 @@
|
|||||||
|
/* eslint-disable no-console */
|
||||||
|
|
||||||
|
const crypto = require('crypto');
|
||||||
|
const http = require('http');
|
||||||
|
const https = require('https');
|
||||||
|
const url = require('url');
|
||||||
|
const assert = require('assert');
|
||||||
|
|
||||||
|
const localtunnel = require('./localtunnel');
|
||||||
|
|
||||||
|
let fakePort;
|
||||||
|
|
||||||
|
before(done => {
|
||||||
|
const server = http.createServer();
|
||||||
|
server.on('request', (req, res) => {
|
||||||
|
res.write(req.headers.host);
|
||||||
|
res.end();
|
||||||
|
});
|
||||||
|
server.listen(() => {
|
||||||
|
const { port } = server.address();
|
||||||
|
fakePort = port;
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('query localtunnel server w/ ident', async done => {
|
||||||
|
const tunnel = await localtunnel({ port: fakePort });
|
||||||
|
assert.ok(new RegExp('^https://.*localtunnel.me$').test(tunnel.url));
|
||||||
|
|
||||||
|
const parsed = url.parse(tunnel.url);
|
||||||
|
const opt = {
|
||||||
|
host: parsed.host,
|
||||||
|
port: 443,
|
||||||
|
headers: { host: parsed.hostname },
|
||||||
|
path: '/',
|
||||||
|
};
|
||||||
|
|
||||||
|
const req = https.request(opt, res => {
|
||||||
|
res.setEncoding('utf8');
|
||||||
|
let body = '';
|
||||||
|
|
||||||
|
res.on('data', chunk => {
|
||||||
|
body += chunk;
|
||||||
|
});
|
||||||
|
|
||||||
|
res.on('end', () => {
|
||||||
|
assert(/.*[.]localtunnel[.]me/.test(body), body);
|
||||||
|
tunnel.close();
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
req.end();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('request specific domain', async () => {
|
||||||
|
const subdomain = Math.random()
|
||||||
|
.toString(36)
|
||||||
|
.substr(2);
|
||||||
|
const tunnel = await localtunnel({ port: fakePort, subdomain });
|
||||||
|
assert.ok(new RegExp(`^https://${subdomain}.localtunnel.me$`).test(tunnel.url));
|
||||||
|
tunnel.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('--local-host localhost', () => {
|
||||||
|
test('override Host header with local-host', async done => {
|
||||||
|
const tunnel = await localtunnel({ port: fakePort, local_host: 'localhost' });
|
||||||
|
assert.ok(new RegExp('^https://.*localtunnel.me$').test(tunnel.url));
|
||||||
|
|
||||||
|
const parsed = url.parse(tunnel.url);
|
||||||
|
const opt = {
|
||||||
|
host: parsed.host,
|
||||||
|
port: 443,
|
||||||
|
headers: { host: parsed.hostname },
|
||||||
|
path: '/',
|
||||||
|
};
|
||||||
|
|
||||||
|
const req = https.request(opt, res => {
|
||||||
|
res.setEncoding('utf8');
|
||||||
|
let body = '';
|
||||||
|
|
||||||
|
res.on('data', chunk => {
|
||||||
|
body += chunk;
|
||||||
|
});
|
||||||
|
|
||||||
|
res.on('end', () => {
|
||||||
|
assert.equal(body, 'localhost');
|
||||||
|
tunnel.close();
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
req.end();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('--local-host 127.0.0.1', () => {
|
||||||
|
test('override Host header with local-host', async done => {
|
||||||
|
const tunnel = await localtunnel({ port: fakePort, local_host: '127.0.0.1' });
|
||||||
|
assert.ok(new RegExp('^https://.*localtunnel.me$').test(tunnel.url));
|
||||||
|
|
||||||
|
const parsed = url.parse(tunnel.url);
|
||||||
|
const opt = {
|
||||||
|
host: parsed.host,
|
||||||
|
port: 443,
|
||||||
|
headers: {
|
||||||
|
host: parsed.hostname,
|
||||||
|
},
|
||||||
|
path: '/',
|
||||||
|
};
|
||||||
|
|
||||||
|
const req = https.request(opt, res => {
|
||||||
|
res.setEncoding('utf8');
|
||||||
|
let body = '';
|
||||||
|
|
||||||
|
res.on('data', chunk => {
|
||||||
|
body += chunk;
|
||||||
|
});
|
||||||
|
|
||||||
|
res.on('end', () => {
|
||||||
|
assert.equal(body, '127.0.0.1');
|
||||||
|
tunnel.close();
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
req.end();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('send chunked request', async done => {
|
||||||
|
const tunnel = await localtunnel({ port: fakePort, local_host: '127.0.0.1' });
|
||||||
|
assert.ok(new RegExp('^https://.*localtunnel.me$').test(tunnel.url));
|
||||||
|
|
||||||
|
const parsed = url.parse(tunnel.url);
|
||||||
|
const opt = {
|
||||||
|
host: parsed.host,
|
||||||
|
port: 443,
|
||||||
|
headers: {
|
||||||
|
host: parsed.hostname,
|
||||||
|
'Transfer-Encoding': 'chunked',
|
||||||
|
},
|
||||||
|
path: '/',
|
||||||
|
};
|
||||||
|
|
||||||
|
const req = https.request(opt, res => {
|
||||||
|
res.setEncoding('utf8');
|
||||||
|
let body = '';
|
||||||
|
|
||||||
|
res.on('data', chunk => {
|
||||||
|
body += chunk;
|
||||||
|
});
|
||||||
|
|
||||||
|
res.on('end', () => {
|
||||||
|
assert.equal(body, '127.0.0.1');
|
||||||
|
tunnel.close();
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
req.end(crypto.randomBytes(1024 * 8).toString('base64'));
|
||||||
|
});
|
||||||
|
});
|
||||||
46
package.json
46
package.json
@@ -1,31 +1,35 @@
|
|||||||
{
|
{
|
||||||
"author": "Roman Shtylman <shtylman@gmail.com>",
|
|
||||||
"name": "localtunnel",
|
"name": "localtunnel",
|
||||||
"description": "expose localhost to the world",
|
"description": "Expose localhost to the world",
|
||||||
"version": "0.0.4",
|
"version": "2.0.0",
|
||||||
|
"license": "MIT",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "git://github.com/shtylman/localtunnel.git"
|
"url": "git://github.com/localtunnel/localtunnel.git"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"author": "Roman Shtylman <shtylman@gmail.com>",
|
||||||
"request": "2.11.4",
|
"contributors": [
|
||||||
"book": "1.2.0",
|
"Roman Shtylman <shtylman@gmail.com>",
|
||||||
"optimist": "0.3.4",
|
"Gert Hengeveld <gert@hichroma.com>",
|
||||||
"http-raw": "1.1.0"
|
"Tom Coleman <tom@hichroma.com>"
|
||||||
},
|
],
|
||||||
"devDependencies": {
|
"main": "./localtunnel.js",
|
||||||
"mocha": "1.6.0"
|
"bin": {
|
||||||
},
|
"lt": "./bin/lt.js"
|
||||||
"optionalDependencies": {},
|
|
||||||
"engines": {
|
|
||||||
"node": "*"
|
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "mocha --ui qunit -- test",
|
"test": "mocha --ui qunit --reporter list --timeout 60000 -- *.spec.js"
|
||||||
"start": "./bin/server"
|
|
||||||
},
|
},
|
||||||
"bin": {
|
"dependencies": {
|
||||||
"lt": "./bin/client"
|
"axios": "0.19.0",
|
||||||
|
"debug": "4.1.1",
|
||||||
|
"openurl": "1.1.1",
|
||||||
|
"yargs": "13.3.0"
|
||||||
},
|
},
|
||||||
"main": "./index.js"
|
"devDependencies": {
|
||||||
|
"mocha": "~1.17.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8.3.0"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
324
server.js
324
server.js
@@ -1,324 +0,0 @@
|
|||||||
|
|
||||||
// builtin
|
|
||||||
var http = require('http');
|
|
||||||
var net = require('net');
|
|
||||||
var url = require('url');
|
|
||||||
|
|
||||||
// here be dragons
|
|
||||||
var HTTPParser = process.binding('http_parser').HTTPParser;
|
|
||||||
|
|
||||||
// vendor
|
|
||||||
var log = require('book');
|
|
||||||
var createRawServer = require('http-raw');
|
|
||||||
|
|
||||||
// local
|
|
||||||
var rand_id = require('./lib/rand_id');
|
|
||||||
|
|
||||||
// id -> client http server
|
|
||||||
var clients = {};
|
|
||||||
|
|
||||||
// available parsers
|
|
||||||
var parsers = http.parsers;
|
|
||||||
|
|
||||||
// send this request to the appropriate client
|
|
||||||
// in -> incoming request stream
|
|
||||||
function proxy_request(client, req, res, rs, ws) {
|
|
||||||
|
|
||||||
rs = rs || req.createRawStream();
|
|
||||||
ws = ws || res.createRawStream();
|
|
||||||
|
|
||||||
// socket is a tcp connection back to the user hosting the site
|
|
||||||
var sock = client.sockets.shift();
|
|
||||||
|
|
||||||
// queue request
|
|
||||||
if (!sock) {
|
|
||||||
log.info('no more clients, queued: %s', req.url);
|
|
||||||
rs.pause();
|
|
||||||
client.waiting.push([req, res, rs, ws]);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
log.info('handle req: %s', req.url);
|
|
||||||
|
|
||||||
// pipe incoming request into tcp socket
|
|
||||||
// incoming request isn't allowed to end the socket back to lt client
|
|
||||||
rs.pipe(sock, { end: false });
|
|
||||||
|
|
||||||
sock.ws = ws;
|
|
||||||
sock.req = req;
|
|
||||||
|
|
||||||
// since tcp connection to upstream are kept open
|
|
||||||
// invoke parsing so we know when the response is complete
|
|
||||||
var parser = sock.parser;
|
|
||||||
parser.reinitialize(HTTPParser.RESPONSE);
|
|
||||||
parser.socket = sock;
|
|
||||||
|
|
||||||
// we have completed a response
|
|
||||||
// the tcp socket is free again
|
|
||||||
parser.onIncoming = function (res) {
|
|
||||||
parser.onMessageComplete = function() {
|
|
||||||
log.info('ended response: %s', req.url);
|
|
||||||
|
|
||||||
// any request we had going on is now done
|
|
||||||
ws.end();
|
|
||||||
|
|
||||||
// no more forwarding
|
|
||||||
delete sock.ws;
|
|
||||||
delete parser.onIncoming;
|
|
||||||
|
|
||||||
// return socket to available pool
|
|
||||||
client.sockets.push(sock);
|
|
||||||
|
|
||||||
var next = client.waiting.shift();
|
|
||||||
if (next) {
|
|
||||||
log.trace('popped');
|
|
||||||
proxy_request(client, next[0], next[1], next[2], next[3]);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
rs.resume();
|
|
||||||
}
|
|
||||||
|
|
||||||
function upstream_response(d, start, end) {
|
|
||||||
var socket = this;
|
|
||||||
|
|
||||||
var ws = socket.ws;
|
|
||||||
if (!ws) {
|
|
||||||
log.warn('no stream set for req:', socket.req.url);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
ws.write(d.slice(start, end));
|
|
||||||
|
|
||||||
if (socket.upgraded) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var ret = socket.parser.execute(d, start, end - start);
|
|
||||||
if (ret instanceof Error) {
|
|
||||||
log.error(ret);
|
|
||||||
parsers.free(parser);
|
|
||||||
socket.destroy(ret);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var handle_req = function (req, res) {
|
|
||||||
|
|
||||||
var max_tcp_sockets = req.socket.server.max_tcp_sockets;
|
|
||||||
|
|
||||||
// ignore favicon
|
|
||||||
if (req.url === '/favicon.ico') {
|
|
||||||
res.writeHead(404);
|
|
||||||
return res.end();
|
|
||||||
}
|
|
||||||
|
|
||||||
var hostname = req.headers.host;
|
|
||||||
if (!hostname) {
|
|
||||||
log.trace('no hostname: %j', req.headers);
|
|
||||||
return res.end();
|
|
||||||
}
|
|
||||||
|
|
||||||
var match = hostname.match(/^([a-z]{4})[.].*/);
|
|
||||||
if (match) {
|
|
||||||
var client_id = match[1];
|
|
||||||
var client = clients[client_id];
|
|
||||||
|
|
||||||
// no such subdomain
|
|
||||||
if (!client) {
|
|
||||||
log.trace('no client found for id: ' + client_id);
|
|
||||||
res.statusCode = 404;
|
|
||||||
return res.end();
|
|
||||||
}
|
|
||||||
|
|
||||||
return proxy_request(client, req, res);
|
|
||||||
}
|
|
||||||
|
|
||||||
var parsed = url.parse(req.url, true);
|
|
||||||
|
|
||||||
// redirect main page to github reference
|
|
||||||
if (req.url === '/' && !parsed.query.new) {
|
|
||||||
res.writeHead(301, { Location: 'http://shtylman.github.com/localtunnel/' });
|
|
||||||
res.end();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// at this point, the client is requesting a new tunnel setup
|
|
||||||
// either generate an id or use the one they requested
|
|
||||||
|
|
||||||
var match = req.url.match(/\/([a-z]{4})?/);
|
|
||||||
|
|
||||||
// user can request a particular set of characters
|
|
||||||
// will be given if not already taken
|
|
||||||
// this is useful when the main server is restarted
|
|
||||||
// users can keep testing with their expected ids
|
|
||||||
var requested_id;
|
|
||||||
if (match && match[1]) {
|
|
||||||
requested_id = match[1];
|
|
||||||
}
|
|
||||||
|
|
||||||
var id = requested_id || rand_id();
|
|
||||||
|
|
||||||
// if the id already exists, this client must use something else
|
|
||||||
if (clients[id]) {
|
|
||||||
id = rand_id();
|
|
||||||
}
|
|
||||||
|
|
||||||
// sockets is a list of available sockets for the connection
|
|
||||||
// waiting is?
|
|
||||||
var client = clients[id] = {
|
|
||||||
sockets: [],
|
|
||||||
waiting: []
|
|
||||||
};
|
|
||||||
|
|
||||||
var client_server = net.createServer();
|
|
||||||
client_server.listen(function() {
|
|
||||||
var port = client_server.address().port;
|
|
||||||
log.info('tcp server listening on port: %d', port);
|
|
||||||
|
|
||||||
var url = 'http://' + id + '.' + req.headers.host;
|
|
||||||
|
|
||||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
||||||
res.end(JSON.stringify({
|
|
||||||
url: url,
|
|
||||||
id: id,
|
|
||||||
port: port,
|
|
||||||
max_conn_count: max_tcp_sockets
|
|
||||||
}));
|
|
||||||
});
|
|
||||||
|
|
||||||
var conn_timeout;
|
|
||||||
|
|
||||||
// user has 5 seconds to connect before their slot is given up
|
|
||||||
function maybe_tcp_close() {
|
|
||||||
conn_timeout = setTimeout(client_server.close.bind(client_server), 5000);
|
|
||||||
}
|
|
||||||
|
|
||||||
maybe_tcp_close();
|
|
||||||
|
|
||||||
// no longer accepting connections for this id
|
|
||||||
client_server.on('close', function() {
|
|
||||||
log.trace('closed tcp socket for client(%s)', id);
|
|
||||||
clearTimeout(conn_timeout);
|
|
||||||
delete clients[id];
|
|
||||||
});
|
|
||||||
|
|
||||||
client_server.on('connection', function(socket) {
|
|
||||||
|
|
||||||
// no more socket connections allowed
|
|
||||||
if (client.sockets.length >= max_tcp_sockets) {
|
|
||||||
return socket.end();
|
|
||||||
}
|
|
||||||
|
|
||||||
log.trace('new connection for id: %s', id);
|
|
||||||
|
|
||||||
// no need to close the client server
|
|
||||||
clearTimeout(conn_timeout);
|
|
||||||
|
|
||||||
// allocate a response parser for the socket
|
|
||||||
// it only needs one since it will reuse it
|
|
||||||
socket.parser = parsers.alloc();
|
|
||||||
|
|
||||||
socket._orig_ondata = socket.ondata;
|
|
||||||
socket.ondata = upstream_response;
|
|
||||||
|
|
||||||
client.sockets.push(socket);
|
|
||||||
|
|
||||||
socket.once('close', function(had_error) {
|
|
||||||
log.trace('client %s closed socket', id);
|
|
||||||
|
|
||||||
// remove this socket
|
|
||||||
var idx = client.sockets.indexOf(socket);
|
|
||||||
client.sockets.splice(idx, 1);
|
|
||||||
|
|
||||||
log.trace('remaining client sockets: %s', client.sockets.length);
|
|
||||||
|
|
||||||
// no more sockets for this ident
|
|
||||||
if (client.sockets.length === 0) {
|
|
||||||
log.trace('all client(%s) sockets disconnected', id);
|
|
||||||
maybe_tcp_close();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// close will be emitted after this
|
|
||||||
socket.on('error', function(err) {
|
|
||||||
log.error(err);
|
|
||||||
socket.end();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
client_server.on('error', function(err) {
|
|
||||||
log.error(err);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
var handle_upgrade = function(req, ws) {
|
|
||||||
|
|
||||||
if (req.headers.connection !== 'Upgrade') {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var hostname = req.headers.host;
|
|
||||||
if (!hostname) {
|
|
||||||
return res.end();
|
|
||||||
}
|
|
||||||
|
|
||||||
var match = hostname.match(/^([a-z]{4})[.].*/);
|
|
||||||
|
|
||||||
// not a valid client
|
|
||||||
if (!match) {
|
|
||||||
return res.end();
|
|
||||||
}
|
|
||||||
|
|
||||||
var client_id = match[1];
|
|
||||||
var client = clients[client_id];
|
|
||||||
|
|
||||||
if (!client) {
|
|
||||||
// no such subdomain
|
|
||||||
return res.end();
|
|
||||||
}
|
|
||||||
|
|
||||||
var socket = client.sockets.shift();
|
|
||||||
if (!socket) {
|
|
||||||
// no available sockets to upgrade to
|
|
||||||
return res.end();
|
|
||||||
}
|
|
||||||
|
|
||||||
var stream = req.createRawStream();
|
|
||||||
|
|
||||||
socket.ws = ws;
|
|
||||||
socket.upgraded = true;
|
|
||||||
|
|
||||||
stream.once('end', function() {
|
|
||||||
delete socket.ws;
|
|
||||||
|
|
||||||
// when this ends, we just reset the socket to the lt client
|
|
||||||
// this is easier than trying to figure anything else out
|
|
||||||
socket.end();
|
|
||||||
|
|
||||||
// put socket back into available pool
|
|
||||||
client.sockets.push(socket);
|
|
||||||
|
|
||||||
var next = client.waiting.shift();
|
|
||||||
if (next) {
|
|
||||||
log.trace('popped');
|
|
||||||
proxy_request(client, next[0], next[1], next[2], next[3]);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
stream.pipe(socket, {end: false});
|
|
||||||
socket.once('end', ws.end.bind(ws));
|
|
||||||
};
|
|
||||||
|
|
||||||
module.exports = function(opt) {
|
|
||||||
opt = opt || {};
|
|
||||||
|
|
||||||
var server = createRawServer();
|
|
||||||
|
|
||||||
server.max_tcp_sockets = opt.max_tcp_sockets || 5;
|
|
||||||
server.on('request', handle_req);
|
|
||||||
server.on('upgrade', handle_upgrade);
|
|
||||||
|
|
||||||
return server;
|
|
||||||
};
|
|
||||||
|
|
||||||
@@ -1,99 +0,0 @@
|
|||||||
var http = require('http');
|
|
||||||
var url = require('url');
|
|
||||||
var assert = require('assert');
|
|
||||||
|
|
||||||
var localtunnel_server = require('../').server();
|
|
||||||
var localtunnel_client = require('../').client;
|
|
||||||
|
|
||||||
test('setup localtunnel server', function(done) {
|
|
||||||
localtunnel_server.listen(3000, function() {
|
|
||||||
console.log('lt server on:', 3000);
|
|
||||||
done();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test('setup 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;
|
|
||||||
console.log('local http on:', port);
|
|
||||||
done();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test('setup localtunnel client', function(done) {
|
|
||||||
var client = localtunnel_client.connect({
|
|
||||||
host: 'http://localhost:' + 3000,
|
|
||||||
port: test._fake_port
|
|
||||||
});
|
|
||||||
|
|
||||||
client.on('url', function(url) {
|
|
||||||
assert.ok(/^http:\/\/.*localhost:3000$/.test(url));
|
|
||||||
test._fake_url = url;
|
|
||||||
done();
|
|
||||||
});
|
|
||||||
|
|
||||||
client.on('error', function(err) {
|
|
||||||
console.error(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: 3000,
|
|
||||||
headers: {
|
|
||||||
host: hostname
|
|
||||||
},
|
|
||||||
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 client = localtunnel_client.connect({
|
|
||||||
host: 'http://localhost:' + 3000,
|
|
||||||
port: test._fake_port,
|
|
||||||
subdomain: 'abcd'
|
|
||||||
});
|
|
||||||
|
|
||||||
client.on('url', function(url) {
|
|
||||||
assert.ok(/^http:\/\/abcd.localhost:3000$/.test(url));
|
|
||||||
done();
|
|
||||||
});
|
|
||||||
|
|
||||||
client.on('error', function(err) {
|
|
||||||
console.error(err);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test('shutdown', function() {
|
|
||||||
localtunnel_server.close();
|
|
||||||
});
|
|
||||||
|
|
||||||
105
test/queue.js
105
test/queue.js
@@ -1,105 +0,0 @@
|
|||||||
var http = require('http');
|
|
||||||
var url = require('url');
|
|
||||||
var assert = require('assert');
|
|
||||||
|
|
||||||
var localtunnel_server = require('../').server({
|
|
||||||
max_tcp_sockets: 1
|
|
||||||
});
|
|
||||||
|
|
||||||
var localtunnel_client = require('../').client;
|
|
||||||
|
|
||||||
var server;
|
|
||||||
|
|
||||||
test('setup localtunnel server', function(done) {
|
|
||||||
localtunnel_server.listen(3000, function() {
|
|
||||||
console.log('lt server on:', 3000);
|
|
||||||
done();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test('setup 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');
|
|
||||||
}, 100);
|
|
||||||
});
|
|
||||||
|
|
||||||
server.listen(function() {
|
|
||||||
var port = server.address().port;
|
|
||||||
|
|
||||||
test._fake_port = port;
|
|
||||||
console.log('local http on:', port);
|
|
||||||
done();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test('setup localtunnel client', function(done) {
|
|
||||||
var client = localtunnel_client.connect({
|
|
||||||
host: 'http://localhost:' + 3000,
|
|
||||||
port: test._fake_port
|
|
||||||
});
|
|
||||||
|
|
||||||
client.on('url', function(url) {
|
|
||||||
assert.ok(/^http:\/\/.*localhost:3000$/.test(url));
|
|
||||||
test._fake_url = url;
|
|
||||||
done();
|
|
||||||
});
|
|
||||||
|
|
||||||
client.on('error', function(err) {
|
|
||||||
console.error(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: 3000,
|
|
||||||
agent: false,
|
|
||||||
headers: {
|
|
||||||
host: hostname
|
|
||||||
},
|
|
||||||
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<num_requests ; ++i) {
|
|
||||||
make_req();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
test('shutdown', function() {
|
|
||||||
localtunnel_server.close();
|
|
||||||
});
|
|
||||||
|
|
||||||
311
yarn.lock
Normal file
311
yarn.lock
Normal file
@@ -0,0 +1,311 @@
|
|||||||
|
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
|
||||||
|
# yarn lockfile v1
|
||||||
|
|
||||||
|
|
||||||
|
ansi-regex@^4.1.0:
|
||||||
|
version "4.1.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-4.1.0.tgz#8b9f8f08cf1acb843756a839ca8c7e3168c51997"
|
||||||
|
integrity sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==
|
||||||
|
|
||||||
|
ansi-styles@^3.2.0:
|
||||||
|
version "3.2.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d"
|
||||||
|
integrity sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==
|
||||||
|
dependencies:
|
||||||
|
color-convert "^1.9.0"
|
||||||
|
|
||||||
|
axios@0.19.0:
|
||||||
|
version "0.19.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/axios/-/axios-0.19.0.tgz#8e09bff3d9122e133f7b8101c8fbdd00ed3d2ab8"
|
||||||
|
integrity sha512-1uvKqKQta3KBxIz14F2v06AEHZ/dIoeKfbTRkK1E5oqjDnuEerLmYTgJB5AiQZHJcljpg1TuRzdjDR06qNk0DQ==
|
||||||
|
dependencies:
|
||||||
|
follow-redirects "1.5.10"
|
||||||
|
is-buffer "^2.0.2"
|
||||||
|
|
||||||
|
camelcase@^5.0.0:
|
||||||
|
version "5.3.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320"
|
||||||
|
integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==
|
||||||
|
|
||||||
|
cliui@^5.0.0:
|
||||||
|
version "5.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/cliui/-/cliui-5.0.0.tgz#deefcfdb2e800784aa34f46fa08e06851c7bbbc5"
|
||||||
|
integrity sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA==
|
||||||
|
dependencies:
|
||||||
|
string-width "^3.1.0"
|
||||||
|
strip-ansi "^5.2.0"
|
||||||
|
wrap-ansi "^5.1.0"
|
||||||
|
|
||||||
|
color-convert@^1.9.0:
|
||||||
|
version "1.9.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8"
|
||||||
|
integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==
|
||||||
|
dependencies:
|
||||||
|
color-name "1.1.3"
|
||||||
|
|
||||||
|
color-name@1.1.3:
|
||||||
|
version "1.1.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25"
|
||||||
|
integrity sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=
|
||||||
|
|
||||||
|
commander@0.6.1:
|
||||||
|
version "0.6.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/commander/-/commander-0.6.1.tgz#fa68a14f6a945d54dbbe50d8cdb3320e9e3b1a06"
|
||||||
|
integrity sha1-+mihT2qUXVTbvlDYzbMyDp47GgY=
|
||||||
|
|
||||||
|
commander@2.0.0:
|
||||||
|
version "2.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/commander/-/commander-2.0.0.tgz#d1b86f901f8b64bd941bdeadaf924530393be928"
|
||||||
|
integrity sha1-0bhvkB+LZL2UG96tr5JFMDk76Sg=
|
||||||
|
|
||||||
|
debug@*, debug@4.1.1:
|
||||||
|
version "4.1.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/debug/-/debug-4.1.1.tgz#3b72260255109c6b589cee050f1d516139664791"
|
||||||
|
integrity sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==
|
||||||
|
dependencies:
|
||||||
|
ms "^2.1.1"
|
||||||
|
|
||||||
|
debug@=3.1.0:
|
||||||
|
version "3.1.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261"
|
||||||
|
integrity sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==
|
||||||
|
dependencies:
|
||||||
|
ms "2.0.0"
|
||||||
|
|
||||||
|
decamelize@^1.2.0:
|
||||||
|
version "1.2.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290"
|
||||||
|
integrity sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=
|
||||||
|
|
||||||
|
diff@1.0.7:
|
||||||
|
version "1.0.7"
|
||||||
|
resolved "https://registry.yarnpkg.com/diff/-/diff-1.0.7.tgz#24bbb001c4a7d5522169e7cabdb2c2814ed91cf4"
|
||||||
|
integrity sha1-JLuwAcSn1VIhaefKvbLCgU7ZHPQ=
|
||||||
|
|
||||||
|
emoji-regex@^7.0.1:
|
||||||
|
version "7.0.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-7.0.3.tgz#933a04052860c85e83c122479c4748a8e4c72156"
|
||||||
|
integrity sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==
|
||||||
|
|
||||||
|
find-up@^3.0.0:
|
||||||
|
version "3.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/find-up/-/find-up-3.0.0.tgz#49169f1d7993430646da61ecc5ae355c21c97b73"
|
||||||
|
integrity sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==
|
||||||
|
dependencies:
|
||||||
|
locate-path "^3.0.0"
|
||||||
|
|
||||||
|
follow-redirects@1.5.10:
|
||||||
|
version "1.5.10"
|
||||||
|
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.5.10.tgz#7b7a9f9aea2fdff36786a94ff643ed07f4ff5e2a"
|
||||||
|
integrity sha512-0V5l4Cizzvqt5D44aTXbFZz+FtyXV1vrDN6qrelxtfYQKW0KO0W2T/hkE8xvGa/540LkZlkaUjO4ailYTFtHVQ==
|
||||||
|
dependencies:
|
||||||
|
debug "=3.1.0"
|
||||||
|
|
||||||
|
get-caller-file@^2.0.1:
|
||||||
|
version "2.0.5"
|
||||||
|
resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e"
|
||||||
|
integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==
|
||||||
|
|
||||||
|
glob@3.2.3:
|
||||||
|
version "3.2.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/glob/-/glob-3.2.3.tgz#e313eeb249c7affaa5c475286b0e115b59839467"
|
||||||
|
integrity sha1-4xPusknHr/qlxHUoaw4RW1mDlGc=
|
||||||
|
dependencies:
|
||||||
|
graceful-fs "~2.0.0"
|
||||||
|
inherits "2"
|
||||||
|
minimatch "~0.2.11"
|
||||||
|
|
||||||
|
graceful-fs@~2.0.0:
|
||||||
|
version "2.0.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-2.0.3.tgz#7cd2cdb228a4a3f36e95efa6cc142de7d1a136d0"
|
||||||
|
integrity sha1-fNLNsiiko/Nule+mzBQt59GhNtA=
|
||||||
|
|
||||||
|
growl@1.7.x:
|
||||||
|
version "1.7.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/growl/-/growl-1.7.0.tgz#de2d66136d002e112ba70f3f10c31cf7c350b2da"
|
||||||
|
integrity sha1-3i1mE20ALhErpw8/EMMc98NQsto=
|
||||||
|
|
||||||
|
inherits@2:
|
||||||
|
version "2.0.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c"
|
||||||
|
integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
|
||||||
|
|
||||||
|
is-buffer@^2.0.2:
|
||||||
|
version "2.0.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-2.0.3.tgz#4ecf3fcf749cbd1e472689e109ac66261a25e725"
|
||||||
|
integrity sha512-U15Q7MXTuZlrbymiz95PJpZxu8IlipAp4dtS3wOdgPXx3mqBnslrWU14kxfHB+Py/+2PVKSr37dMAgM2A4uArw==
|
||||||
|
|
||||||
|
is-fullwidth-code-point@^2.0.0:
|
||||||
|
version "2.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz#a3b30a5c4f199183167aaab93beefae3ddfb654f"
|
||||||
|
integrity sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=
|
||||||
|
|
||||||
|
jade@0.26.3:
|
||||||
|
version "0.26.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/jade/-/jade-0.26.3.tgz#8f10d7977d8d79f2f6ff862a81b0513ccb25686c"
|
||||||
|
integrity sha1-jxDXl32NefL2/4YqgbBRPMslaGw=
|
||||||
|
dependencies:
|
||||||
|
commander "0.6.1"
|
||||||
|
mkdirp "0.3.0"
|
||||||
|
|
||||||
|
locate-path@^3.0.0:
|
||||||
|
version "3.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-3.0.0.tgz#dbec3b3ab759758071b58fe59fc41871af21400e"
|
||||||
|
integrity sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==
|
||||||
|
dependencies:
|
||||||
|
p-locate "^3.0.0"
|
||||||
|
path-exists "^3.0.0"
|
||||||
|
|
||||||
|
lru-cache@2:
|
||||||
|
version "2.7.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-2.7.3.tgz#6d4524e8b955f95d4f5b58851ce21dd72fb4e952"
|
||||||
|
integrity sha1-bUUk6LlV+V1PW1iFHOId1y+06VI=
|
||||||
|
|
||||||
|
minimatch@~0.2.11:
|
||||||
|
version "0.2.14"
|
||||||
|
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-0.2.14.tgz#c74e780574f63c6f9a090e90efbe6ef53a6a756a"
|
||||||
|
integrity sha1-x054BXT2PG+aCQ6Q775u9TpqdWo=
|
||||||
|
dependencies:
|
||||||
|
lru-cache "2"
|
||||||
|
sigmund "~1.0.0"
|
||||||
|
|
||||||
|
mkdirp@0.3.0:
|
||||||
|
version "0.3.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.3.0.tgz#1bbf5ab1ba827af23575143490426455f481fe1e"
|
||||||
|
integrity sha1-G79asbqCevI1dRQ0kEJkVfSB/h4=
|
||||||
|
|
||||||
|
mkdirp@0.3.5:
|
||||||
|
version "0.3.5"
|
||||||
|
resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.3.5.tgz#de3e5f8961c88c787ee1368df849ac4413eca8d7"
|
||||||
|
integrity sha1-3j5fiWHIjHh+4TaN+EmsRBPsqNc=
|
||||||
|
|
||||||
|
mocha@~1.17.0:
|
||||||
|
version "1.17.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/mocha/-/mocha-1.17.1.tgz#7f7671d68526d074b7bae660c9099f87e0ea1ccb"
|
||||||
|
integrity sha1-f3Zx1oUm0HS3uuZgyQmfh+DqHMs=
|
||||||
|
dependencies:
|
||||||
|
commander "2.0.0"
|
||||||
|
debug "*"
|
||||||
|
diff "1.0.7"
|
||||||
|
glob "3.2.3"
|
||||||
|
growl "1.7.x"
|
||||||
|
jade "0.26.3"
|
||||||
|
mkdirp "0.3.5"
|
||||||
|
|
||||||
|
ms@2.0.0:
|
||||||
|
version "2.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"
|
||||||
|
integrity sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=
|
||||||
|
|
||||||
|
ms@^2.1.1:
|
||||||
|
version "2.1.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009"
|
||||||
|
integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==
|
||||||
|
|
||||||
|
openurl@1.1.1:
|
||||||
|
version "1.1.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/openurl/-/openurl-1.1.1.tgz#3875b4b0ef7a52c156f0db41d4609dbb0f94b387"
|
||||||
|
integrity sha1-OHW0sO96UsFW8NtB1GCduw+Us4c=
|
||||||
|
|
||||||
|
p-limit@^2.0.0:
|
||||||
|
version "2.2.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.2.1.tgz#aa07a788cc3151c939b5131f63570f0dd2009537"
|
||||||
|
integrity sha512-85Tk+90UCVWvbDavCLKPOLC9vvY8OwEX/RtKF+/1OADJMVlFfEHOiMTPVyxg7mk/dKa+ipdHm0OUkTvCpMTuwg==
|
||||||
|
dependencies:
|
||||||
|
p-try "^2.0.0"
|
||||||
|
|
||||||
|
p-locate@^3.0.0:
|
||||||
|
version "3.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-3.0.0.tgz#322d69a05c0264b25997d9f40cd8a891ab0064a4"
|
||||||
|
integrity sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==
|
||||||
|
dependencies:
|
||||||
|
p-limit "^2.0.0"
|
||||||
|
|
||||||
|
p-try@^2.0.0:
|
||||||
|
version "2.2.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6"
|
||||||
|
integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==
|
||||||
|
|
||||||
|
path-exists@^3.0.0:
|
||||||
|
version "3.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-3.0.0.tgz#ce0ebeaa5f78cb18925ea7d810d7b59b010fd515"
|
||||||
|
integrity sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=
|
||||||
|
|
||||||
|
require-directory@^2.1.1:
|
||||||
|
version "2.1.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42"
|
||||||
|
integrity sha1-jGStX9MNqxyXbiNE/+f3kqam30I=
|
||||||
|
|
||||||
|
require-main-filename@^2.0.0:
|
||||||
|
version "2.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-2.0.0.tgz#d0b329ecc7cc0f61649f62215be69af54aa8989b"
|
||||||
|
integrity sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==
|
||||||
|
|
||||||
|
set-blocking@^2.0.0:
|
||||||
|
version "2.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7"
|
||||||
|
integrity sha1-BF+XgtARrppoA93TgrJDkrPYkPc=
|
||||||
|
|
||||||
|
sigmund@~1.0.0:
|
||||||
|
version "1.0.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/sigmund/-/sigmund-1.0.1.tgz#3ff21f198cad2175f9f3b781853fd94d0d19b590"
|
||||||
|
integrity sha1-P/IfGYytIXX587eBhT/ZTQ0ZtZA=
|
||||||
|
|
||||||
|
string-width@^3.0.0, string-width@^3.1.0:
|
||||||
|
version "3.1.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/string-width/-/string-width-3.1.0.tgz#22767be21b62af1081574306f69ac51b62203961"
|
||||||
|
integrity sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==
|
||||||
|
dependencies:
|
||||||
|
emoji-regex "^7.0.1"
|
||||||
|
is-fullwidth-code-point "^2.0.0"
|
||||||
|
strip-ansi "^5.1.0"
|
||||||
|
|
||||||
|
strip-ansi@^5.0.0, strip-ansi@^5.1.0, strip-ansi@^5.2.0:
|
||||||
|
version "5.2.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-5.2.0.tgz#8c9a536feb6afc962bdfa5b104a5091c1ad9c0ae"
|
||||||
|
integrity sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==
|
||||||
|
dependencies:
|
||||||
|
ansi-regex "^4.1.0"
|
||||||
|
|
||||||
|
which-module@^2.0.0:
|
||||||
|
version "2.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a"
|
||||||
|
integrity sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=
|
||||||
|
|
||||||
|
wrap-ansi@^5.1.0:
|
||||||
|
version "5.1.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-5.1.0.tgz#1fd1f67235d5b6d0fee781056001bfb694c03b09"
|
||||||
|
integrity sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==
|
||||||
|
dependencies:
|
||||||
|
ansi-styles "^3.2.0"
|
||||||
|
string-width "^3.0.0"
|
||||||
|
strip-ansi "^5.0.0"
|
||||||
|
|
||||||
|
y18n@^4.0.0:
|
||||||
|
version "4.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.0.tgz#95ef94f85ecc81d007c264e190a120f0a3c8566b"
|
||||||
|
integrity sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==
|
||||||
|
|
||||||
|
yargs-parser@^13.1.1:
|
||||||
|
version "13.1.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-13.1.1.tgz#d26058532aa06d365fe091f6a1fc06b2f7e5eca0"
|
||||||
|
integrity sha512-oVAVsHz6uFrg3XQheFII8ESO2ssAf9luWuAd6Wexsu4F3OtIW0o8IribPXYrD4WC24LWtPrJlGy87y5udK+dxQ==
|
||||||
|
dependencies:
|
||||||
|
camelcase "^5.0.0"
|
||||||
|
decamelize "^1.2.0"
|
||||||
|
|
||||||
|
yargs@13.3.0:
|
||||||
|
version "13.3.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/yargs/-/yargs-13.3.0.tgz#4c657a55e07e5f2cf947f8a366567c04a0dedc83"
|
||||||
|
integrity sha512-2eehun/8ALW8TLoIl7MVaRUrg+yCnenu8B4kBlRxj3GJGDKU1Og7sMXPNm1BYyM1DOJmTZ4YeN/Nwxv+8XJsUA==
|
||||||
|
dependencies:
|
||||||
|
cliui "^5.0.0"
|
||||||
|
find-up "^3.0.0"
|
||||||
|
get-caller-file "^2.0.1"
|
||||||
|
require-directory "^2.1.1"
|
||||||
|
require-main-filename "^2.0.0"
|
||||||
|
set-blocking "^2.0.0"
|
||||||
|
string-width "^3.0.0"
|
||||||
|
which-module "^2.0.0"
|
||||||
|
y18n "^4.0.0"
|
||||||
|
yargs-parser "^13.1.1"
|
||||||
Reference in New Issue
Block a user