openhim-core
Version:
The OpenHIM core application that provides logging and routing of http requests
626 lines (574 loc) • 19.2 kB
JavaScript
var Q, SDC, application, buildNonPrimarySendRequestPromise, config, containsMultiplePrimaries, cookie, domain, events, fs, getDestinationPath, handleServerError, http, https, isRouteEnabled, logger, messageStore, net, numberOfPrimaryRoutes, obtainCharset, os, sdc, sendHttpRequest, sendRequest, sendRequestToRoutes, sendSocketRequest, setCookiesOnContext, setKoaResponse, stats, statsdServer, tls, transformPath, util, utils, zlib;
util = require('util');
zlib = require('zlib');
http = require('http');
https = require('https');
net = require('net');
tls = require('tls');
Q = require('q');
config = require('../config/config');
config.mongo = config.get('mongo');
config.router = config.get('router');
logger = require('winston');
cookie = require('cookie');
fs = require('fs');
utils = require('../utils');
messageStore = require('../middleware/messageStore');
events = require('../middleware/events');
stats = require("../stats");
statsdServer = config.get('statsd');
application = config.get('application');
SDC = require('statsd-client');
os = require('os');
domain = (os.hostname()) + "." + application.name + ".appMetrics";
sdc = new SDC(statsdServer);
isRouteEnabled = function(route) {
return (route.status == null) || route.status === 'enabled';
};
exports.numberOfPrimaryRoutes = numberOfPrimaryRoutes = function(routes) {
var i, len, numPrimaries, route;
numPrimaries = 0;
for (i = 0, len = routes.length; i < len; i++) {
route = routes[i];
if (isRouteEnabled(route) && route.primary) {
numPrimaries++;
}
}
return numPrimaries;
};
containsMultiplePrimaries = function(routes) {
return numberOfPrimaryRoutes(routes) > 1;
};
setKoaResponse = function(ctx, response) {
var err, error, error1, key, ref, ref1, ref2, results, value;
if (typeof response.status === 'string') {
try {
response.status = parseInt(response.status);
} catch (error) {
err = error;
logger.error(err);
}
}
ctx.response.status = response.status;
ctx.response.timestamp = response.timestamp;
ctx.response.body = response.body;
if (!ctx.response.header) {
ctx.response.header = {};
}
if ((ref = ctx.request) != null ? (ref1 = ref.header) != null ? ref1["X-OpenHIM-TransactionID"] : void 0 : void 0) {
if ((response != null ? response.headers : void 0) != null) {
response.headers["X-OpenHIM-TransactionID"] = ctx.request.header["X-OpenHIM-TransactionID"];
}
}
ref2 = response.headers;
results = [];
for (key in ref2) {
value = ref2[key];
switch (key.toLowerCase()) {
case 'set-cookie':
results.push(setCookiesOnContext(ctx, value));
break;
case 'location':
if (response.status >= 300 && response.status < 400) {
results.push(ctx.response.redirect(value));
} else {
results.push(ctx.response.set(key, value));
}
break;
case 'content-type':
results.push(ctx.response.type = value);
break;
default:
try {
if (key !== 'content-encoding' && key !== 'transfer-encoding') {
results.push(ctx.response.set(key, value));
} else {
results.push(void 0);
}
} catch (error1) {
err = error1;
results.push(logger.error(err));
}
}
}
return results;
};
if (process.env.NODE_ENV === "test") {
exports.setKoaResponse = setKoaResponse;
}
setCookiesOnContext = function(ctx, value) {
var c_key, c_opts, c_vals, c_value, i, len, p_key, p_key_l, p_val, ref, results;
logger.info('Setting cookies on context');
results = [];
for (c_value = i = 0, len = value.length; i < len; c_value = ++i) {
c_key = value[c_value];
c_opts = {
path: false,
httpOnly: false
};
c_vals = {};
ref = cookie.parse(c_key);
for (p_key in ref) {
p_val = ref[p_key];
p_key_l = p_key.toLowerCase();
switch (p_key_l) {
case 'max-age':
c_opts['maxage'] = parseInt(p_val, 10);
break;
case 'expires':
c_opts['expires'] = new Date(p_val);
break;
case 'path':
case 'domain':
case 'secure':
case 'signed':
case 'overwrite':
c_opts[p_key_l] = p_val;
break;
case 'httponly':
c_opts['httpOnly'] = p_val;
break;
default:
c_vals[p_key] = p_val;
}
}
results.push((function() {
var results1;
results1 = [];
for (p_key in c_vals) {
p_val = c_vals[p_key];
results1.push(ctx.cookies.set(p_key, p_val, c_opts));
}
return results1;
})());
}
return results;
};
handleServerError = function(ctx, err, route) {
var ref;
ctx.autoRetry = true;
if (route) {
route.error = {
message: err.message,
stack: err.stack ? err.stack : void 0
};
} else {
ctx.response.status = 500;
ctx.response.timestamp = new Date();
ctx.response.body = "An internal server error occurred";
ctx.error = {
message: err.message,
stack: err.stack ? err.stack : void 0
};
}
logger.error("[" + ((ref = ctx.transactionId) != null ? ref.toString() : void 0) + "] Internal server error occured: " + err);
if (err.stack) {
return logger.error("" + err.stack);
}
};
sendRequestToRoutes = function(ctx, routes, next) {
var promise, promises;
promises = [];
promise = {};
ctx.timer = new Date;
if (containsMultiplePrimaries(routes)) {
return next(new Error("Cannot route transaction: Channel contains multiple primary routes and only one primary is allowed"));
}
return utils.getKeystore(function(err, keystore) {
var fn, i, len, route;
fn = function(route) {
var options, path;
if (!isRouteEnabled(route)) {
return;
}
path = getDestinationPath(route, ctx.path);
options = {
hostname: route.host,
port: route.port,
path: path,
method: ctx.request.method,
headers: ctx.request.header,
agent: false,
rejectUnauthorized: true,
key: keystore.key,
cert: keystore.cert.data,
secureProtocol: 'TLSv1_method'
};
if (route.cert != null) {
options.ca = keystore.ca.id(route.cert).data;
}
if (ctx.request.querystring) {
options.path += '?' + ctx.request.querystring;
}
if (options.headers && options.headers.authorization && !route.forwardAuthHeader) {
delete options.headers.authorization;
}
if (route.username && route.password) {
options.auth = route.username + ":" + route.password;
}
if (options.headers && options.headers.host) {
delete options.headers.host;
}
if (route.primary) {
ctx.primaryRoute = route;
promise = sendRequest(ctx, route, options).then(function(response) {
var ref, ref1, responseObj;
logger.info("executing primary route : " + route.name);
if (((ref = response.headers) != null ? (ref1 = ref['content-type']) != null ? ref1.indexOf('application/json+openhim') : void 0 : void 0) > -1) {
responseObj = JSON.parse(response.body);
ctx.mediatorResponse = responseObj;
if (responseObj.error != null) {
ctx.autoRetry = true;
ctx.error = responseObj.error;
}
return setKoaResponse(ctx, responseObj.response);
} else {
return setKoaResponse(ctx, response);
}
}).then(function() {
logger.info("primary route completed");
return next();
}).fail(function(reason) {
handleServerError(ctx, reason);
return next();
});
} else {
logger.info("executing non primary: " + route.name);
promise = buildNonPrimarySendRequestPromise(ctx, route, options, path).then(function(routeObj) {
var error;
logger.info("Storing non primary route responses " + route.name);
try {
if ((routeObj != null ? routeObj.name : void 0) == null) {
routeObj = {
name: route.name
};
}
if ((routeObj != null ? routeObj.response : void 0) == null) {
routeObj.response = {
status: 500,
timestamp: ctx.requestTimestamp
};
}
if ((routeObj != null ? routeObj.request : void 0) == null) {
routeObj.request = {
host: options.hostname,
port: options.port,
path: path,
headers: ctx.request.header,
querystring: ctx.request.querystring,
method: ctx.request.method,
timestamp: ctx.requestTimestamp
};
}
return messageStore.storeNonPrimaryResponse(ctx, routeObj, function() {
return stats.nonPrimaryRouteRequestCount(ctx, routeObj, function() {
return stats.nonPrimaryRouteDurations(ctx, routeObj, function() {});
});
});
} catch (error) {
err = error;
return logger.error(err);
}
});
}
return promises.push(promise);
};
for (i = 0, len = routes.length; i < len; i++) {
route = routes[i];
fn(route);
}
return (Q.all(promises)).then(function() {
return messageStore.setFinalStatus(ctx, function() {
var done, trxEvents;
logger.info("All routes completed for transaction: " + (ctx.transactionId.toString()));
if (ctx.routes) {
logger.debug("Storing route events for transaction: " + ctx.transactionId);
done = function(err) {
if (err) {
return logger.error(err);
}
};
trxEvents = [];
events.createSecondaryRouteEvents(trxEvents, ctx.transactionId, ctx.requestTimestamp, ctx.authorisedChannel, ctx.routes, ctx.currentAttempt);
return events.saveEvents(trxEvents, done);
}
});
});
});
};
buildNonPrimarySendRequestPromise = function(ctx, route, options, path) {
return sendRequest(ctx, route, options).then(function(response) {
var ref, ref1, responseObj, routeObj;
routeObj = {};
routeObj.name = route.name;
routeObj.request = {
host: options.hostname,
port: options.port,
path: path,
headers: ctx.request.header,
querystring: ctx.request.querystring,
method: ctx.request.method,
timestamp: ctx.requestTimestamp
};
if (((ref = response.headers) != null ? (ref1 = ref['content-type']) != null ? ref1.indexOf('application/json+openhim') : void 0 : void 0) > -1) {
responseObj = JSON.parse(response.body);
routeObj.mediatorURN = responseObj['x-mediator-urn'];
routeObj.orchestrations = responseObj.orchestrations;
routeObj.properties = responseObj.properties;
if (responseObj.metrics) {
routeObj.metrics = responseObj.metrics;
}
routeObj.response = responseObj.response;
} else {
routeObj.response = response;
}
if (!ctx.routes) {
ctx.routes = [];
}
ctx.routes.push(routeObj);
return routeObj;
}).fail(function(reason) {
var routeObj;
routeObj = {};
routeObj.name = route.name;
handleServerError(ctx, reason, routeObj);
return routeObj;
});
};
sendRequest = function(ctx, route, options) {
if (route.type === 'tcp' || route.type === 'mllp') {
logger.info('Routing socket request');
return sendSocketRequest(ctx, route, options);
} else {
logger.info('Routing http(s) request');
return sendHttpRequest(ctx, route, options);
}
};
obtainCharset = function(headers) {
var contentType, matches;
contentType = headers['content-type'] || '';
matches = contentType.match(/charset=([^;,\r\n]+)/i);
if (matches && matches[1]) {
return matches[1];
}
return 'utf-8';
};
/*
* A promise returning function that send a request to the given route and resolves
* the returned promise with a response object of the following form:
* response =
* status: <http_status code>
* body: <http body>
* headers: <http_headers_object>
* timestamp: <the time the response was recieved>
*/
sendHttpRequest = function(ctx, route, options) {
var defered, gunzip, inflate, method, response, routeReq;
defered = Q.defer();
response = {};
gunzip = zlib.createGunzip();
inflate = zlib.createInflate();
method = http;
if (route.secured) {
method = https;
}
routeReq = method.request(options, function(routeRes) {
var bufs, uncompressedBodyBufs;
response.status = routeRes.statusCode;
response.headers = routeRes.headers;
uncompressedBodyBufs = [];
if (routeRes.headers['content-encoding'] === 'gzip') {
routeRes.pipe(gunzip);
gunzip.on("data", function(data) {
uncompressedBodyBufs.push(data);
});
}
if (routeRes.headers['content-encoding'] === 'deflate') {
routeRes.pipe(inflate);
inflate.on("data", function(data) {
uncompressedBodyBufs.push(data);
});
}
bufs = [];
routeRes.on("data", function(chunk) {
return bufs.push(chunk);
});
return routeRes.on("end", function() {
var charset;
response.timestamp = new Date();
charset = obtainCharset(routeRes.headers);
if (routeRes.headers['content-encoding'] === 'gzip') {
return gunzip.on("end", function() {
var uncompressedBody;
uncompressedBody = Buffer.concat(uncompressedBodyBufs);
response.body = uncompressedBody.toString(charset);
if (!defered.promise.isRejected()) {
defered.resolve(response);
}
});
} else if (routeRes.headers['content-encoding'] === 'deflate') {
return inflate.on("end", function() {
var uncompressedBody;
uncompressedBody = Buffer.concat(uncompressedBodyBufs);
response.body = uncompressedBody.toString(charset);
if (!defered.promise.isRejected()) {
defered.resolve(response);
}
});
} else {
response.body = Buffer.concat(bufs);
if (!defered.promise.isRejected()) {
return defered.resolve(response);
}
}
});
});
routeReq.on("error", function(err) {
return defered.reject(err);
});
routeReq.on("clientError", function(err) {
return defered.reject(err);
});
routeReq.setTimeout(+config.router.timeout, function() {
return defered.reject("Request Timed Out");
});
if (ctx.request.method === "POST" || ctx.request.method === "PUT") {
routeReq.write(ctx.body);
}
routeReq.end();
return defered.promise;
};
/*
* A promise returning function that send a request to the given route using sockets and resolves
* the returned promise with a response object of the following form: ()
* response =
* status: <200 if all work, else 500>
* body: <the received data from the socket>
* timestamp: <the time the response was recieved>
*
* Supports both normal and MLLP sockets
*/
sendSocketRequest = function(ctx, route, options) {
var bufs, client, defered, method, mllpEndChar, requestBody, response;
mllpEndChar = String.fromCharCode(0x1c);
defered = Q.defer();
requestBody = ctx.body;
response = {};
method = net;
if (route.secured) {
method = tls;
}
options = {
host: options.hostname,
port: options.port,
rejectUnauthorized: options.rejectUnauthorized,
key: options.key,
cert: options.cert,
secureProtocol: options.secureProtocol,
ca: options.ca
};
client = method.connect(options, function() {
logger.info("Opened " + route.type + " connection to " + options.host + ":" + options.port);
if (route.type === 'tcp') {
return client.end(requestBody);
} else if (route.type === 'mllp') {
return client.write(requestBody);
} else {
return logger.error("Unkown route type " + route.type);
}
});
bufs = [];
client.on('data', function(chunk) {
bufs.push(chunk);
if (route.type === 'mllp' && chunk.toString().indexOf(mllpEndChar) > -1) {
logger.debug('Received MLLP response end character');
return client.end();
}
});
client.on('error', function(err) {
return defered.reject(err);
});
client.on('clientError', function(err) {
return defered.reject(err);
});
client.on('end', function() {
logger.info("Closed " + route.type + " connection to " + options.host + ":" + options.port);
if (route.secured && !client.authorized) {
return defered.reject(new Error('Client authorization failed'));
}
response.body = Buffer.concat(bufs);
response.status = 200;
response.timestamp = new Date();
if (!defered.promise.isRejected()) {
return defered.resolve(response);
}
});
return defered.promise;
};
getDestinationPath = function(route, requestPath) {
if (route.path) {
return route.path;
} else if (route.pathTransform) {
return transformPath(requestPath, route.pathTransform);
} else {
return requestPath;
}
};
/*
* Applies a sed-like expression to the path string
*
* An expression takes the form s/from/to
* Only the first 'from' match will be substituted
* unless the global modifier as appended: s/from/to/g
*
* Slashes can be escaped as \/
*/
exports.transformPath = transformPath = function(path, expression) {
var from, fromRegex, sExpression, sub, to;
sExpression = expression.replace(/\\\//g, ':');
sub = sExpression.split('/');
from = sub[1].replace(/:/g, '\/');
to = sub.length > 2 ? sub[2] : "";
to = to.replace(/:/g, '\/');
if (sub.length > 3 && sub[3] === 'g') {
fromRegex = new RegExp(from, 'g');
} else {
fromRegex = new RegExp(from);
}
return path.replace(fromRegex, to);
};
/*
* Gets the authorised channel and routes
* the request to all routes within that channel. It updates the
* response of the context object to reflect the response recieved from the
* route that is marked as 'primary'.
*
* Accepts (ctx, next) where ctx is a [Koa](http://koajs.com/) context
* object and next is a callback that is called once the route marked as
* primary has returned an the ctx.response object has been updated to
* reflect the response from that route.
*/
exports.route = function(ctx, next) {
var channel;
channel = ctx.authorisedChannel;
return sendRequestToRoutes(ctx, channel.routes, next);
};
/*
* The [Koa](http://koajs.com/) middleware function that enables the
* router to work with the Koa framework.
*
* Use with: app.use(router.koaMiddleware)
*/
exports.koaMiddleware = function*(next) {
var route, startTime;
if (statsdServer.enabled) {
startTime = new Date();
}
route = Q.denodeify(exports.route);
(yield route(this));
if (statsdServer.enabled) {
sdc.timing(domain + ".routerMiddleware", startTime);
}
return (yield next);
};
//# sourceMappingURL=router.js.map