mirror of
https://github.com/bitinflow/localtunnel.git
synced 2026-03-14 05:55:53 +00:00
Compare commits
127 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
5340659954 | ||
|
|
5c6558ed91 | ||
|
|
79ca069c38 | ||
|
|
741db27084 | ||
|
|
b605e9b823 | ||
|
|
b5830c3840 | ||
|
|
06b85ad0aa | ||
|
|
943a7dc35b |
1
.npmignore
Normal file
1
.npmignore
Normal file
@@ -0,0 +1 @@
|
||||
support
|
||||
@@ -1,3 +1,6 @@
|
||||
language: node_js
|
||||
node_js:
|
||||
- 0.8
|
||||
- "4"
|
||||
- "6"
|
||||
- "8"
|
||||
- "9"
|
||||
|
||||
57
History.md
Normal file
57
History.md
Normal file
@@ -0,0 +1,57 @@
|
||||
# 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.
|
||||
88
README.md
88
README.md
@@ -1,8 +1,10 @@
|
||||
# localtunnel [](http://travis-ci.org/shtylman/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.
|
||||
|
||||
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 ##
|
||||
|
||||
@@ -10,9 +12,11 @@ Great for working with browser testing tools like browserling or external api ca
|
||||
npm install -g localtunnel
|
||||
```
|
||||
|
||||
This will install the localtunnel module globally and add the 'lt' client cli tool to your PATH.
|
||||
|
||||
## use ##
|
||||
|
||||
Super East! Assuming your local server is running on port 8000, just use the ```lt``` command to start the tunnel.
|
||||
Assuming your local server is running on port 8000, just use the ```lt``` command to start the tunnel.
|
||||
|
||||
```
|
||||
lt --port 8000
|
||||
@@ -22,27 +26,75 @@ Thats it! It will connect to the tunnel server, setup the tunnel, and tell you w
|
||||
|
||||
You can restart your local server all you want, ```lt``` is smart enough to detect this and reconnect once it is back.
|
||||
|
||||
### 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)
|
||||
* `--local-host` proxy to a hostname other than localhost
|
||||
|
||||
You may also specify arguments via env variables. E.x.
|
||||
|
||||
```
|
||||
PORT=3000 lt
|
||||
```
|
||||
|
||||
## API ##
|
||||
|
||||
The localtunnel client is also usable through an API (test integration, automation, etc)
|
||||
The localtunnel client is also usable through an API (for test integration, automation, etc)
|
||||
|
||||
### localtunnel(port [,opts], fn)
|
||||
|
||||
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`.
|
||||
|
||||
```javascript
|
||||
var lt_client = require('localtunnel').client;
|
||||
var localtunnel = require('localtunnel');
|
||||
|
||||
var client = lt_client.connect({
|
||||
// the localtunnel server
|
||||
host: 'http://localtunnel.com',
|
||||
// your local application port
|
||||
port: 12345
|
||||
var tunnel = localtunnel(port, function(err, tunnel) {
|
||||
if (err) ...
|
||||
|
||||
// the assigned public url for your tunnel
|
||||
// i.e. https://abcdefgjhij.localtunnel.me
|
||||
tunnel.url;
|
||||
});
|
||||
|
||||
// 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!
|
||||
tunnel.on('close', function() {
|
||||
// tunnels are closed
|
||||
});
|
||||
```
|
||||
|
||||
### opts
|
||||
|
||||
* `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.
|
||||
|
||||
### 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
|
||||
|
||||
77
bin/client
77
bin/client
@@ -1,34 +1,75 @@
|
||||
#!/usr/bin/env node
|
||||
var lt_client = require(__dirname + '/../client');
|
||||
var lt_client = require('../client');
|
||||
var open_url = require('openurl');
|
||||
|
||||
var argv = require('optimist')
|
||||
.usage('Usage: $0 --port [num]')
|
||||
.demand(['port'])
|
||||
.options('host', {
|
||||
default: 'http://localtunnel.me',
|
||||
describe: 'upstream server providing forwarding'
|
||||
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',
|
||||
})
|
||||
.options('subdomain', {
|
||||
describe: 'request this subdomain'
|
||||
.option('s', {
|
||||
alias: 'subdomain',
|
||||
describe: 'Request this subdomain'
|
||||
})
|
||||
.describe('port', 'internal http server port')
|
||||
.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,
|
||||
}
|
||||
};
|
||||
|
||||
var client = lt_client.connect(opt);
|
||||
const PrintRequests = argv['print-requests'];
|
||||
|
||||
// only emitted when the url changes
|
||||
client.on('url', function(url) {
|
||||
console.log('your url is: %s', url);
|
||||
});
|
||||
lt_client(opt.port, opt, function(err, tunnel) {
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
|
||||
client.on('error', function(err) {
|
||||
console.error(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
|
||||
|
||||
33
bin/server
33
bin/server
@@ -1,33 +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');
|
||||
|
||||
server.listen(argv.port, function() {
|
||||
log.info('server listening on port: %d', server.address().port);
|
||||
});
|
||||
|
||||
// vim: ft=javascript
|
||||
|
||||
143
client.js
143
client.js
@@ -1,133 +1,24 @@
|
||||
// builtin
|
||||
var net = require('net');
|
||||
var url = require('url');
|
||||
var request = require('request');
|
||||
var EventEmitter = require('events').EventEmitter;
|
||||
var debug = require('debug')('localtunnel:client');
|
||||
|
||||
// request upstream url and connection info
|
||||
var request_url = function(params, cb) {
|
||||
request(params, function(err, res, body) {
|
||||
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) {
|
||||
cb(err);
|
||||
return fn(err);
|
||||
}
|
||||
|
||||
cb(null, body);
|
||||
fn(null, client);
|
||||
});
|
||||
return client;
|
||||
};
|
||||
|
||||
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, max_conn) {
|
||||
var count = 0;
|
||||
|
||||
// open 5 connections to the localtunnel server
|
||||
// allows for resources to be served faster
|
||||
for (var count = 0 ; count < max_conn ; ++count) {
|
||||
var upstream = duplex(remote_host, remote_port, 'localhost', local_port);
|
||||
upstream.once('end', function() {
|
||||
// all upstream connections have been closed
|
||||
if (--count <= 0) {
|
||||
tunnel(remote_host, remote_port, max_conn);
|
||||
}
|
||||
});
|
||||
|
||||
upstream.on('error', function(err) {
|
||||
console.error(err);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
var params = {
|
||||
path: '/',
|
||||
json: true
|
||||
};
|
||||
|
||||
// where to quest
|
||||
params.uri = base_uri + ((assigned_domain) ? assigned_domain : '?new');
|
||||
|
||||
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;
|
||||
|
||||
tunnel(host, port, body.max_conn_count || 1);
|
||||
|
||||
ev.emit('url', body.url);
|
||||
});
|
||||
|
||||
return ev;
|
||||
};
|
||||
|
||||
var duplex = function(remote_host, remote_port, local_host, local_port) {
|
||||
var ev = new EventEmitter();
|
||||
|
||||
// connect to remote tcp server
|
||||
var upstream = net.createConnection(remote_port, remote_host);
|
||||
var internal;
|
||||
|
||||
// when upstream connection is closed, close other associated connections
|
||||
upstream.once('end', function() {
|
||||
ev.emit('error', new Error('upstream connection terminated'));
|
||||
|
||||
// sever connection to internal server
|
||||
// on reconnect we will re-establish
|
||||
internal.end();
|
||||
|
||||
ev.emit('end');
|
||||
});
|
||||
|
||||
upstream.on('error', function(err) {
|
||||
ev.emit('error', err);
|
||||
});
|
||||
|
||||
(function connect_internal() {
|
||||
|
||||
internal = net.createConnection(local_port, local_host);
|
||||
internal.on('error', function() {
|
||||
ev.emit('error', new Error('error connecting to local server. retrying in 1s'));
|
||||
setTimeout(function() {
|
||||
connect_internal();
|
||||
}, 1000);
|
||||
});
|
||||
|
||||
internal.on('end', function() {
|
||||
ev.emit('error', new Error('disconnected from local server. retrying in 1s'));
|
||||
setTimeout(function() {
|
||||
connect_internal();
|
||||
}, 1000);
|
||||
});
|
||||
|
||||
upstream.pipe(internal).pipe(upstream);
|
||||
})();
|
||||
|
||||
return ev;
|
||||
}
|
||||
|
||||
module.exports.connect = connect;
|
||||
|
||||
|
||||
39
lib/HeaderHostTransformer.js
Normal file
39
lib/HeaderHostTransformer.js
Normal file
@@ -0,0 +1,39 @@
|
||||
var stream = require('stream');
|
||||
var util = require('util');
|
||||
|
||||
var Transform = stream.Transform;
|
||||
|
||||
var HeaderHostTransformer = function(opts) {
|
||||
if (!(this instanceof HeaderHostTransformer)) {
|
||||
return new HeaderHostTransformer(opts);
|
||||
}
|
||||
|
||||
opts = opts || {}
|
||||
Transform.call(this, opts);
|
||||
|
||||
var self = this;
|
||||
self.host = opts.host || 'localhost';
|
||||
self.replaced = false;
|
||||
}
|
||||
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;
|
||||
158
lib/Tunnel.js
Normal file
158
lib/Tunnel.js
Normal file
@@ -0,0 +1,158 @@
|
||||
var url = require('url');
|
||||
var EventEmitter = require('events').EventEmitter;
|
||||
var axios = require('axios');
|
||||
var debug = require('debug')('localtunnel:client');
|
||||
|
||||
var TunnelCluster = require('./TunnelCluster');
|
||||
|
||||
var Tunnel = function(opt) {
|
||||
if (!(this instanceof Tunnel)) {
|
||||
return new Tunnel(opt);
|
||||
}
|
||||
|
||||
var self = this;
|
||||
self._closed = false;
|
||||
self._opt = opt || {};
|
||||
|
||||
self._opt.host = self._opt.host || 'https://localtunnel.me';
|
||||
};
|
||||
|
||||
Tunnel.prototype.__proto__ = EventEmitter.prototype;
|
||||
|
||||
// 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 + '/';
|
||||
|
||||
// optionally override the upstream server
|
||||
var upstream = url.parse(opt.host);
|
||||
|
||||
// no subdomain at first, maybe use requested domain
|
||||
var assigned_domain = opt.subdomain;
|
||||
|
||||
// where to quest
|
||||
var uri = base_uri + ((assigned_domain) ? assigned_domain : '?new');
|
||||
|
||||
(function get_url() {
|
||||
axios.get(uri, params)
|
||||
.then(function(res){
|
||||
var body = res.data;
|
||||
if (res.status !== 200) {
|
||||
var err = new Error((body && body.message) || 'localtunnel server returned an error, please try again');
|
||||
return cb(err);
|
||||
}
|
||||
var port = body.port;
|
||||
var host = upstream.hostname;
|
||||
var max_conn = body.max_conn_count || 1;
|
||||
cb(null, {
|
||||
remote_host: upstream.hostname,
|
||||
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;
|
||||
|
||||
// increase max event listeners so that localtunnel consumers don't get
|
||||
// warning messages as soon as they setup even one listener. See #71
|
||||
self.setMaxListeners(info.max_conn + (EventEmitter.defaultMaxListeners || 10));
|
||||
|
||||
info.local_host = opt.local_host;
|
||||
info.local_port = opt.port;
|
||||
|
||||
var tunnels = self.tunnel_cluster = TunnelCluster(info);
|
||||
|
||||
// only emit the url the first time
|
||||
tunnels.once('open', function() {
|
||||
self.emit('url', info.url);
|
||||
});
|
||||
|
||||
// re-emit socket error
|
||||
tunnels.on('error', function(err) {
|
||||
self.emit('error', err);
|
||||
});
|
||||
|
||||
var tunnel_count = 0;
|
||||
|
||||
// track open count
|
||||
tunnels.on('open', function(tunnel) {
|
||||
tunnel_count++;
|
||||
debug('tunnel open [total: %d]', tunnel_count);
|
||||
|
||||
var close_handler = function() {
|
||||
tunnel.destroy();
|
||||
};
|
||||
|
||||
if (self._closed) {
|
||||
return close_handler();
|
||||
}
|
||||
|
||||
self.once('close', close_handler);
|
||||
tunnel.once('close', function() {
|
||||
self.removeListener('close', close_handler);
|
||||
});
|
||||
});
|
||||
|
||||
// when a tunnel dies, open a new one
|
||||
tunnels.on('dead', function(tunnel) {
|
||||
tunnel_count--;
|
||||
debug('tunnel dead [total: %d]', tunnel_count);
|
||||
|
||||
if (self._closed) {
|
||||
return;
|
||||
}
|
||||
|
||||
tunnels.open();
|
||||
});
|
||||
|
||||
tunnels.on('request', function(info) {
|
||||
self.emit('request', info);
|
||||
});
|
||||
|
||||
// establish as many tunnels as allowed
|
||||
for (var count = 0 ; count < info.max_conn ; ++count) {
|
||||
tunnels.open();
|
||||
}
|
||||
};
|
||||
|
||||
Tunnel.prototype.open = function(cb) {
|
||||
var self = this;
|
||||
|
||||
self._init(function(err, info) {
|
||||
if (err) {
|
||||
return cb(err);
|
||||
}
|
||||
|
||||
self.url = info.url;
|
||||
self._establish(info);
|
||||
cb();
|
||||
});
|
||||
};
|
||||
|
||||
// shutdown tunnels
|
||||
Tunnel.prototype.close = function() {
|
||||
var self = this;
|
||||
|
||||
self._closed = true;
|
||||
self.emit('close');
|
||||
};
|
||||
|
||||
module.exports = Tunnel;
|
||||
133
lib/TunnelCluster.js
Normal file
133
lib/TunnelCluster.js
Normal file
@@ -0,0 +1,133 @@
|
||||
var EventEmitter = require('events').EventEmitter;
|
||||
var debug = require('debug')('localtunnel:client');
|
||||
var net = require('net');
|
||||
|
||||
var HeaderHostTransformer = require('./HeaderHostTransformer');
|
||||
|
||||
// manages groups of tunnels
|
||||
var TunnelCluster = function(opt) {
|
||||
if (!(this instanceof TunnelCluster)) {
|
||||
return new TunnelCluster(opt);
|
||||
}
|
||||
|
||||
var self = this;
|
||||
self._opt = opt;
|
||||
|
||||
EventEmitter.call(self);
|
||||
};
|
||||
|
||||
TunnelCluster.prototype.__proto__ = EventEmitter.prototype;
|
||||
|
||||
// establish a new tunnel
|
||||
TunnelCluster.prototype.open = function() {
|
||||
var self = this;
|
||||
|
||||
var opt = self._opt || {};
|
||||
|
||||
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
|
||||
var remote = net.connect({
|
||||
host: remote_host,
|
||||
port: remote_port
|
||||
});
|
||||
|
||||
remote.setKeepAlive(true);
|
||||
|
||||
remote.on('error', function(err) {
|
||||
// emit connection refused errors immediately, because they
|
||||
// indicate that the tunnel can't be established.
|
||||
if (err.code === 'ECONNREFUSED') {
|
||||
self.emit('error', new Error('connection refused: ' + remote_host + ':' + remote_port + ' (check your firewall settings)'));
|
||||
}
|
||||
|
||||
remote.end();
|
||||
});
|
||||
|
||||
function conn_local() {
|
||||
if (remote.destroyed) {
|
||||
debug('remote destroyed');
|
||||
self.emit('dead');
|
||||
return;
|
||||
}
|
||||
|
||||
debug('connecting locally to %s:%d', local_host, local_port);
|
||||
remote.pause();
|
||||
|
||||
// connection to local http server
|
||||
var local = net.connect({
|
||||
host: local_host,
|
||||
port: local_port
|
||||
});
|
||||
|
||||
function remote_close() {
|
||||
debug('remote close');
|
||||
self.emit('dead');
|
||||
local.end();
|
||||
};
|
||||
|
||||
remote.once('close', remote_close);
|
||||
|
||||
// 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', function(err) {
|
||||
debug('local error %s', err.message);
|
||||
local.end();
|
||||
|
||||
remote.removeListener('close', remote_close);
|
||||
|
||||
if (err.code !== 'ECONNREFUSED') {
|
||||
return remote.end();
|
||||
}
|
||||
|
||||
// retrying connection to local server
|
||||
setTimeout(conn_local, 1000);
|
||||
});
|
||||
|
||||
local.once('connect', function() {
|
||||
debug('connected locally');
|
||||
remote.resume();
|
||||
|
||||
var 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(HeaderHostTransformer({ host: opt.local_host }));
|
||||
}
|
||||
|
||||
stream.pipe(local).pipe(remote);
|
||||
|
||||
// when local closes, also get a new remote
|
||||
local.once('close', function(had_error) {
|
||||
debug('local connection closed [%s]', had_error);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
remote.on('data', function(data) {
|
||||
const match = data.toString().match(/^(\w+) (\S+)/);
|
||||
if (match) {
|
||||
self.emit('request', {
|
||||
method: match[1],
|
||||
path: match[2],
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// tunnel is considered open when remote connects
|
||||
remote.once('connect', function() {
|
||||
self.emit('open', remote);
|
||||
conn_local();
|
||||
});
|
||||
};
|
||||
|
||||
module.exports = TunnelCluster;
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
26
package.json
26
package.json
@@ -2,28 +2,26 @@
|
||||
"author": "Roman Shtylman <shtylman@gmail.com>",
|
||||
"name": "localtunnel",
|
||||
"description": "expose localhost to the world",
|
||||
"version": "0.0.2",
|
||||
"version": "1.9.0",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git://github.com/shtylman/localtunnel.git"
|
||||
"url": "git://github.com/localtunnel/localtunnel.git"
|
||||
},
|
||||
"dependencies": {
|
||||
"request": "2.11.4",
|
||||
"book": "1.2.0",
|
||||
"optimist": "0.3.4"
|
||||
"axios": "0.17.1",
|
||||
"debug": "2.6.8",
|
||||
"openurl": "1.1.1",
|
||||
"yargs": "6.6.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"mocha": "1.6.0"
|
||||
},
|
||||
"optionalDependencies": {},
|
||||
"engines": {
|
||||
"node": "*"
|
||||
"mocha": "~1.17.0"
|
||||
},
|
||||
"scripts": {
|
||||
"test": "mocha --ui qunit -- test",
|
||||
"start": "./bin/server"
|
||||
"test": "mocha --ui qunit --reporter list --timeout 10000 -- test/index.js"
|
||||
},
|
||||
"bin": {
|
||||
"lt": "./bin/client"
|
||||
}
|
||||
}
|
||||
},
|
||||
"main": "./client.js"
|
||||
}
|
||||
390
server.js
390
server.js
@@ -1,390 +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;
|
||||
var ServerResponse = http.ServerResponse;
|
||||
var IncomingMessage = http.IncomingMessage;
|
||||
|
||||
// vendor
|
||||
var log = require('book');
|
||||
|
||||
// local
|
||||
var rand_id = require('./lib/rand_id');
|
||||
|
||||
var server = http.createServer();
|
||||
|
||||
// id -> client http server
|
||||
var clients = {};
|
||||
|
||||
// available parsers
|
||||
var parsers = http.parsers;
|
||||
|
||||
// data going back to a client (the last client that made a request)
|
||||
function socketOnData(d, start, end) {
|
||||
|
||||
var socket = this;
|
||||
var req = this._httpMessage;
|
||||
|
||||
var response_socket = socket.respond_socket;
|
||||
if (!response_socket) {
|
||||
log.error('no response socket assigned for http response from backend');
|
||||
return;
|
||||
}
|
||||
|
||||
// pass the response from our client back to the requesting socket
|
||||
response_socket.write(d.slice(start, end));
|
||||
|
||||
if (socket.for_websocket) {
|
||||
return;
|
||||
}
|
||||
|
||||
// invoke parsing so we know when the response is complete
|
||||
var parser = response_socket.out_parser;
|
||||
parser.socket = socket;
|
||||
|
||||
var ret = parser.execute(d, start, end - start);
|
||||
if (ret instanceof Error) {
|
||||
log.error(ret);
|
||||
freeParser(parser, req);
|
||||
socket.destroy(ret);
|
||||
}
|
||||
}
|
||||
|
||||
function freeParser(parser, req) {
|
||||
if (parser) {
|
||||
parser._headers = [];
|
||||
parser.onIncoming = null;
|
||||
if (parser.socket) {
|
||||
parser.socket.onend = null;
|
||||
parser.socket.ondata = null;
|
||||
parser.socket.parser = null;
|
||||
}
|
||||
parser.socket = null;
|
||||
parser.incoming = null;
|
||||
parsers.free(parser);
|
||||
parser = null;
|
||||
}
|
||||
if (req) {
|
||||
req.parser = null;
|
||||
}
|
||||
}
|
||||
|
||||
// single http connection
|
||||
// gets a single http response back
|
||||
server.on('connection', function(socket) {
|
||||
|
||||
var self = this;
|
||||
|
||||
// parser handles incoming requests for the socket
|
||||
// the request is what lets us know if we proxy or not
|
||||
var parser = parsers.alloc();
|
||||
parser.socket = socket;
|
||||
parser.reinitialize(HTTPParser.REQUEST);
|
||||
|
||||
function our_request(req) {
|
||||
var res = new ServerResponse(req);
|
||||
res.assignSocket(socket);
|
||||
self.emit('request', req, res);
|
||||
return;
|
||||
}
|
||||
|
||||
// a full request is complete
|
||||
// we wait for the response from the server
|
||||
parser.onIncoming = function(req, shouldKeepAlive) {
|
||||
|
||||
log.trace('request', req.url);
|
||||
|
||||
// default is that the data is not for the client
|
||||
delete parser.sock;
|
||||
delete parser.buffer;
|
||||
delete parser.client;
|
||||
|
||||
var hostname = req.headers.host;
|
||||
if (!hostname) {
|
||||
log.trace('no hostname: %j', req.headers);
|
||||
return our_request(req);
|
||||
}
|
||||
|
||||
var match = hostname.match(/^([a-z]{4})[.].*/);
|
||||
if (!match) {
|
||||
return our_request(req);
|
||||
}
|
||||
|
||||
var client_id = match[1];
|
||||
var client = clients[client_id];
|
||||
|
||||
// requesting a subdomain that doesn't exist
|
||||
if (!client) {
|
||||
return socket.end();
|
||||
}
|
||||
|
||||
parser.client = client;
|
||||
|
||||
// assigned socket for the client
|
||||
var sock = client.sockets.shift();
|
||||
|
||||
// no free sockets, queue
|
||||
if (!sock) {
|
||||
parser.buffer = true;
|
||||
return;
|
||||
}
|
||||
|
||||
// for tcp proxying
|
||||
parser.sock = sock;
|
||||
|
||||
// set who we will respond back to
|
||||
sock.respond_socket = socket;
|
||||
|
||||
var out_parser = parsers.alloc();
|
||||
out_parser.reinitialize(HTTPParser.RESPONSE);
|
||||
socket.out_parser = out_parser;
|
||||
|
||||
// we have completed a response
|
||||
// the tcp socket is free again
|
||||
out_parser.onIncoming = function (res) {
|
||||
res.on('end', function() {
|
||||
log.trace('done with response for: %s', req.url);
|
||||
|
||||
// done with the parser
|
||||
parsers.free(out_parser);
|
||||
|
||||
// unset the response
|
||||
delete sock.respond_socket;
|
||||
|
||||
var next = client.waiting.shift();
|
||||
if (!next) {
|
||||
// return socket to available
|
||||
client.sockets.push(sock);
|
||||
return;
|
||||
}
|
||||
|
||||
// reuse avail socket for next connection
|
||||
sock.respond_socket = next;
|
||||
|
||||
// needed to know when this response will be done
|
||||
out_parser.reinitialize(HTTPParser.RESPONSE);
|
||||
next.out_parser = out_parser;
|
||||
|
||||
// write original bytes we held cause we were busy
|
||||
sock.write(next.queue);
|
||||
|
||||
// continue with other bytes
|
||||
next.resume();
|
||||
|
||||
return;
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
// process new data on the client socket
|
||||
// we may need to forward this it the backend
|
||||
socket.ondata = function(d, start, end) {
|
||||
|
||||
// run through request parser to determine if we should pass to tcp
|
||||
// onIncoming will be run before this returns
|
||||
var ret = parser.execute(d, start, end - start);
|
||||
|
||||
// invalid request from the user
|
||||
if (ret instanceof Error) {
|
||||
log.error(ret);
|
||||
socket.destroy(ret);
|
||||
return;
|
||||
}
|
||||
|
||||
// websocket stuff
|
||||
if (parser.incoming && parser.incoming.upgrade) {
|
||||
log.trace('upgrade request');
|
||||
|
||||
parser.finish();
|
||||
|
||||
var hostname = parser.incoming.headers.host;
|
||||
|
||||
var match = hostname.match(/^([a-z]{4})[.].*/);
|
||||
if (!match) {
|
||||
return our_request(req);
|
||||
}
|
||||
|
||||
var client_id = match[1];
|
||||
var client = clients[client_id];
|
||||
|
||||
var sock = client.sockets.shift();
|
||||
sock.respond_socket = socket;
|
||||
sock.for_websocket = true;
|
||||
|
||||
socket.ondata = function(d, start, end) {
|
||||
sock.write(d.slice(start, end));
|
||||
};
|
||||
|
||||
socket.end = function() {
|
||||
log.trace('websocket end');
|
||||
|
||||
delete sock.respond_socket;
|
||||
client.sockets.push(sock);
|
||||
}
|
||||
|
||||
sock.write(d.slice(start, end));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// if no available socket, buffer the request for later
|
||||
if (parser.buffer) {
|
||||
|
||||
// pause any further data on this socket
|
||||
socket.pause();
|
||||
|
||||
// copy the current data since we have already received it
|
||||
var copy = Buffer(end - start);
|
||||
d.copy(copy, 0, start, end);
|
||||
socket.queue = copy;
|
||||
|
||||
// add socket to queue
|
||||
parser.client.waiting.push(socket);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (!parser.sock) {
|
||||
return;
|
||||
}
|
||||
|
||||
// assert, respond socket should be set
|
||||
|
||||
// send through tcp tunnel
|
||||
// responses will go back to the respond_socket
|
||||
parser.sock.write(d.slice(start, end));
|
||||
};
|
||||
|
||||
socket.onend = function() {
|
||||
var ret = parser.finish();
|
||||
|
||||
if (ret instanceof Error) {
|
||||
log.error(ret);
|
||||
socket.destroy(ret);
|
||||
return;
|
||||
}
|
||||
|
||||
socket.end();
|
||||
};
|
||||
|
||||
socket.on('close', function() {
|
||||
parsers.free(parser);
|
||||
});
|
||||
});
|
||||
|
||||
server.on('request', function(req, res) {
|
||||
|
||||
// ignore favicon
|
||||
if (req.url === '/favicon.ico') {
|
||||
res.writeHead(404);
|
||||
return res.end();
|
||||
}
|
||||
|
||||
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();
|
||||
|
||||
// maximum number of tcp connections the client can setup
|
||||
// each tcp channel allows for more parallel requests
|
||||
var max_tcp_sockets = 4;
|
||||
|
||||
// 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
|
||||
}));
|
||||
});
|
||||
|
||||
// user has 5 seconds to connect before their slot is given up
|
||||
var conn_timeout = setTimeout(function() {
|
||||
client_server.close();
|
||||
}, 5000);
|
||||
|
||||
// no longer accepting connections for this id
|
||||
client_server.on('close', function() {
|
||||
delete clients[id];
|
||||
});
|
||||
|
||||
var count = 0;
|
||||
client_server.on('connection', function(socket) {
|
||||
|
||||
// no more socket connections allowed
|
||||
if (count++ >= max_tcp_sockets) {
|
||||
return socket.end();
|
||||
}
|
||||
|
||||
log.trace('new connection for id: %s', id);
|
||||
|
||||
// multiplexes socket data out to clients
|
||||
socket.ondata = socketOnData;
|
||||
|
||||
// no need to close the client server
|
||||
clearTimeout(conn_timeout);
|
||||
|
||||
// add socket to pool for this id
|
||||
var idx = client.sockets.push(socket) - 1;
|
||||
|
||||
socket.on('close', function(had_error) {
|
||||
count--;
|
||||
client.sockets.splice(idx, 1);
|
||||
|
||||
// no more sockets for this ident
|
||||
if (client.sockets.length === 0) {
|
||||
delete clients[id];
|
||||
}
|
||||
});
|
||||
|
||||
// close will be emitted after this
|
||||
socket.on('error', function(err) {
|
||||
log.error(err);
|
||||
});
|
||||
});
|
||||
|
||||
client_server.on('error', function(err) {
|
||||
log.error(err);
|
||||
});
|
||||
});
|
||||
|
||||
module.exports = server;
|
||||
|
||||
95
test.js
95
test.js
@@ -1,95 +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);
|
||||
});
|
||||
});
|
||||
|
||||
188
test/index.js
Normal file
188
test/index.js
Normal file
@@ -0,0 +1,188 @@
|
||||
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'));
|
||||
});
|
||||
204
yarn.lock
Normal file
204
yarn.lock
Normal file
@@ -0,0 +1,204 @@
|
||||
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
|
||||
# yarn lockfile v1
|
||||
|
||||
|
||||
ansi-regex@^2.0.0:
|
||||
version "2.1.1"
|
||||
resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-2.1.1.tgz#c3b33ab5ee360d86e0e628f0468ae7ef27d654df"
|
||||
|
||||
axios@0.17.1:
|
||||
version "0.17.1"
|
||||
resolved "https://registry.yarnpkg.com/axios/-/axios-0.17.1.tgz#2d8e3e5d0bdbd7327f91bc814f5c57660f81824d"
|
||||
dependencies:
|
||||
follow-redirects "^1.2.5"
|
||||
is-buffer "^1.1.5"
|
||||
|
||||
camelcase@^1.2.1:
|
||||
version "1.2.1"
|
||||
resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-1.2.1.tgz#9bb5304d2e0b56698b2c758b08a3eaa9daa58a39"
|
||||
|
||||
cliui@^3.0.3:
|
||||
version "3.2.0"
|
||||
resolved "https://registry.yarnpkg.com/cliui/-/cliui-3.2.0.tgz#120601537a916d29940f934da3b48d585a39213d"
|
||||
dependencies:
|
||||
string-width "^1.0.1"
|
||||
strip-ansi "^3.0.1"
|
||||
wrap-ansi "^2.0.0"
|
||||
|
||||
code-point-at@^1.0.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77"
|
||||
|
||||
commander@0.6.1:
|
||||
version "0.6.1"
|
||||
resolved "https://registry.yarnpkg.com/commander/-/commander-0.6.1.tgz#fa68a14f6a945d54dbbe50d8cdb3320e9e3b1a06"
|
||||
|
||||
commander@2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/commander/-/commander-2.0.0.tgz#d1b86f901f8b64bd941bdeadaf924530393be928"
|
||||
|
||||
debug@*, debug@^3.1.0:
|
||||
version "3.1.0"
|
||||
resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261"
|
||||
dependencies:
|
||||
ms "2.0.0"
|
||||
|
||||
debug@2.6.8:
|
||||
version "2.6.8"
|
||||
resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.8.tgz#e731531ca2ede27d188222427da17821d68ff4fc"
|
||||
dependencies:
|
||||
ms "2.0.0"
|
||||
|
||||
decamelize@^1.0.0:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290"
|
||||
|
||||
diff@1.0.7:
|
||||
version "1.0.7"
|
||||
resolved "https://registry.yarnpkg.com/diff/-/diff-1.0.7.tgz#24bbb001c4a7d5522169e7cabdb2c2814ed91cf4"
|
||||
|
||||
follow-redirects@^1.2.5:
|
||||
version "1.4.1"
|
||||
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.4.1.tgz#d8120f4518190f55aac65bb6fc7b85fcd666d6aa"
|
||||
dependencies:
|
||||
debug "^3.1.0"
|
||||
|
||||
glob@3.2.3:
|
||||
version "3.2.3"
|
||||
resolved "https://registry.yarnpkg.com/glob/-/glob-3.2.3.tgz#e313eeb249c7affaa5c475286b0e115b59839467"
|
||||
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"
|
||||
|
||||
growl@1.7.x:
|
||||
version "1.7.0"
|
||||
resolved "https://registry.yarnpkg.com/growl/-/growl-1.7.0.tgz#de2d66136d002e112ba70f3f10c31cf7c350b2da"
|
||||
|
||||
inherits@2:
|
||||
version "2.0.3"
|
||||
resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de"
|
||||
|
||||
invert-kv@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/invert-kv/-/invert-kv-1.0.0.tgz#104a8e4aaca6d3d8cd157a8ef8bfab2d7a3ffdb6"
|
||||
|
||||
is-buffer@^1.1.5:
|
||||
version "1.1.6"
|
||||
resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be"
|
||||
|
||||
is-fullwidth-code-point@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz#ef9e31386f031a7f0d643af82fde50c457ef00cb"
|
||||
dependencies:
|
||||
number-is-nan "^1.0.0"
|
||||
|
||||
jade@0.26.3:
|
||||
version "0.26.3"
|
||||
resolved "https://registry.yarnpkg.com/jade/-/jade-0.26.3.tgz#8f10d7977d8d79f2f6ff862a81b0513ccb25686c"
|
||||
dependencies:
|
||||
commander "0.6.1"
|
||||
mkdirp "0.3.0"
|
||||
|
||||
lcid@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/lcid/-/lcid-1.0.0.tgz#308accafa0bc483a3867b4b6f2b9506251d1b835"
|
||||
dependencies:
|
||||
invert-kv "^1.0.0"
|
||||
|
||||
lru-cache@2:
|
||||
version "2.7.3"
|
||||
resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-2.7.3.tgz#6d4524e8b955f95d4f5b58851ce21dd72fb4e952"
|
||||
|
||||
minimatch@~0.2.11:
|
||||
version "0.2.14"
|
||||
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-0.2.14.tgz#c74e780574f63c6f9a090e90efbe6ef53a6a756a"
|
||||
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"
|
||||
|
||||
mkdirp@0.3.5:
|
||||
version "0.3.5"
|
||||
resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.3.5.tgz#de3e5f8961c88c787ee1368df849ac4413eca8d7"
|
||||
|
||||
mocha@~1.17.0:
|
||||
version "1.17.1"
|
||||
resolved "https://registry.yarnpkg.com/mocha/-/mocha-1.17.1.tgz#7f7671d68526d074b7bae660c9099f87e0ea1ccb"
|
||||
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"
|
||||
|
||||
number-is-nan@^1.0.0:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/number-is-nan/-/number-is-nan-1.0.1.tgz#097b602b53422a522c1afb8790318336941a011d"
|
||||
|
||||
openurl@1.1.1:
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/openurl/-/openurl-1.1.1.tgz#3875b4b0ef7a52c156f0db41d4609dbb0f94b387"
|
||||
|
||||
os-locale@^1.4.0:
|
||||
version "1.4.0"
|
||||
resolved "https://registry.yarnpkg.com/os-locale/-/os-locale-1.4.0.tgz#20f9f17ae29ed345e8bde583b13d2009803c14d9"
|
||||
dependencies:
|
||||
lcid "^1.0.0"
|
||||
|
||||
sigmund@~1.0.0:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/sigmund/-/sigmund-1.0.1.tgz#3ff21f198cad2175f9f3b781853fd94d0d19b590"
|
||||
|
||||
string-width@^1.0.1:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/string-width/-/string-width-1.0.2.tgz#118bdf5b8cdc51a2a7e70d211e07e2b0b9b107d3"
|
||||
dependencies:
|
||||
code-point-at "^1.0.0"
|
||||
is-fullwidth-code-point "^1.0.0"
|
||||
strip-ansi "^3.0.0"
|
||||
|
||||
strip-ansi@^3.0.0, strip-ansi@^3.0.1:
|
||||
version "3.0.1"
|
||||
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-3.0.1.tgz#6a385fb8853d952d5ff05d0e8aaf94278dc63dcf"
|
||||
dependencies:
|
||||
ansi-regex "^2.0.0"
|
||||
|
||||
window-size@^0.1.2:
|
||||
version "0.1.4"
|
||||
resolved "https://registry.yarnpkg.com/window-size/-/window-size-0.1.4.tgz#f8e1aa1ee5a53ec5bf151ffa09742a6ad7697876"
|
||||
|
||||
wrap-ansi@^2.0.0:
|
||||
version "2.1.0"
|
||||
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-2.1.0.tgz#d8fc3d284dd05794fe84973caecdd1cf824fdd85"
|
||||
dependencies:
|
||||
string-width "^1.0.1"
|
||||
strip-ansi "^3.0.1"
|
||||
|
||||
y18n@^3.2.0:
|
||||
version "3.2.1"
|
||||
resolved "https://registry.yarnpkg.com/y18n/-/y18n-3.2.1.tgz#6d15fba884c08679c0d77e88e7759e811e07fa41"
|
||||
|
||||
yargs@3.29.0:
|
||||
version "3.29.0"
|
||||
resolved "https://registry.yarnpkg.com/yargs/-/yargs-3.29.0.tgz#1aab9660eae79d8b8f675bcaeeab6ee34c2cf69c"
|
||||
dependencies:
|
||||
camelcase "^1.2.1"
|
||||
cliui "^3.0.3"
|
||||
decamelize "^1.0.0"
|
||||
os-locale "^1.4.0"
|
||||
window-size "^0.1.2"
|
||||
y18n "^3.2.0"
|
||||
Reference in New Issue
Block a user