import Koa from "koa"; import tldjs from "tldjs"; import Debug from "debug"; import http from "http"; import { hri } from "human-readable-ids"; import Router from "koa-router"; import { ClientManager } from "./lib/manager"; export type Options = { domain: string; secure: boolean; landing?: string; max_tcp_sockets?: number; }; const debug = Debug("localtunnel:server"); export const CreateServer = (opt: Options) => { opt = opt || {}; const validHosts = opt.domain ? [opt.domain] : undefined; const myTldjs = tldjs.fromUserSettings({ validHosts }); const landingPage = opt.landing || "https://localtunnel.github.io/www/"; function getClientIdFromHostname(hostname: string) { return myTldjs.getSubdomain(hostname); } const manager = new ClientManager(opt); const schema = opt.secure ? "https" : "http"; const app = new Koa(); const router = new Router(); router.get("/api/status", async (ctx, _next) => { const stats = manager.stats; ctx.body = { tunnels: stats.tunnels, mem: process.memoryUsage(), }; }); router.get("/api/tunnels/:id/status", async (ctx, _next) => { const clientId = ctx.params.id; const client = manager.getClient(clientId); if (!client) { ctx.throw(404); return; } const stats = client.stats(); ctx.body = { connected_sockets: stats.connectedSockets, }; }); app.use(router.routes()); app.use(router.allowedMethods()); // 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 reqId = hri.random(); debug("making new client with id %s", reqId); const info = await manager.newClient(reqId); 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(landingPage); }); // 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 reqId = parts[1]; // limit requested hostnames to 63 characters if (!/^(?:[a-z0-9][a-z0-9\-]{4,63}[a-z0-9]|[a-z0-9]{4,63})$/.test(reqId)) { 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", reqId); const info = await manager.newClient(reqId); 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; } const client = manager.getClient(clientId); if (!client) { res.statusCode = 404; res.end("404"); return; } client.handleRequest(req, res); }); server.on("upgrade", (req, socket, head) => { console.log(`\n\n\n\x1b[33m==>UPGRADE \x1b[0m\n`); const hostname = req.headers.host; if (!hostname) { socket.destroy(); return; } const clientId = getClientIdFromHostname(hostname); if (!clientId) { socket.destroy(); return; } const client = manager.getClient(clientId); if (!client) { socket.destroy(); return; } client.handleUpgrade(req, socket); }); return server; };