cranker-router
Version:
A cranker router in Javascript.
310 lines (262 loc) • 12.6 kB
JavaScript
// 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