168 lines
5.7 KiB
JavaScript
168 lines
5.7 KiB
JavaScript
import debug from 'debug';
|
|
import http from 'http';
|
|
import { hri } from 'human-readable-ids';
|
|
import Koa from 'koa';
|
|
import Router from 'koa-router';
|
|
import tldjs from 'tldjs';
|
|
import { ClientManager } from './ClientManager.js';
|
|
import { TunnelServer } from './TunnelServer.js';
|
|
const log = debug('pipenet:server');
|
|
export function createServer(opt = {}) {
|
|
const validHosts = opt.domains && opt.domains.length > 0 ? opt.domains : undefined;
|
|
const myTldjs = tldjs.fromUserSettings({ validHosts });
|
|
const landingPage = opt.landing || 'https://pipenet.dev/';
|
|
function GetClientIdFromHostname(hostname) {
|
|
return myTldjs.getSubdomain(hostname);
|
|
}
|
|
// Create shared tunnel server if tunnelPort is specified
|
|
const tunnelServer = opt.tunnelPort ? new TunnelServer() : undefined;
|
|
const manager = new ClientManager({
|
|
...opt,
|
|
tunnelServer,
|
|
});
|
|
const schema = opt.secure ? 'https' : 'http';
|
|
const tunnelPort = opt.tunnelPort;
|
|
const app = new Koa();
|
|
const router = new Router();
|
|
// CORS middleware
|
|
app.use(async (ctx, next) => {
|
|
ctx.set('Access-Control-Allow-Origin', '*');
|
|
ctx.set('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
|
|
ctx.set('Access-Control-Allow-Headers', 'Content-Type, Authorization');
|
|
if (ctx.method === 'OPTIONS') {
|
|
ctx.status = 204;
|
|
return;
|
|
}
|
|
await next();
|
|
});
|
|
router.get('/api/status', async (ctx) => {
|
|
const stats = manager.stats;
|
|
ctx.body = {
|
|
mem: process.memoryUsage(),
|
|
tunnels: stats.tunnels,
|
|
};
|
|
});
|
|
router.get('/api/tunnels/:id/status', async (ctx) => {
|
|
const clientId = ctx.params.id;
|
|
const client = manager.getClient(clientId);
|
|
if (!client) {
|
|
ctx.throw(404);
|
|
return;
|
|
}
|
|
const stats = client.stats();
|
|
ctx.body = {
|
|
connectedSockets: stats.connectedSockets,
|
|
};
|
|
});
|
|
app.use(router.routes());
|
|
app.use(router.allowedMethods());
|
|
// Helper to extract domain from host (strips port if present)
|
|
const getDomain = (host) => {
|
|
// Remove port if present (e.g., "localhost:3000" -> "localhost")
|
|
return host.split(':')[0];
|
|
};
|
|
// Helper to build the tunnel URL
|
|
const buildUrl = (id, host) => {
|
|
return schema + '://' + id + '.' + host;
|
|
};
|
|
// Helper to build response with tunnel port if configured
|
|
const buildResponse = (info) => {
|
|
const response = { ...info };
|
|
// If using shared tunnel server, override port and add sharedTunnel flag
|
|
if (tunnelPort) {
|
|
response.port = tunnelPort;
|
|
response.sharedTunnel = true;
|
|
}
|
|
return response;
|
|
};
|
|
app.use(async (ctx, next) => {
|
|
const path = ctx.request.path;
|
|
if (path !== '/') {
|
|
await next();
|
|
return;
|
|
}
|
|
const isNewClientRequest = ctx.query['new'] !== undefined;
|
|
if (isNewClientRequest) {
|
|
const reqId = hri.random();
|
|
const domain = getDomain(ctx.request.host);
|
|
const url = buildUrl(reqId, ctx.request.host);
|
|
log('making new client with id %s', reqId);
|
|
const info = await manager.newClient(reqId, url, domain);
|
|
ctx.body = buildResponse(info);
|
|
return;
|
|
}
|
|
ctx.redirect(landingPage);
|
|
});
|
|
app.use(async (ctx, next) => {
|
|
const parts = ctx.request.path.split('/');
|
|
if (parts.length !== 2) {
|
|
await next();
|
|
return;
|
|
}
|
|
const reqId = parts[1];
|
|
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;
|
|
}
|
|
const domain = getDomain(ctx.request.host);
|
|
const url = buildUrl(reqId, ctx.request.host);
|
|
log('making new client with id %s', reqId);
|
|
const info = await manager.newClient(reqId, url, domain);
|
|
ctx.body = buildResponse(info);
|
|
});
|
|
const server = http.createServer();
|
|
server.tunnelServer = tunnelServer;
|
|
const appCallback = app.callback();
|
|
server.on('request', (req, res) => {
|
|
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;
|
|
}
|
|
// Call the onRequest hook
|
|
if (opt.onRequest) {
|
|
opt.onRequest({
|
|
headers: req.headers,
|
|
method: req.method || 'GET',
|
|
path: req.url || '/',
|
|
remoteAddress: req.socket.remoteAddress,
|
|
tunnelId: clientId,
|
|
});
|
|
}
|
|
client.handleRequest(req, res);
|
|
});
|
|
server.on('upgrade', (req, socket) => {
|
|
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;
|
|
}
|
|
//# sourceMappingURL=server.js.map
|