Files
server/server.js
2017-12-11 21:29:11 -08:00

152 lines
3.9 KiB
JavaScript

import log from 'book';
import Koa from 'koa';
import tldjs from 'tldjs';
import Debug from 'debug';
import http from 'http';
import Promise from 'bluebird';
import ClientManager from './lib/ClientManager';
import rand_id from './lib/rand_id';
const debug = Debug('localtunnel:server');
function GetClientIdFromHostname(hostname) {
return tldjs.getSubdomain(hostname);
}
module.exports = function(opt) {
opt = opt || {};
const manager = new ClientManager(opt);
const schema = opt.secure ? 'https' : 'http';
const app = new Koa();
// api status endpoint
app.use(async (ctx, next) => {
const path = ctx.request.path;
if (path !== '/api/status') {
await next();
return;
}
const stats = manager.stats;
ctx.body = {
tunnels: stats.tunnels,
mem: process.memoryUsage(),
};
});
// root endpoint
app.use(async (ctx, next) => {
const path = ctx.request.path;
// skip anything not on the root path
if (path !== '/') {
await next();
return;
}
const isNewClientRequest = ctx.query['new'] !== undefined;
if (isNewClientRequest) {
const req_id = rand_id();
debug('making new client with id %s', req_id);
const info = await manager.newClient(req_id);
const url = schema + '://' + info.id + '.' + ctx.request.host;
info.url = url;
ctx.body = info;
return;
}
// no new client request, send to landing page
ctx.redirect('https://localtunnel.github.io/www/');
});
// anything after the / path is a request for a specific client name
// This is a backwards compat feature
app.use(async (ctx, next) => {
const parts = ctx.request.path.split('/');
// any request with several layers of paths is not allowed
// rejects /foo/bar
// allow /foo
if (parts.length !== 2) {
await next();
return;
}
const req_id = parts[1];
// limit requested hostnames to 63 characters
if (! /^[a-z0-9]{4,63}$/.test(req_id)) {
const msg = 'Invalid subdomain. Subdomains must be lowercase and between 4 and 63 alphanumeric characters.';
ctx.status = 403;
ctx.body = {
message: msg,
};
return;
}
debug('making new client with id %s', req_id);
const info = await manager.newClient(req_id);
const url = schema + '://' + info.id + '.' + ctx.request.host;
info.url = url;
ctx.body = info;
return;
});
const server = http.createServer();
const appCallback = app.callback();
server.on('request', (req, res) => {
// without a hostname, we won't know who the request is for
const hostname = req.headers.host;
if (!hostname) {
res.statusCode = 400;
res.end('Host header is required');
return;
}
const clientId = GetClientIdFromHostname(hostname);
if (!clientId) {
appCallback(req, res);
return;
}
if (manager.hasClient(clientId)) {
manager.handleRequest(clientId, req, res);
return;
}
res.statusCode = 404;
res.end('404');
});
server.on('upgrade', (req, socket, head) => {
const hostname = req.headers.host;
if (!hostname) {
sock.destroy();
return;
}
const clientId = GetClientIdFromHostname(hostname);
if (!clientId) {
sock.destroy();
return;
}
if (manager.hasClient(clientId)) {
manager.handleUpgrade(clientId, req, socket);
return;
}
socket.destroy();
});
return server;
};