mirror of
https://github.com/bitinflow/localtunnel.git
synced 2026-03-13 13:35:54 +00:00
HTTPS support, Promise API, modern ES syntax
* Add support for tunneling a local HTTPS server. * Return a Promise from localtunnel.
This commit is contained in:
committed by
Roman Shtylman
parent
d7330a7121
commit
2a74d6be9f
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 +0,0 @@
|
|||||||
support
|
|
||||||
73
CHANGELOG.md
Normal file
73
CHANGELOG.md
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
# UNRELEASED
|
||||||
|
|
||||||
|
- 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
|
||||||
66
History.md
66
History.md
@@ -1,66 +0,0 @@
|
|||||||
# 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
|
|
||||||
92
README.md
92
README.md
@@ -1,22 +1,32 @@
|
|||||||
# localtunnel
|
# localtunnel
|
||||||
|
|
||||||
[](https://travis-ci.org/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
|
||||||
```
|
```
|
||||||
|
|
||||||
This will install the localtunnel module globally and add the 'lt' client cli tool to your PATH.
|
### As a dependency in your project
|
||||||
|
|
||||||
## use ##
|
```
|
||||||
|
yarn add localtunnel
|
||||||
|
```
|
||||||
|
|
||||||
Assuming your local server is running on port 8000, just use the ```lt``` command to start the tunnel.
|
## CLI usage
|
||||||
|
|
||||||
|
When localtunnel is installed globally, just use the `lt` command to start the tunnel.
|
||||||
|
|
||||||
```
|
```
|
||||||
lt --port 8000
|
lt --port 8000
|
||||||
@@ -24,14 +34,14 @@ 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.
|
||||||
|
|
||||||
### arguments
|
### Arguments
|
||||||
|
|
||||||
Below are some common arguments. See `lt --help` for additional arguments
|
Below are some common arguments. See `lt --help` for additional arguments
|
||||||
|
|
||||||
* `--subdomain` request a named subdomain on the localtunnel server (default is random characters)
|
- `--subdomain` request a named subdomain on the localtunnel server (default is random characters)
|
||||||
* `--local-host` proxy to a hostname other than localhost
|
- `--local-host` proxy to a hostname other than localhost
|
||||||
|
|
||||||
You may also specify arguments via env variables. E.x.
|
You may also specify arguments via env variables. E.x.
|
||||||
|
|
||||||
@@ -39,62 +49,72 @@ You may also specify arguments via env variables. E.x.
|
|||||||
PORT=3000 lt
|
PORT=3000 lt
|
||||||
```
|
```
|
||||||
|
|
||||||
## API ##
|
## API
|
||||||
|
|
||||||
The localtunnel client is also usable through an API (for test integration, automation, etc)
|
The localtunnel client is also usable through an API (for test integration, automation, etc)
|
||||||
|
|
||||||
### localtunnel(port [,opts], fn)
|
### localtunnel(port [,options][,callback])
|
||||||
|
|
||||||
Creates a new localtunnel to the specified local `port`. `fn` will be called once you have been assigned a public localtunnel url. `opts` can be used to request a specific `subdomain`.
|
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.
|
||||||
|
|
||||||
```javascript
|
```js
|
||||||
var localtunnel = require('localtunnel');
|
const localtunnel = require('localtunnel');
|
||||||
|
|
||||||
var tunnel = localtunnel(port, function(err, tunnel) {
|
(async () => {
|
||||||
if (err) ...
|
const tunnel = await localtunnel({ port: 3000 });
|
||||||
|
|
||||||
// the assigned public url for your tunnel
|
// the assigned public url for your tunnel
|
||||||
// i.e. https://abcdefgjhij.localtunnel.me
|
// i.e. https://abcdefgjhij.localtunnel.me
|
||||||
tunnel.url;
|
tunnel.url;
|
||||||
});
|
|
||||||
|
|
||||||
tunnel.on('close', function() {
|
tunnel.on('close', () => {
|
||||||
// tunnels are closed
|
// tunnels are closed
|
||||||
});
|
});
|
||||||
|
})();
|
||||||
```
|
```
|
||||||
|
|
||||||
### opts
|
#### options
|
||||||
|
|
||||||
* `subdomain` A *string* value requesting a specific subdomain on the proxy server. **Note** You may not actually receive this name depending on availability.
|
- `port` (number) [required] The local port number to expose through localtunnel.
|
||||||
* `local_host` Proxy to this hostname instead of `localhost`. This will also cause the `Host` header to be re-written to this value in proxied requests.
|
- `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
|
### Tunnel
|
||||||
|
|
||||||
The `tunnel` instance returned to your callback emits the following events
|
The `tunnel` instance returned to your callback emits the following events
|
||||||
|
|
||||||
|event|args|description|
|
| event | args | description |
|
||||||
|----|----|----|
|
| ------- | ---- | ------------------------------------------------------------------------------------ |
|
||||||
|request|info|fires when a request is processed by the tunnel, contains _method_ and _path_ fields|
|
| 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|
|
| error | err | fires when an error happens on the tunnel |
|
||||||
|close||fires when the tunnel has closed|
|
| close | | fires when the tunnel has closed |
|
||||||
|
|
||||||
The `tunnel` instance has the following methods
|
The `tunnel` instance has the following methods
|
||||||
|
|
||||||
|method|args|description|
|
| method | args | description |
|
||||||
|----|----|----|
|
| ------ | ---- | ---------------- |
|
||||||
|close||close the tunnel|
|
| close | | close the tunnel |
|
||||||
|
|
||||||
## other clients ##
|
## other clients
|
||||||
|
|
||||||
Clients in other languages
|
Clients in other languages
|
||||||
|
|
||||||
*go* [gotunnelme](https://github.com/NoahShen/gotunnelme)
|
_go_ [gotunnelme](https://github.com/NoahShen/gotunnelme)
|
||||||
|
|
||||||
*go* [go-localtunnel](https://github.com/localtunnel/go-localtunnel)
|
_go_ [go-localtunnel](https://github.com/localtunnel/go-localtunnel)
|
||||||
|
|
||||||
## server ##
|
## server
|
||||||
|
|
||||||
See [localtunnel/server](//github.com/localtunnel/server) for details on the server that powers localtunnel.
|
See [localtunnel/server](//github.com/localtunnel/server) for details on the server that powers localtunnel.
|
||||||
|
|
||||||
## License ##
|
## License
|
||||||
|
|
||||||
MIT
|
MIT
|
||||||
|
|||||||
75
bin/client
75
bin/client
@@ -1,75 +0,0 @@
|
|||||||
#!/usr/bin/env node
|
|
||||||
var lt_client = require('../client');
|
|
||||||
var open_url = require('openurl');
|
|
||||||
|
|
||||||
var argv = require('yargs')
|
|
||||||
.usage('Usage: $0 --port [num] <options>')
|
|
||||||
.env(true)
|
|
||||||
.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'
|
|
||||||
})
|
|
||||||
.options('o', {
|
|
||||||
alias: 'open',
|
|
||||||
describe: 'opens url in your browser'
|
|
||||||
})
|
|
||||||
.option('p', {
|
|
||||||
alias: 'port',
|
|
||||||
describe: 'Internal http server port',
|
|
||||||
})
|
|
||||||
.option('print-requests', {
|
|
||||||
describe: 'Print basic request info',
|
|
||||||
})
|
|
||||||
.require('port')
|
|
||||||
.boolean('print-requests')
|
|
||||||
.help('help', 'Show this help and exit')
|
|
||||||
.version(require('../package').version)
|
|
||||||
.argv;
|
|
||||||
|
|
||||||
if (typeof argv.port !== 'number') {
|
|
||||||
require('yargs').showHelp();
|
|
||||||
console.error('port must be a number');
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
var opt = {
|
|
||||||
host: argv.host,
|
|
||||||
port: argv.port,
|
|
||||||
local_host: argv['local-host'],
|
|
||||||
subdomain: argv.subdomain,
|
|
||||||
};
|
|
||||||
|
|
||||||
const PrintRequests = argv['print-requests'];
|
|
||||||
|
|
||||||
lt_client(opt.port, opt, function(err, tunnel) {
|
|
||||||
if (err) {
|
|
||||||
throw err;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('your url is: %s', tunnel.url);
|
|
||||||
|
|
||||||
if (argv.open) {
|
|
||||||
open_url.open(tunnel.url);
|
|
||||||
}
|
|
||||||
|
|
||||||
tunnel.on('error', function(err) {
|
|
||||||
throw err;
|
|
||||||
});
|
|
||||||
|
|
||||||
if (PrintRequests) {
|
|
||||||
tunnel.on('request', function(info) {
|
|
||||||
console.log(new Date().toString(), info.method, info.path);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// 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);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})();
|
||||||
24
client.js
24
client.js
@@ -1,24 +0,0 @@
|
|||||||
var EventEmitter = require('events').EventEmitter;
|
|
||||||
var debug = require('debug')('localtunnel:client');
|
|
||||||
|
|
||||||
var Tunnel = require('./lib/Tunnel');
|
|
||||||
|
|
||||||
module.exports = function localtunnel(port, opt, fn) {
|
|
||||||
if (typeof opt === 'function') {
|
|
||||||
fn = opt;
|
|
||||||
opt = {};
|
|
||||||
}
|
|
||||||
|
|
||||||
opt = opt || {};
|
|
||||||
opt.port = port;
|
|
||||||
|
|
||||||
var client = Tunnel(opt);
|
|
||||||
client.open(function(err) {
|
|
||||||
if (err) {
|
|
||||||
return fn(err);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn(null, client);
|
|
||||||
});
|
|
||||||
return client;
|
|
||||||
};
|
|
||||||
@@ -1,39 +1,23 @@
|
|||||||
var stream = require('stream');
|
const { Transform } = require('stream');
|
||||||
var util = require('util');
|
|
||||||
|
|
||||||
var Transform = stream.Transform;
|
class HeaderHostTransformer extends Transform {
|
||||||
|
constructor(opts = {}) {
|
||||||
var HeaderHostTransformer = function(opts) {
|
super(opts);
|
||||||
if (!(this instanceof HeaderHostTransformer)) {
|
this.host = opts.host || 'localhost';
|
||||||
return new HeaderHostTransformer(opts);
|
this.replaced = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
opts = opts || {}
|
_transform(data, encoding, callback) {
|
||||||
Transform.call(this, opts);
|
callback(
|
||||||
|
null,
|
||||||
var self = this;
|
this.replaced // after replacing the first instance of the Host header we just become a regular passthrough
|
||||||
self.host = opts.host || 'localhost';
|
? data
|
||||||
self.replaced = false;
|
: data.toString().replace(/(\r\n[Hh]ost: )\S+/, (match, $1) => {
|
||||||
|
this.replaced = true;
|
||||||
|
return $1 + this.host;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
util.inherits(HeaderHostTransformer, Transform);
|
|
||||||
|
|
||||||
HeaderHostTransformer.prototype._transform = function (chunk, enc, cb) {
|
|
||||||
var self = this;
|
|
||||||
|
|
||||||
// after replacing the first instance of the Host header
|
|
||||||
// we just become a regular passthrough
|
|
||||||
if (!self.replaced) {
|
|
||||||
chunk = chunk.toString();
|
|
||||||
self.push(chunk.replace(/(\r\n[Hh]ost: )\S+/, function(match, $1) {
|
|
||||||
self.replaced = true;
|
|
||||||
return $1 + self.host;
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
self.push(chunk);
|
|
||||||
}
|
|
||||||
|
|
||||||
cb();
|
|
||||||
};
|
|
||||||
|
|
||||||
module.exports = HeaderHostTransformer;
|
module.exports = HeaderHostTransformer;
|
||||||
|
|||||||
211
lib/Tunnel.js
211
lib/Tunnel.js
@@ -1,158 +1,163 @@
|
|||||||
var url = require('url');
|
/* eslint-disable consistent-return, no-underscore-dangle */
|
||||||
var EventEmitter = require('events').EventEmitter;
|
|
||||||
var axios = require('axios');
|
|
||||||
var debug = require('debug')('localtunnel:client');
|
|
||||||
|
|
||||||
var TunnelCluster = require('./TunnelCluster');
|
const { parse } = require('url');
|
||||||
|
const { EventEmitter } = require('events');
|
||||||
|
const axios = require('axios');
|
||||||
|
const debug = require('debug')('localtunnel:client');
|
||||||
|
|
||||||
var Tunnel = function(opt) {
|
const TunnelCluster = require('./TunnelCluster');
|
||||||
if (!(this instanceof Tunnel)) {
|
|
||||||
return new Tunnel(opt);
|
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';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var self = this;
|
_getInfo(body) {
|
||||||
self._closed = false;
|
/* eslint-disable camelcase */
|
||||||
self._opt = opt || {};
|
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 */
|
||||||
|
}
|
||||||
|
|
||||||
self._opt.host = self._opt.host || 'https://localtunnel.me';
|
// initialize connection
|
||||||
};
|
// callback with connection info
|
||||||
|
_init(cb) {
|
||||||
|
const opt = this.opts;
|
||||||
|
const getInfo = this._getInfo.bind(this);
|
||||||
|
|
||||||
Tunnel.prototype.__proto__ = EventEmitter.prototype;
|
const params = {
|
||||||
|
responseType: 'json',
|
||||||
// initialize connection
|
|
||||||
// callback with connection info
|
|
||||||
Tunnel.prototype._init = function(cb) {
|
|
||||||
var self = this;
|
|
||||||
var opt = self._opt;
|
|
||||||
|
|
||||||
var params = {
|
|
||||||
responseType: 'json'
|
|
||||||
};
|
};
|
||||||
|
|
||||||
var base_uri = opt.host + '/';
|
const baseUri = `${opt.host}/`;
|
||||||
|
|
||||||
// optionally override the upstream server
|
|
||||||
var upstream = url.parse(opt.host);
|
|
||||||
|
|
||||||
// no subdomain at first, maybe use requested domain
|
// no subdomain at first, maybe use requested domain
|
||||||
var assigned_domain = opt.subdomain;
|
const assignedDomain = opt.subdomain;
|
||||||
|
|
||||||
// where to quest
|
// where to quest
|
||||||
var uri = base_uri + ((assigned_domain) ? assigned_domain : '?new');
|
const uri = baseUri + (assignedDomain || '?new');
|
||||||
|
|
||||||
(function get_url() {
|
(function getUrl() {
|
||||||
axios.get(uri, params)
|
axios
|
||||||
.then(function(res){
|
.get(uri, params)
|
||||||
var body = res.data;
|
.then(res => {
|
||||||
|
const body = res.data;
|
||||||
|
debug('got tunnel information', res.data);
|
||||||
if (res.status !== 200) {
|
if (res.status !== 200) {
|
||||||
var err = new Error((body && body.message) || 'localtunnel server returned an error, please try again');
|
const err = new Error(
|
||||||
|
(body && body.message) || 'localtunnel server returned an error, please try again'
|
||||||
|
);
|
||||||
return cb(err);
|
return cb(err);
|
||||||
}
|
}
|
||||||
var port = body.port;
|
cb(null, getInfo(body));
|
||||||
var host = upstream.hostname;
|
})
|
||||||
var max_conn = body.max_conn_count || 1;
|
.catch(err => {
|
||||||
cb(null, {
|
debug(`tunnel server offline: ${err.message}, retry 1s`);
|
||||||
remote_host: upstream.hostname,
|
return setTimeout(getUrl, 1000);
|
||||||
remote_port: body.port,
|
|
||||||
name: body.id,
|
|
||||||
url: body.url,
|
|
||||||
max_conn: max_conn
|
|
||||||
});
|
});
|
||||||
})
|
|
||||||
.catch(function(err){
|
|
||||||
// TODO (shtylman) don't print to stdout?
|
|
||||||
console.log('tunnel server offline: ' + err.message + ', retry 1s');
|
|
||||||
return setTimeout(get_url, 1000);
|
|
||||||
})
|
|
||||||
})();
|
})();
|
||||||
};
|
}
|
||||||
|
|
||||||
Tunnel.prototype._establish = function(info) {
|
|
||||||
var self = this;
|
|
||||||
var opt = self._opt;
|
|
||||||
|
|
||||||
|
_establish(info) {
|
||||||
// increase max event listeners so that localtunnel consumers don't get
|
// increase max event listeners so that localtunnel consumers don't get
|
||||||
// warning messages as soon as they setup even one listener. See #71
|
// warning messages as soon as they setup even one listener. See #71
|
||||||
self.setMaxListeners(info.max_conn + (EventEmitter.defaultMaxListeners || 10));
|
this.setMaxListeners(info.max_conn + (EventEmitter.defaultMaxListeners || 10));
|
||||||
|
|
||||||
info.local_host = opt.local_host;
|
this.tunnelCluster = new TunnelCluster(info);
|
||||||
info.local_port = opt.port;
|
|
||||||
|
|
||||||
var tunnels = self.tunnel_cluster = TunnelCluster(info);
|
|
||||||
|
|
||||||
// only emit the url the first time
|
// only emit the url the first time
|
||||||
tunnels.once('open', function() {
|
this.tunnelCluster.once('open', () => {
|
||||||
self.emit('url', info.url);
|
this.emit('url', info.url);
|
||||||
});
|
});
|
||||||
|
|
||||||
// re-emit socket error
|
// re-emit socket error
|
||||||
tunnels.on('error', function(err) {
|
this.tunnelCluster.on('error', err => {
|
||||||
self.emit('error', err);
|
debug('got socket error', err.message);
|
||||||
|
this.emit('error', err);
|
||||||
});
|
});
|
||||||
|
|
||||||
var tunnel_count = 0;
|
let tunnelCount = 0;
|
||||||
|
|
||||||
// track open count
|
// track open count
|
||||||
tunnels.on('open', function(tunnel) {
|
this.tunnelCluster.on('open', tunnel => {
|
||||||
tunnel_count++;
|
tunnelCount++;
|
||||||
debug('tunnel open [total: %d]', tunnel_count);
|
debug('tunnel open [total: %d]', tunnelCount);
|
||||||
|
|
||||||
var close_handler = function() {
|
const closeHandler = () => {
|
||||||
tunnel.destroy();
|
tunnel.destroy();
|
||||||
};
|
};
|
||||||
|
|
||||||
if (self._closed) {
|
if (this.closed) {
|
||||||
return close_handler();
|
return closeHandler();
|
||||||
}
|
}
|
||||||
|
|
||||||
self.once('close', close_handler);
|
this.once('close', closeHandler);
|
||||||
tunnel.once('close', function() {
|
tunnel.once('close', () => {
|
||||||
self.removeListener('close', close_handler);
|
this.removeListener('close', closeHandler);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// when a tunnel dies, open a new one
|
// when a tunnel dies, open a new one
|
||||||
tunnels.on('dead', function(tunnel) {
|
this.tunnelCluster.on('dead', () => {
|
||||||
tunnel_count--;
|
tunnelCount--;
|
||||||
debug('tunnel dead [total: %d]', tunnel_count);
|
debug('tunnel dead [total: %d]', tunnelCount);
|
||||||
|
if (this.closed) {
|
||||||
if (self._closed) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
this.tunnelCluster.open();
|
||||||
tunnels.open();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
tunnels.on('request', function(info) {
|
this.tunnelCluster.on('request', req => {
|
||||||
self.emit('request', info);
|
this.emit('request', req);
|
||||||
});
|
});
|
||||||
|
|
||||||
// establish as many tunnels as allowed
|
// establish as many tunnels as allowed
|
||||||
for (var count = 0 ; count < info.max_conn ; ++count) {
|
for (let count = 0; count < info.max_conn; ++count) {
|
||||||
tunnels.open();
|
this.tunnelCluster.open();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
Tunnel.prototype.open = function(cb) {
|
open(cb) {
|
||||||
var self = this;
|
this._init((err, info) => {
|
||||||
|
|
||||||
self._init(function(err, info) {
|
|
||||||
if (err) {
|
if (err) {
|
||||||
return cb(err);
|
return cb(err);
|
||||||
}
|
}
|
||||||
|
|
||||||
self.url = info.url;
|
this.clientId = info.name;
|
||||||
self._establish(info);
|
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();
|
cb();
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
close() {
|
||||||
|
this.closed = true;
|
||||||
|
this.emit('close');
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// shutdown tunnels
|
|
||||||
Tunnel.prototype.close = function() {
|
|
||||||
var self = this;
|
|
||||||
|
|
||||||
self._closed = true;
|
|
||||||
self.emit('close');
|
|
||||||
};
|
|
||||||
|
|
||||||
module.exports = Tunnel;
|
|
||||||
|
|||||||
@@ -1,122 +1,142 @@
|
|||||||
var EventEmitter = require('events').EventEmitter;
|
const { EventEmitter } = require('events');
|
||||||
var debug = require('debug')('localtunnel:client');
|
const debug = require('debug')('localtunnel:client');
|
||||||
var net = require('net');
|
const fs = require('fs');
|
||||||
|
const net = require('net');
|
||||||
|
const tls = require('tls');
|
||||||
|
|
||||||
var HeaderHostTransformer = require('./HeaderHostTransformer');
|
const HeaderHostTransformer = require('./HeaderHostTransformer');
|
||||||
|
|
||||||
// manages groups of tunnels
|
// manages groups of tunnels
|
||||||
var TunnelCluster = function(opt) {
|
module.exports = class TunnelCluster extends EventEmitter {
|
||||||
if (!(this instanceof TunnelCluster)) {
|
constructor(opts = {}) {
|
||||||
return new TunnelCluster(opt);
|
super(opts);
|
||||||
|
this.opts = opts;
|
||||||
}
|
}
|
||||||
|
|
||||||
var self = this;
|
open() {
|
||||||
self._opt = opt;
|
const opt = this.opts;
|
||||||
|
|
||||||
EventEmitter.call(self);
|
// 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;
|
||||||
|
|
||||||
TunnelCluster.prototype.__proto__ = EventEmitter.prototype;
|
debug(
|
||||||
|
'establishing tunnel %s://%s:%s <> %s:%s',
|
||||||
// establish a new tunnel
|
localProtocol,
|
||||||
TunnelCluster.prototype.open = function() {
|
localHost,
|
||||||
var self = this;
|
localPort,
|
||||||
|
remoteHostOrIp,
|
||||||
var opt = self._opt || {};
|
remotePort
|
||||||
|
);
|
||||||
var remote_host = opt.remote_host;
|
|
||||||
var remote_port = opt.remote_port;
|
|
||||||
|
|
||||||
var local_host = opt.local_host || 'localhost';
|
|
||||||
var local_port = opt.local_port;
|
|
||||||
|
|
||||||
debug('establishing tunnel %s:%s <> %s:%s', local_host, local_port, remote_host, remote_port);
|
|
||||||
|
|
||||||
// connection to localtunnel server
|
// connection to localtunnel server
|
||||||
var remote = net.connect({
|
const remote = net.connect({
|
||||||
host: remote_host,
|
host: remoteHostOrIp,
|
||||||
port: remote_port
|
port: remotePort,
|
||||||
});
|
});
|
||||||
|
|
||||||
remote.setKeepAlive(true);
|
remote.setKeepAlive(true);
|
||||||
|
|
||||||
remote.on('error', function(err) {
|
remote.on('error', err => {
|
||||||
|
debug('got remote connection error', err.message);
|
||||||
|
|
||||||
// emit connection refused errors immediately, because they
|
// emit connection refused errors immediately, because they
|
||||||
// indicate that the tunnel can't be established.
|
// indicate that the tunnel can't be established.
|
||||||
if (err.code === 'ECONNREFUSED') {
|
if (err.code === 'ECONNREFUSED') {
|
||||||
self.emit('error', new Error('connection refused: ' + remote_host + ':' + remote_port + ' (check your firewall settings)'));
|
this.emit(
|
||||||
|
'error',
|
||||||
|
new Error(
|
||||||
|
`connection refused: ${remoteHostOrIp}:${remotePort} (check your firewall settings)`
|
||||||
|
)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
remote.end();
|
remote.end();
|
||||||
});
|
});
|
||||||
|
|
||||||
function conn_local() {
|
const connLocal = () => {
|
||||||
if (remote.destroyed) {
|
if (remote.destroyed) {
|
||||||
debug('remote destroyed');
|
debug('remote destroyed');
|
||||||
self.emit('dead');
|
this.emit('dead');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
debug('connecting locally to %s:%d', local_host, local_port);
|
debug('connecting locally to %s://%s:%d', localProtocol, localHost, localPort);
|
||||||
remote.pause();
|
remote.pause();
|
||||||
|
|
||||||
// connection to local http server
|
if (allowInvalidCert) {
|
||||||
var local = net.connect({
|
debug('allowing invalid certificates');
|
||||||
host: local_host,
|
}
|
||||||
port: local_port
|
|
||||||
});
|
|
||||||
|
|
||||||
function remote_close() {
|
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');
|
debug('remote close');
|
||||||
self.emit('dead');
|
this.emit('dead');
|
||||||
local.end();
|
local.end();
|
||||||
};
|
};
|
||||||
|
|
||||||
remote.once('close', remote_close);
|
remote.once('close', remoteClose);
|
||||||
|
|
||||||
// TODO some languages have single threaded servers which makes opening up
|
// TODO some languages have single threaded servers which makes opening up
|
||||||
// multiple local connections impossible. We need a smarter way to scale
|
// 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
|
// and adjust for such instances to avoid beating on the door of the server
|
||||||
local.once('error', function(err) {
|
local.once('error', err => {
|
||||||
debug('local error %s', err.message);
|
debug('local error %s', err.message);
|
||||||
local.end();
|
local.end();
|
||||||
|
|
||||||
remote.removeListener('close', remote_close);
|
remote.removeListener('close', remoteClose);
|
||||||
|
|
||||||
if (err.code !== 'ECONNREFUSED') {
|
if (err.code !== 'ECONNREFUSED') {
|
||||||
return remote.end();
|
return remote.end();
|
||||||
}
|
}
|
||||||
|
|
||||||
// retrying connection to local server
|
// retrying connection to local server
|
||||||
setTimeout(conn_local, 1000);
|
setTimeout(connLocal, 1000);
|
||||||
});
|
});
|
||||||
|
|
||||||
local.once('connect', function() {
|
local.once('connect', () => {
|
||||||
debug('connected locally');
|
debug('connected locally');
|
||||||
remote.resume();
|
remote.resume();
|
||||||
|
|
||||||
var stream = remote;
|
let stream = remote;
|
||||||
|
|
||||||
// if user requested specific local host
|
// if user requested specific local host
|
||||||
// then we use host header transform to replace the host header
|
// then we use host header transform to replace the host header
|
||||||
if (opt.local_host) {
|
if (opt.local_host) {
|
||||||
debug('transform Host header to %s', opt.local_host);
|
debug('transform Host header to %s', opt.local_host);
|
||||||
stream = remote.pipe(HeaderHostTransformer({ host: opt.local_host }));
|
stream = remote.pipe(new HeaderHostTransformer({ host: opt.local_host }));
|
||||||
}
|
}
|
||||||
|
|
||||||
stream.pipe(local).pipe(remote);
|
stream.pipe(local).pipe(remote);
|
||||||
|
|
||||||
// when local closes, also get a new remote
|
// when local closes, also get a new remote
|
||||||
local.once('close', function(had_error) {
|
local.once('close', hadError => {
|
||||||
debug('local connection closed [%s]', had_error);
|
debug('local connection closed [%s]', hadError);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
remote.on('data', function(data) {
|
remote.on('data', data => {
|
||||||
const match = data.toString().match(/^(\w+) (\S+)/);
|
const match = data.toString().match(/^(\w+) (\S+)/);
|
||||||
if (match) {
|
if (match) {
|
||||||
self.emit('request', {
|
this.emit('request', {
|
||||||
method: match[1],
|
method: match[1],
|
||||||
path: match[2],
|
path: match[2],
|
||||||
});
|
});
|
||||||
@@ -124,10 +144,9 @@ TunnelCluster.prototype.open = function() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// tunnel is considered open when remote connects
|
// tunnel is considered open when remote connects
|
||||||
remote.once('connect', function() {
|
remote.once('connect', () => {
|
||||||
self.emit('open', remote);
|
this.emit('open', remote);
|
||||||
conn_local();
|
connLocal();
|
||||||
});
|
});
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = TunnelCluster;
|
|
||||||
|
|||||||
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'));
|
||||||
|
});
|
||||||
|
});
|
||||||
30
package.json
30
package.json
@@ -1,27 +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": "1.9.2",
|
"version": "2.0.0-pre",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "git://github.com/localtunnel/localtunnel.git"
|
"url": "git://github.com/localtunnel/localtunnel.git"
|
||||||
},
|
},
|
||||||
|
"author": "Roman Shtylman <shtylman@gmail.com>",
|
||||||
|
"contributors": [
|
||||||
|
"Roman Shtylman <shtylman@gmail.com>",
|
||||||
|
"Gert Hengeveld <gert@hichroma.com>",
|
||||||
|
"Tom Coleman <tom@hichroma.com>"
|
||||||
|
],
|
||||||
|
"main": "./localtunnel.js",
|
||||||
|
"bin": {
|
||||||
|
"lt": "./bin/lt.js"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"test": "mocha --ui qunit --reporter list --timeout 60000 -- *.spec.js"
|
||||||
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"axios": "0.19.0",
|
"axios": "0.19.0",
|
||||||
"debug": "4.1.1",
|
"debug": "4.1.1",
|
||||||
"openurl": "1.1.1",
|
"openurl": "1.1.1",
|
||||||
"yargs": "6.6.0"
|
"yargs": "13.3.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"mocha": "~1.17.0"
|
"mocha": "~1.17.0"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"engines": {
|
||||||
"test": "mocha --ui qunit --reporter list --timeout 10000 -- test/index.js"
|
"node": ">=8.3.0"
|
||||||
},
|
}
|
||||||
"bin": {
|
|
||||||
"lt": "./bin/client"
|
|
||||||
},
|
|
||||||
"main": "./client.js"
|
|
||||||
}
|
}
|
||||||
188
test/index.js
188
test/index.js
@@ -1,188 +0,0 @@
|
|||||||
var http = require('http');
|
|
||||||
var https = require('https');
|
|
||||||
var url = require('url');
|
|
||||||
var assert = require('assert');
|
|
||||||
|
|
||||||
var localtunnel = require('../');
|
|
||||||
|
|
||||||
test('setup local http server', function(done) {
|
|
||||||
var server = http.createServer();
|
|
||||||
server.on('request', function(req, res) {
|
|
||||||
res.write(req.headers.host);
|
|
||||||
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) {
|
|
||||||
localtunnel(test._fake_port, function(err, tunnel) {
|
|
||||||
assert.ifError(err);
|
|
||||||
assert.ok(new RegExp('^https:\/\/.*localtunnel.me' + '$').test(tunnel.url));
|
|
||||||
test._fake_url = tunnel.url;
|
|
||||||
done();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test('query localtunnel server w/ ident', function(done) {
|
|
||||||
var uri = test._fake_url;
|
|
||||||
var parsed = url.parse(uri);
|
|
||||||
|
|
||||||
var opt = {
|
|
||||||
host: parsed.host,
|
|
||||||
port: 443,
|
|
||||||
headers: {
|
|
||||||
host: parsed.hostname
|
|
||||||
},
|
|
||||||
path: '/'
|
|
||||||
};
|
|
||||||
|
|
||||||
var req = https.request(opt, function(res) {
|
|
||||||
res.setEncoding('utf8');
|
|
||||||
var body = '';
|
|
||||||
|
|
||||||
res.on('data', function(chunk) {
|
|
||||||
body += chunk;
|
|
||||||
});
|
|
||||||
|
|
||||||
res.on('end', function() {
|
|
||||||
assert(/.*[.]localtunnel[.]me/.test(body), body);
|
|
||||||
done();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
req.end();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('request specific domain', function(done) {
|
|
||||||
localtunnel(test._fake_port, { subdomain: 'abcd' }, function(err, tunnel) {
|
|
||||||
assert.ifError(err);
|
|
||||||
assert.ok(new RegExp('^https:\/\/abcd.localtunnel.me' + '$').test(tunnel.url));
|
|
||||||
tunnel.close();
|
|
||||||
done();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
suite('--local-host localhost');
|
|
||||||
|
|
||||||
test('setup localtunnel client', function(done) {
|
|
||||||
var opt = {
|
|
||||||
local_host: 'localhost'
|
|
||||||
};
|
|
||||||
localtunnel(test._fake_port, opt, function(err, tunnel) {
|
|
||||||
assert.ifError(err);
|
|
||||||
assert.ok(new RegExp('^https:\/\/.*localtunnel.me' + '$').test(tunnel.url));
|
|
||||||
test._fake_url = tunnel.url;
|
|
||||||
done();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test('override Host header with local-host', function(done) {
|
|
||||||
var uri = test._fake_url;
|
|
||||||
var parsed = url.parse(uri);
|
|
||||||
|
|
||||||
var opt = {
|
|
||||||
host: parsed.host,
|
|
||||||
port: 443,
|
|
||||||
headers: {
|
|
||||||
host: parsed.hostname
|
|
||||||
},
|
|
||||||
path: '/'
|
|
||||||
};
|
|
||||||
|
|
||||||
var req = https.request(opt, function(res) {
|
|
||||||
res.setEncoding('utf8');
|
|
||||||
var body = '';
|
|
||||||
|
|
||||||
res.on('data', function(chunk) {
|
|
||||||
body += chunk;
|
|
||||||
});
|
|
||||||
|
|
||||||
res.on('end', function() {
|
|
||||||
assert.equal(body, 'localhost');
|
|
||||||
done();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
req.end();
|
|
||||||
});
|
|
||||||
|
|
||||||
suite('--local-host 127.0.0.1');
|
|
||||||
|
|
||||||
test('setup localtunnel client', function(done) {
|
|
||||||
var opt = {
|
|
||||||
local_host: '127.0.0.1'
|
|
||||||
};
|
|
||||||
localtunnel(test._fake_port, opt, function(err, tunnel) {
|
|
||||||
assert.ifError(err);
|
|
||||||
assert.ok(new RegExp('^https:\/\/.*localtunnel.me' + '$').test(tunnel.url));
|
|
||||||
test._fake_url = tunnel.url;
|
|
||||||
done();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test('override Host header with local-host', function(done) {
|
|
||||||
var uri = test._fake_url;
|
|
||||||
var parsed = url.parse(uri);
|
|
||||||
|
|
||||||
var opt = {
|
|
||||||
host: parsed.host,
|
|
||||||
port: 443,
|
|
||||||
headers: {
|
|
||||||
host: parsed.hostname
|
|
||||||
},
|
|
||||||
path: '/'
|
|
||||||
};
|
|
||||||
|
|
||||||
var req = https.request(opt, function(res) {
|
|
||||||
res.setEncoding('utf8');
|
|
||||||
var body = '';
|
|
||||||
|
|
||||||
res.on('data', function(chunk) {
|
|
||||||
body += chunk;
|
|
||||||
});
|
|
||||||
|
|
||||||
res.on('end', function() {
|
|
||||||
assert.equal(body, '127.0.0.1');
|
|
||||||
done();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
req.end();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('send chunked request', function(done) {
|
|
||||||
var uri = test._fake_url;
|
|
||||||
var parsed = url.parse(uri);
|
|
||||||
|
|
||||||
var opt = {
|
|
||||||
host: parsed.host,
|
|
||||||
port: 443,
|
|
||||||
headers: {
|
|
||||||
host: parsed.hostname,
|
|
||||||
'Transfer-Encoding': 'chunked'
|
|
||||||
},
|
|
||||||
path: '/'
|
|
||||||
};
|
|
||||||
|
|
||||||
var req = https.request(opt, function(res) {
|
|
||||||
res.setEncoding('utf8');
|
|
||||||
var body = '';
|
|
||||||
|
|
||||||
res.on('data', function(chunk) {
|
|
||||||
body += chunk;
|
|
||||||
});
|
|
||||||
|
|
||||||
res.on('end', function() {
|
|
||||||
assert.equal(body, '127.0.0.1');
|
|
||||||
done();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
req.end(require('crypto').randomBytes(1024 * 8).toString('base64'));
|
|
||||||
});
|
|
||||||
Reference in New Issue
Block a user