102 Commits

Author SHA1 Message Date
Roman Shtylman
86cd2d3c58 v1.8.0 2015-11-04 08:04:54 -08:00
Roman Shtylman
d70c743014 update history 2015-11-04 08:04:29 -08:00
Roman Shtylman
4940043378 Merge pull request #102 from aronwoost/patch-01
Emit socket errors, so they can be handled
2015-11-04 07:54:45 -08:00
Aron Woost
b4a22bff64 Re-emit socket error 2015-11-04 12:04:07 +01:00
Roman Shtylman
176ec0479d Merge pull request #92 from cbas/master
Bumped deps to fix upstream SPDX license issues
2015-11-03 16:13:30 -08:00
Sebastiaan Deckers
fe316de3e0 Bumped deps to fix upstream SPDX license issues 2015-11-04 08:10:18 +08:00
Roman Shtylman
4f97434a69 v1.7.0 2015-07-22 17:19:32 -07:00
Roman Shtylman
a46cd02fcb update history 2015-07-22 17:19:15 -07:00
Roman Shtylman
792d9f19bd update history 2015-07-22 17:18:45 -07:00
Roman Shtylman
d0b483b92b Merge pull request #75 from artmees/master
Implement short argv via yargs
2015-07-22 17:16:49 -07:00
Ahmed Abdel Razzak
59d96a3cc6 Update the client to include shorthand options
Providing aliases for all of the supported options
-h, --host
-s, --subdomain
-l, --local-host
-o, --open
-p, --port
--help
--version

Minor clean up in the code removing the blocks that already handled
by yargs ( version, help and required options )

Handle validating that the passed port could be validated as a number

Remove the spaces in empty lines
2015-07-23 02:05:17 +02:00
Ahmed Abdel Razzak
b516ecccfa Replace optimist with yargs 2015-07-23 00:57:01 +02:00
Roman Shtylman
f68b1f06d9 1.6.0 2015-07-15 22:29:49 -07:00
Roman Shtylman
4d09875163 update history 2015-07-15 22:26:40 -07:00
Roman Shtylman
2773fe6923 Merge pull request #74 from davej/patch-1
Increase max event listeners to prevent premature memory-leak warnings
2015-07-15 22:24:47 -07:00
Roman Shtylman
457bd64ecc Merge pull request #85 from zeevl/keep-alive
Keep sockets alive after connecting
2015-07-15 15:19:35 -07:00
Steve Lamb
eb31659345 Keep sockets alive after connecting 2015-07-15 10:29:53 -07:00
Roman Shtylman
3ee8b1b884 Merge pull request #79 from Urucas/master
Add --open param to open tunnel url
2015-06-23 23:29:39 -07:00
vrunoa
15aac729bb add open param; opens url in your browser after localtunnel is launched
add open param; opens url in your browser after localtunnel is launched

update package version

multiplatform open with openurl package

update package.json

correct source code spacing & indentation

update openurl dependency

revert package version

move require('openurl') to the top, correct 4 spaces indentation
2015-06-23 17:10:28 -03:00
Dave Jeffery
e73cfe3e45 Increase max event listeners to prevent premature memory-leak warnings #71 2015-05-31 22:19:45 +01:00
Roman Shtylman
790a642a83 1.5.1 2015-05-26 09:28:59 -04:00
Roman Shtylman
e6539e1225 Merge pull request #72 from Lykathia/patch-license
package.json: Add license to package.json
2015-05-25 23:04:03 -04:00
Evan Lowry
4c0a5dc4eb Add license to package.json 2015-05-25 22:55:41 -03:00
Roman Shtylman
809262cf3d refactor into files 2014-12-21 17:46:38 -08:00
Roman Shtylman
ddb47d2f90 add .npmignore file 2014-10-25 17:21:59 -07:00
Roman Shtylman
a6845ec63b v1.5.0 2014-10-25 17:13:58 -07:00
Roman Shtylman
adecf03f41 update history 2014-10-25 17:13:49 -07:00
Roman Shtylman
9bdb40e97c when remote endpoint has an error, just close it 2014-10-25 17:05:19 -07:00
Roman Shtylman
006ce7733b v1.4.0 2014-08-31 22:59:17 -07:00
Roman Shtylman
624d279c26 History: update 2014-08-31 22:58:43 -07:00
Roman Shtylman
c2b8f2b7ab Merge pull request #57 from kesla/handle-etimedout
don't emit ETIMEDOUT-errors
2014-08-31 22:57:30 -07:00
David Björklund
4d9dcc1711 don't emit ETIMEDOUT-errors 2014-08-28 20:12:09 +02:00
Roman Shtylman
7b1fef982f 1.3.0 2014-05-13 20:33:41 -04:00
Roman Shtylman
b1ebef2b0b fix code style 2014-05-13 20:32:43 -04:00
Tymoteusz Paszun
9ad43778b7 fixed Host header replacement - remembers if header is already replaced and does not remove _transform method 2014-05-13 09:24:08 +02:00
Tymoteusz Paszun
887c444543 added test for chunked requests 2014-05-13 09:16:50 +02:00
Tymoteusz Paszun
828cb2afcb transform Host header when local-host is defined explicitly 2014-05-09 15:07:38 +02:00
Roman Shtylman
8768329fdd update history with two recent minor versions 2014-04-28 19:03:33 -04:00
Roman Shtylman
3026d6a42c 1.2.0 2014-04-28 19:00:12 -04:00
Roman Shtylman
abd461f83a Merge pull request #47 from dscape/patch-1
return client from 'localtunnel' api call
2014-04-28 18:58:54 -04:00
Nuno Job
2acea3d77f Return client
This allows manipulating the client from outside. Allowing, for example, to close a connection.
2014-04-28 23:32:00 +01:00
Roman Shtylman
5d0eb3382a add --version CLI flag to get version info 2014-04-22 19:55:19 -04:00
Roman Shtylman
3b67c8a8ce 1.1.2 2014-04-20 10:51:24 -04:00
Kevin Ingersoll
71552a336e Increase default Mocha timeout 2014-04-20 10:45:21 -04:00
Roman Shtylman
87a23bf28c fix status code check for url request 2014-04-19 19:33:52 -04:00
Roman Shtylman
3d54de851f handle errors from localtunnel server when requesting initial url 2014-04-19 19:30:44 -04:00
Roman Shtylman
92bb807908 fix typo
fixes #45
2014-04-18 09:34:48 -04:00
Roman Shtylman
afbdc3697e 1.1.1 2014-04-15 09:33:34 -04:00
Roman Shtylman
0049f21b55 Merge pull request #38 from LinusU/patch-1
re-throw client errors from bin/lt to let node handle it
2014-04-14 19:27:56 -04:00
Roman Shtylman
509841104b fix for RangeError stack size exceeded
This error would happen when there was a problem connecting to the local
server. The local.on('error') handler should have been a 'once' handler
because we emit the error again if it isn't a CONNREFUSED. So in the
case of a CONNRESET, it would trigger an infinite loop since the error
was being emitted back onto the local variable. Instead we just close
the remote socket and let a new one takes its place.

fixes #36
2014-04-14 15:36:42 -04:00
Linus Unnebäck
92caf2f204 lt: better error handling
Let node handle the displaying of error and setting exit code.
2014-04-14 21:15:11 +02:00
Roman Shtylman
9487797e02 1.1.0 2014-02-24 19:51:14 -05:00
Roman Shtylman
a42f6a8d8d document local-host option 2014-02-24 19:49:40 -05:00
Roman Shtylman
14b4bcb96f add tests for host header transform 2014-02-24 19:43:34 -05:00
Roman Shtylman
4aa65002eb use host header transform only when local-host is specified 2014-02-24 19:43:07 -05:00
Fredi Pevcin
08676ba81d Set Host header accordingly to local-host option 2014-02-24 00:26:38 +01:00
Roman Shtylman
174e7f3982 1.0.0 2014-02-14 00:37:45 -05:00
Roman Shtylman
44be55cd7b readme: fix markdown table
[ci skip]
2014-02-14 00:36:53 -05:00
Roman Shtylman
5c6cd2359c add travis badge
- remove node 0.8
- add node 0.10
2014-02-14 00:34:40 -05:00
Roman Shtylman
2f6f9459ad change main export signature to localtunnel(port, opt, fn)
Makes for a simpler hello world app
2014-02-14 00:32:43 -05:00
Roman Shtylman
7217a08a05 add history.md to track changes 2014-02-13 23:52:12 -05:00
Roman Shtylman
fbfc923a7e remove connect export in favor of single function
Since connect was the only function exported, we can just export the
function directly to make things simpler.
2014-02-13 23:51:29 -05:00
Roman Shtylman
d9bc11b520 default host to localtuunel.me
If no host specified, then default to localtunnel.me

close #31
2014-02-13 23:43:54 -05:00
Roman Shtylman
ad64611bd1 add tests 2014-02-13 23:41:49 -05:00
Roman Shtylman
ac70515143 0.2.2 2014-01-09 11:07:18 -05:00
Roman Shtylman
8d7ccccf21 remove local.unpipe() on remote close
This will happen automatically.

close #28
2014-01-09 11:06:58 -05:00
Roman Shtylman
77091b3d93 0.2.1 2013-12-31 17:34:04 -05:00
Roman Shtylman
4f4a147b45 don't unpipe on local close
Pipe will do this for us
2013-12-31 17:33:49 -05:00
Roman Shtylman
f1d809a84d 0.2.0 2013-12-31 15:39:16 -05:00
Roman Shtylman
eba003bd26 add a .close method to shutdown the tunnel 2013-12-31 15:38:45 -05:00
Roman Shtylman
3354c4c6e3 rework tunnel logic
Refactoring to make things a bit saner and easier to debug.
2013-12-05 11:26:19 -05:00
Roman Shtylman
1c2757e604 Merge pull request #27 from adammck/couldnt-establish-tunnel
Add more verbose error for ECONNREFUSED
2013-11-20 09:34:48 -08:00
Adam Mckaig
790e55e881 Add more verbose error for ECONNREFUSED
If the tunnel server can be reached (at e.g. http://localtunnel.me/?new)
but the tunnel (to e.g. grpi.localtunnel.me:44827) can't actually be
established, the client currently gets stuck in a loop retrying forever
with no indication as to what's wrong. This doesn't fix the loop, since
it does seem desirable to retry forever, but logs:

    [Error: connection refused: localtunnel.me:44827]
2013-11-20 12:29:31 -05:00
Roman Shtylman
a9b0274ff4 0.1.3 2013-11-14 12:10:02 -05:00
Roman Shtylman
83ecb29eff Merge pull request #26 from EverythingMe/override_localhost
Added the --localhost parameter to tunnel the traffic to other hosts
2013-11-14 09:08:47 -08:00
Omri Bahumi
21df257d16 Added the --local-host parameter to tunnel the traffic to other hosts 2013-11-14 18:06:19 +02:00
Roman Shtylman
18ada0854a 0.1.2 2013-11-06 23:25:28 -05:00
Roman Shtylman
34afd6537d more resilient to upstream server failure and restart 2013-11-06 23:25:05 -05:00
Roman Shtylman
2c38aefb9d add go client to readme 2013-10-23 11:31:28 -04:00
Roman Shtylman
aa488f6e76 0.1.1 2013-10-22 15:57:36 -04:00
Roman Shtylman
f6618953f9 Merge pull request #20 from eagleeye/master
Do not call success callback right after error in request_url
2013-10-16 18:49:48 -07:00
Andrii Shumada
092d050fa0 Do not call success callback right after error in request_url 2013-10-16 12:05:37 +03:00
Roman Shtylman
0334ace20b 0.1.0 2013-06-17 02:13:17 -04:00
Roman Shtylman
13afcff1ae fix README api example 2013-06-17 02:12:54 -04:00
Roman Shtylman
ed5aa3f16b remove server components
moved to localtunnel-server repo
2013-06-17 02:11:41 -04:00
Roman Shtylman
2fcac1336c add debug module to deps 2013-06-16 18:35:55 -04:00
Roman Shtylman
0568ae0bef close client tcp sockets after each http response
While a little less efficient than keeping tcp connections open, this
helps ensure that bad things don't happen on the socket connections when
http protocol issues happen.
2013-06-16 18:24:27 -04:00
Roman Shtylman
585a8afad7 fix undefined variable
fixes #8
2013-02-11 14:19:34 -05:00
Roman Shtylman
fbe841a1c5 add node 0.9 for travis testing 2013-01-26 13:01:16 -05:00
Roman Shtylman
929473913f add notes about running your own server 2012-12-24 03:14:19 -05:00
Roman Shtylman
5340659954 0.0.4 2012-12-17 14:32:31 -05:00
Roman Shtylman
5c6558ed91 fix missing query parsing in server 2012-12-17 14:31:05 -05:00
Roman Shtylman
79ca069c38 refactor to use http-raw for lt server incoming
- http raw exposes a socket to the req/res pair
- cleanup client to be more resilient
- add test for queued requests
2012-12-17 14:23:12 -05:00
Roman Shtylman
741db27084 fix domain typo in readme
localtunnel.me not .com
2012-12-14 23:43:06 -05:00
Roman Shtylman
b605e9b823 server: make sure client id are released when unused
When clients disconnect, their tcp server should be shutdown and the id
released after a grace period.
2012-11-14 13:53:33 -05:00
Roman Shtylman
b5830c3840 0.0.3 2012-11-06 10:55:24 -05:00
Roman Shtylman
06b85ad0aa expose client and server api 2012-11-06 10:55:08 -05:00
Roman Shtylman
943a7dc35b typo 2012-11-05 16:05:32 -05:00
Roman Shtylman
c46a94b7a0 0.0.2 2012-11-03 15:33:32 -04:00
Roman Shtylman
2f692b8e29 expose client as a library
- Allows for using localtunnel from code instead of manually invoking
- add tests
- add travis config
- add travis badge
2012-11-03 15:16:30 -04:00
Roman Shtylman
51d91ce0e8 refactor server tcp handling
- limit on number of tcp connections
- preliminary support for websockets
2012-10-17 22:50:59 -04:00
Roman Shtylman
ab28444802 add server launcher to bin 2012-10-17 18:59:09 -04:00
13 changed files with 709 additions and 446 deletions

1
.npmignore Normal file
View File

@@ -0,0 +1 @@
support

3
.travis.yml Normal file
View File

@@ -0,0 +1,3 @@
language: node_js
node_js:
- "0.10"

36
History.md Normal file
View File

@@ -0,0 +1,36 @@
# 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

View File

@@ -1,8 +1,8 @@
# localtunnel #
# localtunnel [![Build Status](https://travis-ci.org/defunctzombie/localtunnel.png?branch=master)](https://travis-ci.org/defunctzombie/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 +10,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
@@ -21,3 +23,67 @@ 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!
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
## API ##
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 localtunnel = require('localtunnel');
var tunnel = localtunnel(port, function(err, tunnel) {
if (err) ...
// the assigned public url for your tunnel
// i.e. https://abcdefgjhij.localtunnel.me
tunnel.url;
});
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 availablily.
* `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|
|----|----|----|
|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)
## server ##
See defunctzombie/localtunnel-server for details on the server that powers localtunnel.
## License ##
MIT

62
bin/client Executable file
View File

@@ -0,0 +1,62 @@
#!/usr/bin/env node
var lt_client = require('../client');
var open_url = require('openurl');
var argv = require('yargs')
.usage('Usage: $0 --port [num] <options>')
.option('h', {
alias: 'host',
describe: 'Upstream server providing forwarding',
default: 'http://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',
})
.require('port')
.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,
};
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;
});
});
// vim: ft=javascript

2
bin/lt
View File

@@ -1,2 +0,0 @@
#!/usr/bin/env node
require(__dirname + '/../client');

103
client.js
View File

@@ -1,95 +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');
var argv = require('optimist')
.usage('Usage: $0 --port [num]')
.demand(['port'])
.options('host', {
default: 'http://localtunnel.me',
describe: 'upstream server providing forwarding'
})
.describe('port', 'internal http server port')
.argv;
var Tunnel = require('./lib/Tunnel');
// local port
var local_port = argv.port;
module.exports = function localtunnel(port, opt, fn) {
if (typeof opt === 'function') {
fn = opt;
opt = {};
}
// optionally override the upstream server
var upstream = url.parse(argv.host);
opt = opt || {};
opt.port = port;
// query options
var opt = {
host: upstream.hostname,
port: upstream.port || 80,
path: '/',
json: true
};
var base_uri = 'http://' + opt.host + ':' + opt.port + opt.path;
var internal;
var upstream;
var prev_id;
(function connect_proxy() {
opt.uri = base_uri + ((prev_id) ? prev_id : '?new');
request(opt, function(err, res, body) {
var client = Tunnel(opt);
client.open(function(err) {
if (err) {
console.error('upstream not available: %s', err.message);
return process.exit(-1);
return fn(err);
}
// our assigned hostname and tcp port
var port = body.port;
var host = opt.host;
// store the id so we can try to get the same one
prev_id = body.id;
console.log('your url is: %s', body.url);
// connect to remote tcp server
upstream = net.createConnection(port, host);
// reconnect internal
connect_internal();
upstream.on('end', function() {
console.log('> upstream connection terminated');
// sever connection to internal server
// on reconnect we will re-establish
internal.end();
setTimeout(function() {
connect_proxy();
}, 1000);
});
fn(null, client);
});
})();
function connect_internal() {
internal = net.createConnection(local_port);
internal.on('error', function(err) {
console.log('error connecting to local server. retrying in 1s');
setTimeout(function() {
connect_internal();
}, 1000);
});
internal.on('end', function() {
console.log('disconnected from local server. retrying in 1s');
setTimeout(function() {
connect_internal();
}, 1000);
});
upstream.pipe(internal);
internal.pipe(upstream);
}
return client;
};

View 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;
chunk = chunk.toString();
// after replacing the first instance of the Host header
// we just become a regular passthrough
if (!self.replaced) {
self.push(chunk.replace(/(\r\nHost: )\S+/, function(match, $1) {
self.replaced = true;
return $1 + self.host;
}));
}
else {
self.push(chunk);
}
cb();
};
module.exports = HeaderHostTransformer;

157
lib/Tunnel.js Normal file
View File

@@ -0,0 +1,157 @@
var url = require('url');
var EventEmitter = require('events').EventEmitter;
var request = require('request');
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 = {
path: '/',
json: true
};
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
params.uri = base_uri + ((assigned_domain) ? assigned_domain : '?new');
(function get_url() {
request(params, function(err, res, body) {
if (err) {
// TODO (shtylman) don't print to stdout?
console.log('tunnel server offline: ' + err.message + ', retry 1s');
return setTimeout(get_url, 1000);
}
if (res.statusCode !== 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
});
});
})();
};
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();
});
// 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;

123
lib/TunnelCluster.js Normal file
View File

@@ -0,0 +1,123 @@
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);
});
});
}
// tunnel is considered open when remote connects
remote.once('connect', function() {
self.emit('open', remote);
conn_local();
});
};
module.exports = TunnelCluster;

View File

@@ -2,22 +2,26 @@
"author": "Roman Shtylman <shtylman@gmail.com>",
"name": "localtunnel",
"description": "expose localhost to the world",
"version": "0.0.1",
"version": "1.8.0",
"license": "MIT",
"repository": {
"type": "git",
"url": "git://github.com/shtylman/localtunnel.git"
},
"dependencies": {
"request": "2.11.4",
"book": "1.2.0",
"optimist": "0.3.4"
"request": "2.65.0",
"yargs": "3.29.0",
"debug": "2.2.0",
"openurl": "1.1.0"
},
"devDependencies": {},
"optionalDependencies": {},
"engines": {
"node": "*"
"devDependencies": {
"mocha": "~1.17.0"
},
"scripts": {
"test": "mocha --ui qunit --reporter list --timeout 10000 -- test/index.js"
},
"bin": {
"lt": "./bin/lt"
}
}
"lt": "./bin/client"
},
"main": "./client.js"
}

343
server.js
View File

@@ -1,343 +0,0 @@
// builtin
var http = require('http');
var net = require('net');
var url = require('url');
var FreeList = require('freelist').FreeList;
var argv = require('optimist')
.usage('Usage: $0 --port [num]')
.options('port', {
default: '80',
describe: 'listen on this port for outside requests'
})
.argv;
if (argv.help) {
require('optimist').showHelp();
process.exit();
}
// here be dragons
var HTTPParser = process.binding('http_parser').HTTPParser;
var ServerResponse = http.ServerResponse;
var IncomingMessage = http.IncomingMessage;
var log = require('book');
var chars = 'abcdefghiklmnopqrstuvwxyz';
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;
}
var server = http.createServer();
// id -> client http server
var clients = {};
// id -> list of sockets waiting for a valid response
var wait_list = {};
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 current = clients[socket.subdomain].current;
if (!current) {
log.error('no current for http response from backend');
return;
}
// send the goodies
current.write(d.slice(start, end));
// invoke parsing so we know when all the goodies have been sent
var parser = current.out_parser;
parser.socket = socket;
var ret = parser.execute(d, start, end - start);
if (ret instanceof Error) {
debug('parse error');
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;
var for_client = false;
var client_id;
var request;
var parser = parsers.alloc();
parser.socket = socket;
parser.reinitialize(HTTPParser.REQUEST);
// a full request is complete
// we wait for the response from the server
parser.onIncoming = function(req, shouldKeepAlive) {
log.trace('request', req.url);
request = req;
for_client = false;
var hostname = req.headers.host;
if (!hostname) {
log.trace('no hostname: %j', req.headers);
// normal processing if not proxy
var res = new ServerResponse(req);
// TODO(shtylman) skip favicon for now, it caused problems
if (req.url === '/favicon.ico') {
return;
}
res.assignSocket(parser.socket);
self.emit('request', req, res);
return;
}
var match = hostname.match(/^([a-z]{4})[.].*/);
if (!match) {
// normal processing if not proxy
var res = new ServerResponse(req);
// TODO(shtylman) skip favicon for now, it caused problems
if (req.url === '/favicon.ico') {
return;
}
res.assignSocket(parser.socket);
self.emit('request', req, res);
return;
}
client_id = match[1];
for_client = true;
var out_parser = parsers.alloc();
out_parser.reinitialize(HTTPParser.RESPONSE);
socket.out_parser = out_parser;
// we have a response
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);
var next = wait_list[client_id].shift();
clients[client_id].current = next;
if (!next) {
return;
}
// write original bytes that we held cause client was busy
clients[client_id].write(next.queue);
next.resume();
});
};
};
// process new data on the client socket
// we may need to forward this it the backend
socket.ondata = function(d, start, end) {
var ret = parser.execute(d, start, end - start);
// invalid request from the user
if (ret instanceof Error) {
debug('parse error');
socket.destroy(ret);
return;
}
// only write data if previous request to this client is done?
log.trace('%s %s', parser.incoming && parser.incoming.upgrade, for_client);
// what if the subdomains are treated differently
// as individual channels to the backend if available?
// how can I do that?
if (parser.incoming && parser.incoming.upgrade) {
// websocket shit
}
// wtf do you do with upgraded connections?
// forward the data to the backend
if (for_client) {
var client = clients[client_id];
// requesting a subdomain that doesn't exist
if (!client) {
return;
}
// if the client is already processing something
// then new connections need to go into pause mode
// and when they are revived, then they can send data along
if (client.current && client.current !== socket) {
log.trace('pausing', request.url);
// prevent new data from gathering for this connection
// we are waiting for a response to a previous request
socket.pause();
var copy = Buffer(end - start);
d.copy(copy, 0, start, end);
socket.queue = copy;
wait_list[client_id].push(socket);
return;
}
// this socket needs to receive responses
client.current = socket;
// send through tcp tunnel
client.write(d.slice(start, end));
}
};
socket.onend = function() {
var ret = parser.finish();
if (ret instanceof Error) {
log.trace('parse error');
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();
}
var match = req.url.match(/\/([a-z]{4})?/);
// user can request a particular set of characters
// will be given if not already taken
// this is useful when the main server is restarted
// users can keep testing with their expected ids
var requested_id;
if (match && match[1]) {
requested_id = match[1];
}
var id = requested_id || rand_id();
if (wait_list[id]) {
// new id
id = rand_id();
}
// generate new shit for client
if (wait_list[id]) {
wait_list[id].forEach(function(waiting) {
waiting.end();
});
}
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 }));
});
// user has 5 seconds to connect before their slot is given up
var conn_timeout = setTimeout(function() {
client_server.close();
}, 5000);
client_server.on('connection', function(socket) {
// who the info should route back to
socket.subdomain = id;
// multiplexes socket data out to clients
socket.ondata = socketOnData;
clearTimeout(conn_timeout);
log.trace('new connection for id: %s', id);
clients[id] = socket;
wait_list[id] = [];
socket.on('end', function() {
delete clients[id];
});
});
client_server.on('err', function(err) {
log.error(err);
});
});
server.listen(argv.port, function() {
log.info('server listening on port: %d', server.address().port);
});

188
test/index.js Normal file
View 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'));
});