Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merge upstream changes #8

Merged
merged 16 commits into from
Feb 17, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
10 changes: 10 additions & 0 deletions .prettierrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"semi": true,
"printWidth": 100,
"tabWidth": 2,
"bracketSpacing": true,
"trailingComma": "es5",
"singleQuote": true,
"jsxBracketSameLine": false,
"arrowParens": "avoid"
}
22 changes: 16 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,9 @@ PORT=3000 lt

The localtunnel client is also usable through an API (for test integration, automation, etc)

### localtunnel({ port, ...options })
### 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`.
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('@chromaui/localtunnel');
Expand All @@ -59,11 +59,19 @@ const localtunnel = require('@chromaui/localtunnel');
})();
```

### options
#### options

- `port` [required] The local port number to expose through localtunnel.
- `subdomain` A _string_ value requesting a specific subdomain on the proxy server. **Note** You may not actually receive this name depending on availability.
- `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.
- `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

Expand All @@ -89,6 +97,8 @@ _go_ [gotunnelme](https://github.com/NoahShen/gotunnelme)

_go_ [go-localtunnel](https://github.com/localtunnel/go-localtunnel)

_C#/.NET_ [localtunnel-client](https://github.com/angelobreuer/localtunnel-client)

## server

See [localtunnel/server](//github.com/localtunnel/server) for details on the server that powers localtunnel.
Expand Down
53 changes: 41 additions & 12 deletions bin/lt.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
#!/usr/bin/env node
/* eslint-disable no-console */

const openurl = require('openurl');
const yargs = require('yargs');
Expand All @@ -9,6 +10,10 @@ 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',
Expand All @@ -22,35 +27,52 @@ const { argv } = yargs
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 url in your browser',
})
.option('p', {
alias: 'port',
describe: 'Internal http server port',
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();
// eslint-disable-next-line no-console
console.error('port must be a number');
console.error('\nInvalid argument: `port` must be a number');
process.exit(1);
}

(async () => {
const tunnel = await localtunnel({
host: argv.host,
port: argv.port,
local_host: argv['local-host'],
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;
});
Expand All @@ -59,16 +81,23 @@ if (typeof argv.port !== 'number') {
throw err;
});

// eslint-disable-next-line no-console
console.log('your url is: %s, your cachedUrl is %s', tunnel.url, tunnel.cachedUrl);
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 => {
// eslint-disable-next-line no-console
console.log(new Date().toString(), info.method, info.path);
});
}
Expand Down
69 changes: 43 additions & 26 deletions lib/Tunnel.js
Original file line number Diff line number Diff line change
@@ -1,32 +1,59 @@
/* eslint-disable consistent-return, no-underscore-dangle */

const url = require('url');
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 {
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';
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: cached_url || body.cached,
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}/`;
// optionally override the upstream server
const upstream = url.parse(opt.host);
// no subdomain at first, maybe use requested domain
const assignedDomain = opt.subdomain;
// where to quest
Expand All @@ -44,20 +71,7 @@ module.exports = class Tunnel extends EventEmitter {
);
return cb(err);
}
const maxConn = body.max_conn_count || 1;
cb(null, {
remote_host: upstream.hostname,
remote_ip: body.ip,
remote_port: body.port,
name: body.id,
url: body.url,
cachedUrl: body.cachedUrl,
max_conn: maxConn,
local_https: opt.https,
local_cert: opt.cert,
local_key: opt.key,
local_ca: opt.ca,
});
cb(null, getInfo(body));
})
.catch(err => {
debug(`tunnel server offline: ${err.message}, retry 1s`);
Expand All @@ -71,11 +85,7 @@ module.exports = class Tunnel extends EventEmitter {
// 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,
local_host: this.opts.local_host,
local_port: this.opts.port,
});
this.tunnelCluster = new TunnelCluster(info);

// only emit the url the first time
this.tunnelCluster.once('open', () => {
Expand Down Expand Up @@ -113,7 +123,9 @@ module.exports = class Tunnel extends EventEmitter {
this.tunnelCluster.on('dead', () => {
tunnelCount--;
debug('tunnel dead [total: %d]', tunnelCount);
if (this.closed) return;
if (this.closed) {
return;
}
this.tunnelCluster.open();
});

Expand All @@ -135,7 +147,12 @@ module.exports = class Tunnel extends EventEmitter {

this.clientId = info.name;
this.url = info.url;
this.cachedUrl = info.cachedUrl;

// `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();
});
Expand Down
13 changes: 9 additions & 4 deletions lib/TunnelCluster.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@ const tls = require('tls');
const HeaderHostTransformer = require('./HeaderHostTransformer');

// manages groups of tunnels
module.exports = class TunnelCluster extends EventEmitter {
module.exports = class TunnelCluster extends (
EventEmitter
) {
constructor(opts = {}) {
super(opts);
this.opts = opts;
Expand All @@ -16,12 +18,14 @@ module.exports = class TunnelCluster extends EventEmitter {
open() {
const opt = this.opts;

// Preference IP if returned by the server
// 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 || process.env.NODE_TLS_REJECT_UNAUTHORIZED === '0';

debug(
'establishing tunnel %s://%s:%s <> %s:%s',
Expand Down Expand Up @@ -67,8 +71,9 @@ module.exports = class TunnelCluster extends EventEmitter {
debug('connecting locally to %s://%s:%d', localProtocol, localHost, localPort);
remote.pause();

const allowInvalidCert = process.env.NODE_TLS_REJECT_UNAUTHORIZED === '0';
if (allowInvalidCert) debug('allowing invalid certificates');
if (allowInvalidCert) {
debug('allowing invalid certificates');
}

const getLocalCertOpts = () =>
allowInvalidCert
Expand Down