Sub host options #65

Closed
wclr wants to merge 3 commits from sub-host into master
5 changed files with 105 additions and 21 deletions
+9 -5
View File
@@ -6,7 +6,7 @@ localtunnel exposes your localhost to the world for easy testing and sharing! No
This repo is the server component. If you are just looking for the CLI localtunnel app, see (https://github.com/localtunnel/localtunnel). This repo is the server component. If you are just looking for the CLI localtunnel app, see (https://github.com/localtunnel/localtunnel).
## overview ## ## overview
The default localtunnel client connects to the `localtunnel.me` server. You can, however, easily set up and run your own server. In order to run your own localtunnel server you must ensure that your server can meet the following requirements: The default localtunnel client connects to the `localtunnel.me` server. You can, however, easily set up and run your own server. In order to run your own localtunnel server you must ensure that your server can meet the following requirements:
@@ -15,7 +15,7 @@ The default localtunnel client connects to the `localtunnel.me` server. You can,
The above are important as the client will ask the server for a subdomain under a particular domain. The server will listen on any OS-assigned TCP port for client connections. The above are important as the client will ask the server for a subdomain under a particular domain. The server will listen on any OS-assigned TCP port for client connections.
#### setup ## setup
```shell ```shell
# pick a place where the files will live # pick a place where the files will live
@@ -29,7 +29,7 @@ bin/server --port 1234
The localtunnel server is now running and waiting for client requests on port 1234. You will most likely want to set up a reverse proxy to listen on port 80 (or start localtunnel on port 80 directly). The localtunnel server is now running and waiting for client requests on port 1234. You will most likely want to set up a reverse proxy to listen on port 80 (or start localtunnel on port 80 directly).
#### use your server ## use your server
You can now use your domain with the `--host` flag for the `lt` client. You can now use your domain with the `--host` flag for the `lt` client.
@@ -41,7 +41,9 @@ You will be assigned a URL similar to `qdci.sub.example.com:1234`.
If your server is acting as a reverse proxy (i.e. nginx) and is able to listen on port 80, then you do not need the `:1234` part of the hostname for the `lt` client. If your server is acting as a reverse proxy (i.e. nginx) and is able to listen on port 80, then you do not need the `:1234` part of the hostname for the `lt` client.
## Deploy Not that to use sub-hosts you should pass `--sub-host` option to server
## deploy
You can deploy your own localtunnel server using the prebuilt docker image. You can deploy your own localtunnel server using the prebuilt docker image.
@@ -54,5 +56,7 @@ docker run -d \
--restart always \ --restart always \
--name localtunnel \ --name localtunnel \
--net host \ --net host \
defunctzombie/localtunnel-server:latest --port 3000 defunctzombie/localtunnel-server:latest --port 3000 --sub-host sub
``` ```
+6 -1
View File
@@ -17,6 +17,9 @@ const argv = optimist
default: '80', default: '80',
describe: 'listen on this port for outside requests' describe: 'listen on this port for outside requests'
}) })
.options('sub-host', {
describe: 'additionally listen on this sub host for outside requests (can use this option multiple times)'
})
.options('max-sockets', { .options('max-sockets', {
default: 10, default: 10,
describe: 'maximum number of tcp sockets each client is allowed to establish at one time (the tunnels)' describe: 'maximum number of tcp sockets each client is allowed to establish at one time (the tunnels)'
@@ -30,7 +33,9 @@ if (argv.help) {
const server = require('../server')({ const server = require('../server')({
max_tcp_sockets: argv['max-sockets'], max_tcp_sockets: argv['max-sockets'],
secure: argv.secure secure: argv.secure,
port: argv.port,
subHost: argv['sub-host']
}); });
server.listen(argv.port, () => { server.listen(argv.port, () => {
+25 -13
View File
@@ -40,22 +40,35 @@ const stats = {
// handle proxying a request to a client // handle proxying a request to a client
// will wait for a tunnel socket to become available // will wait for a tunnel socket to become available
function maybe_bounce(req, res, sock, head) { function maybe_bounce(req, res, sock, head, opt) {
// without a hostname, we won't know who the request is for // without a hostname, we won't know who the request is for
const hostname = req.headers.host; const hostname = req.headers.host;
if (!hostname) { if (!hostname) {
return false; return false;
} }
const subdomain = tldjs.getSubdomain(hostname); let subdomain = tldjs.getSubdomain(hostname);
if (subdomain && opt.subHost) {
const subHosts = Array.isArray(opt.subHost)
? opt.subHost : [opt.subHost]
const subHost = subHosts.reduce((found, sub) => {
return found
|| (subdomain.slice(-sub.length) == sub ? sub : '')
}, '')
subdomain = subHost
? subdomain.slice(0, -(subHost.length + 1))
: subdomain
}
if (!subdomain) { if (!subdomain) {
return false; return false;
} }
const client = clients[subdomain]; const client = clients[subdomain];
// no such subdomain // no such subdomain
// we use 502 error to the client to signify we can't service the request // we use 502 error to the client to signify we can't service the request
if (!client) { if (!client) {
if (res) { if (res) {
res.statusCode = 502; res.statusCode = 502;
@@ -68,7 +81,7 @@ function maybe_bounce(req, res, sock, head) {
return true; return true;
} }
let finished = false; let finished = false;
if (sock) { if (sock) {
sock.once('end', function() { sock.once('end', function() {
@@ -233,7 +246,7 @@ module.exports = function(opt) {
if (req.query['new'] === undefined) { if (req.query['new'] === undefined) {
return next(); return next();
} }
const req_id = rand_id(); const req_id = rand_id();
debug('making new client with id %s', req_id); debug('making new client with id %s', req_id);
new_client(req_id, opt, function(err, info) { new_client(req_id, opt, function(err, info) {
@@ -271,20 +284,19 @@ module.exports = function(opt) {
app.get('/:req_id', function(req, res, next) { app.get('/:req_id', function(req, res, next) {
const req_id = req.params.req_id; const req_id = req.params.req_id;
// limit requested hostnames to 63 characters // limit requested hostnames to 63 characters
if (! /^[a-z0-9]{4,63}$/.test(req_id)) { if (! /^[a-z0-9-]{4,63}$/.test(req_id)) {
const err = new Error('Invalid subdomain. Subdomains must be lowercase and between 4 and 63 alphanumeric characters.'); const err = new Error('Invalid subdomain. Subdomains must be lowercase and between 4 and 63 alphanumeric characters.');
err.statusCode = 403; err.statusCode = 403;
return next(err); return next(err);
} }
debug('making new client with id %s', req_id); debug('making new client with id %s', req_id);
new_client(req_id, opt, function(err, info) { new_client(req_id, opt, function(err, info) {
if (err) { if (err) {
return next(err); return next(err);
} }
const url = schema + '://' + req_id + '.' + req.headers.host; const url = schema + '://' + req_id + '.' + req.headers.host;
info.url = url; info.url = url;
res.json(info); res.json(info);
@@ -312,7 +324,7 @@ module.exports = function(opt) {
}); });
debug('request %s', req.url); debug('request %s', req.url);
if (maybe_bounce(req, res, null, null)) { if (maybe_bounce(req, res, null, null, opt)) {
return; return;
}; };
@@ -328,7 +340,7 @@ module.exports = function(opt) {
console.error('ws socket', err); console.error('ws socket', err);
}); });
if (maybe_bounce(req, null, socket, head)) { if (maybe_bounce(req, null, socket, head, opt)) {
return; return;
}; };
+36 -1
View File
@@ -4,7 +4,8 @@ var assert = require('assert');
var localtunnel = require('localtunnel'); var localtunnel = require('localtunnel');
var localtunnel_server = require('../server')({ var localtunnel_server = require('../server')({
max_tcp_sockets: 2 max_tcp_sockets: 2,
subHost: ['nested.sub']
}); });
var lt_server_port var lt_server_port
@@ -66,3 +67,37 @@ test('should respond to request', function(done) {
}); });
}); });
}); });
test('set up localtunnel client (sub-host)', function (done) {
var opt = {
host: 'http://nested.sub.host.dev:' + lt_server_port,
subdomain: 'txyz'
};
localtunnel(test._fake_port, opt, function (err, tunnel) {
assert.ifError(err);
var url = tunnel.url;
assert.ok(new RegExp('^http:\/\/.*nested.sub.host.dev:' + lt_server_port + '$').test(url));
test._fake_url = url;
done(err);
});
});
test('should respond to request (sub-host)', function (done) {
var opt = {
host: 'txyz.nested.sub.host.dev',
port: lt_server_port,
};
http.get(opt, function (res) {
var body = '';
res.setEncoding('utf-8');
res.on('data', function (chunk) {
body += chunk;
});
res.on('end', function () {
assert.equal(body, 'hello world!');
done();
});
});
});
+29 -1
View File
@@ -6,7 +6,8 @@ var WebSocket = require('ws');
var WebSocketServer = require('ws').Server; var WebSocketServer = require('ws').Server;
var localtunnel_server = require('../server')({ var localtunnel_server = require('../server')({
max_tcp_sockets: 2 max_tcp_sockets: 2,
subHost: ['nested.sub', 'sub']
}); });
var lt_server_port var lt_server_port
@@ -72,3 +73,30 @@ test('websocket server request', function(done) {
ws.send('something'); ws.send('something');
}); });
}); });
test('set up localtunnel client (sub host)', function (done) {
var opt = {
host: 'http://sub.host.dev:' + lt_server_port
};
localtunnel(test._fake_port, opt, function (err, tunnel) {
assert.ifError(err);
var url = tunnel.url;
assert.ok(new RegExp('^http:\/\/.*sub.host.dev:' + lt_server_port + '$').test(url));
test._fake_url = url;
done(err);
});
});
test('websocket server request (sub-host)', function (done) {
var ws = new WebSocket(test._fake_url);
ws.on('message', function (msg) {
assert.equal(msg, 'something');
done();
});
ws.on('open', function open() {
ws.send('something');
});
});