UNPKG

cranker-router

Version:
310 lines (262 loc) 12.6 kB
// A Cranker Router with Express -*- js-indent-level: 4 -*- const http = require("http"); const https = require("https"); const express = require("express"); const expressWsModule = require("express-ws"); const EventEmitter = require('events'); const getRawBody = require("raw-body"); const mapping = require("./mapping.js"); function getRemoteAddr(request) { const ip = request.headers["x-forwarded-for"] || request.connection.remoteAddress || request.socket.remoteAddress || request.connection.socket.remoteAddress; const remotePort = request.connection.remotePort; const remoteAddr = ip + ":" + remotePort; return remoteAddr; } // If options is an object then use callback... // ... but if options is a function and callback is not present then options is a callback function chooseCallback(options, callback) { return callback !== undefined && typeof(callback) === "function" ? callback : typeof(options) === "function" ? options : undefined; } const sockAddr = function (req) { return req.socket.remoteAddress + ":" + req.socket.remotePort; } const debug = process.env["CRANKER_ROUTER_DEBUG"] === "1" || false; const log = function () { if (debug) { console.log.apply(null, arguments); } }; function router(port, options, listenerCallback) { const opts = options !== undefined && typeof(options) === "object" ? options : {}; const requestedRouterPort = port; const routerListenerCallback = chooseCallback(options, listenerCallback); const { crankerPort: optsCrankerPort, pingDebug = false, waitOnCrankerTimeout = 8000, routerTlsOpts, crankerTlsOpts } = opts; const crankerPort = optsCrankerPort !== undefined ? optsCrankerPort : 0; const { key: privateKeyPem, cert, ca } = routerTlsOpts || {}; const { key: crankerPrivateKeyPem, cert: crankerCert, ca: crankerCa } = crankerTlsOpts || {}; // Initialize the routeList const routeList = {}; // Initialize the app that will present the routing front end const routerApp = express(); let routerRouteInitialized = false; const crankerApp = express(); const crankerServer = crankerTlsOpts !== undefined ? https.createServer(crankerTlsOpts, crankerApp) : http.createServer(crankerApp); const crankerWs = expressWsModule(crankerApp, crankerServer); // Yes, we throw this away: as per spec. return new Promise((routerResolve, routerReject) => { crankerApp.ws("/register", function (ws, req) { if (!routerRouteInitialized) { routerApp.all("*", async function (req, response, next) { const {method, headers, path, url} = req; const {routePath, wsList} = mapping.pathMatch(path, routeList, next); if (wsList === undefined) { // The route isn't registered return next(); } // Here, the route must be registered.... but is it empty? if (wsList.length < 1) { await new Promise((resolve, reject) => { const start = new Date().valueOf(); const wsListInterval = setInterval(evt => { log("router waiting..."); wsList = routeList[routePath]; if (wsList !== undefined && wsList.length > 0) { clearInterval(wsListInterval); resolve(); } const now = new Date().valueOf(); if (now - start > waitOnCrankerTimeout) { log(`router waited ${now - start} ms for a registered route`, path); clearInterval(wsListInterval); reject(); } }, 100); }).catch(e => log("router http acceptor waiting error!", routePath, path)); } if (wsList.length === 0) { return res.sendStatus(504); // Gateway timeout } if (wsList === undefined) { return next(); } // We should do something here if the list is empty const wsObj = wsList.shift(); log("router ws shifted wsObj", wsObj.addr, "wsList remainder", wsList.map(w => w.addr), "routeList", Object.keys(routeList).length, "path", path); const {addr, ws: crankerWs } = wsObj; // We remove from the pool try { const requestLength = req.headers["content-length"]; const headerLines = Object.keys(headers) .map(name => name + ": " + headers[name]); const headerString = headerLines.join("\n"); const bodyMarker = requestLength === undefined ? "_2" : "_1"; const packet = `${method} ${url} 1.1\n${headerString}\n\n${bodyMarker}`; crankerWs.crankId = new Date().valueOf(); crankerWs.crankerRouterResponse = response; crankerWs.send(packet); if (requestLength !== undefined) { const packet = await getRawBody(req, { length: requestLength }); crankerWs.send(packet); crankerWs.send("_3"); } } catch (e) { log("cranker-router: error handling router request", e); } }); routerRouteInitialized = true; } const socketAddress = sockAddr(req); log("router register received!", socketAddress); ws.on("ping", evt => { ws.pong(); if (pingDebug) { log("cranker-router: ping"); } }); try { let responseHeadersReceived = false; const protocol = req.headers["crankerprotocol"]; const route = req.headers["route"]; const addr = getRemoteAddr(req); let cranker = routeList[route]; if (cranker === undefined) { cranker = [] routeList[route] = cranker; } const wsDescription = {addr: addr, ws: ws} cranker.push(wsDescription); routeList[route] = cranker; log("router register continues - cranker count", socketAddress, route, routeList[route].map(w => w.addr)); ws.on("error", function (err) { console.log("ROUTER ERROR KLAXON"); }); ws.on("message", function (msg) { if (!responseHeadersReceived) { const response = new String(msg); const [responseLine, ...responseHeaderLines] = response.split("\n"); const header = {}; const responseHeaders = responseHeaderLines // FIXME - need a better http header parser .filter(line => line.indexOf(":") > -1) .map(line => new RegExp("([A-Za-z0-9-_]+):[ ]*(.*)").exec(line).slice(1,3)) .forEach(([name, value]) => header[name] = value); const [httpVersion, statusCode, statusMessage] = responseLine.split(" "); ws.crankerRouterResponse.writeHead(statusCode, header); responseHeadersReceived = true; } else { // log("ws received extra for", ws.crankId, msg); ws.crankerRouterResponse.write(msg); } }); ws.on("close", function (evt) { log("router ws close called", ws._socket.remotePort); const index = cranker.indexOf(wsDescription); log("router ws close index to remove", index, ws._socket.remotePort); log("router ws close before splice", ws._socket.remotePort, cranker.map(w=>w.addr)); if (index > -1) { cranker.splice(index, 1); } log("router ws close after splice", ws._socket.remotePort, cranker.map(w=>w.addr)); if (ws.crankerRouterResponse !== undefined) { ws.crankerRouterResponse.end(); } }); log("router: connector registered route>", route); } catch (e) { log("router: error handling cranker reg", e); ws.close(); } }); log("router: starting cranker listener xxx"); const crankerListener = crankerServer.listen(crankerPort, async () => { const crankerAddress = crankerListener.address(); const crankerPort = crankerAddress.port; log("cranker-router about to listen on", requestedRouterPort); const routerServer = routerTlsOpts !== undefined ? https.createServer(routerTlsOpts, routerApp) : http.createServer(routerApp); const routerListener = routerServer.listen(requestedRouterPort, async () => { class Router extends EventEmitter { constructor() { super(); } getListener() { return routerListener; } getCrankerListener() { return crankerListener; } getCrankedRoutes () { const routePaths = Object.keys(routeList); const newObj = (a,b) => { // Apparently we don't have obj initializers in this version const x = {}; x[a] = b; return x; }; const routesRegistered = routePaths.map( p => newObj(p, routeList[p].map(a => a.addr)) ); if (routesRegistered.length < 1) { return []; } const routesWithUsers = routesRegistered.reduce( (a,b) => Object.assign(a, b) ); return routesWithUsers; } addStatusRoute(path) { const routerObj = this; routerApp.get(path, (req, res) => { const routes = routerObj.getCrankedRoutes(); res.json(routes); }); } get() { return routerApp.get.apply(routerApp, arguments); } post() { return routerApp.post.apply(routerApp, arguments); } delete() { return routerApp.delete.apply(routerApp, arguments); } close() { const routerListenerClose = routerListener.close(); const crankerListenerClose = crankerListener.close(); } } const routerObject = new Router(); if (routerListenerCallback !== undefined) { routerCallback(routerObject); } else { routerResolve(routerObject); } }); }); }); } module.exports = router; // End