reso-api-reverse-proxy
Version:
A Reverse Proxy for a RESO API Server
711 lines (652 loc) • 23.3 kB
JavaScript
//
// includes
//
var accessTokenCache = {}
, responseCache = {}
, metadataCache = {};
var DEBUG_HEADERS = false;
//
// module defines
//
function createReverseProxy(configurationFile){
//
// read configuration file
//
var fs = require("fs");
var aConfigFile = configurationFile || "./proxy.conf";
console.log("");
fs.exists(aConfigFile, function(exists) {
if (!exists) {
console.log("Configuration file " + aConfigFile + " missing");
console.log("");
process.exit(0);
} else {
console.log("Using configuration file " + aConfigFile);
var contents = fs.readFileSync(aConfigFile).toString().split("\n");
var i;
var userConfig = {};
for(i in contents) {
var line = contents[i];
var data = line.split(":");
if (data.length != 1) {
if (line.substring(0,1) != "#") {
var aValue = data[1].trim().toUpperCase();
switch (aValue) {
case "false":
aValue = false;
break;
case "FALSE":
aValue = false;
break;
case "true":
aValue = true;
break;
case "TRUE":
aValue = true;
break;
default:
aValue = data[1].trim();
}
userConfig[data[0]] = aValue;
}
}
}
var getIPAddress = function() {
var interfaces = require('os').networkInterfaces();
for (var devName in interfaces) {
var iface = interfaces[devName];
for (var i = iface.length; i--;) {
var alias = iface[i];
if (alias.family === 'IPv4' && alias.address !== '127.0.0.1' && !alias.internal) {
return alias.address;
}
}
}
return '0.0.0.0';
}
if (!userConfig["LISTENING_DOMAIN"]) {
userConfig["LISTENING_DOMAIN"] = getIPAddress();
}
startReverseProxy(userConfig);
}
});
}; // createReverseProxy
function startReverseProxy(userConfig) {
var btoa = require("btoa")
, crypto = require("crypto")
, http = require("http")
, https = require("https")
, randomstring = require("just.randomstring")
, cnonce = randomstring(16);
//
// banner processing
//
var bannerWidth = 78;
function bannerTop() {
var bannerText = ".";
for (var i = bannerWidth; i--;) {
bannerText += "-";
}
bannerText += ".";
console.log(bannerText);
}
function bannerSpacer() {
var bannerText = "|";
for (var i = bannerWidth; i--;) {
bannerText += "-";
}
bannerText += "|";
console.log(bannerText);
}
function bannerLine(text) {
if (!text) {
text = "";
}
var bannerText = "| " + text;
for (var i = (bannerWidth-text.length-1); i--;) {
bannerText += " ";
}
bannerText += "|";
console.log(bannerText);
}
function bannerBottom() {
var bannerText = "'";
for (var i = bannerWidth; i--;) {
bannerText += "-";
}
bannerText += "'";
console.log(bannerText);
}
http.createServer(function(request, response) {
var startTime = new Date();
//
// items to keep around during the proxy
//
var post_body;
var uri;
//
// timers
//
var timeout;
var fn;
function timeout_wrapper(req) {
return function( ) { req.abort(); };
};
//
// error processor
//
function errorResponse(text) {
var tempHeaders = {
"host": userConfig["LISTENING_DOMAIN"] + ":" + userConfig["LISTENING_PORT"],
"connection": "close"
};
if (request.headers["origin"]) {
tempHeaders["access-control-allow-origin"] = request.headers["origin"];
} else {
tempHeaders["access-control-allow-origin"] = request.connection.remoteAddress;
}
response.writeHead(503, tempHeaders);
response.write(text);
response.end();
bannerTop();
bannerLine(text);
bannerBottom();
}
function constructDigestResponse(realm, method, nonce, nc, qop) {
/*
var md5 = function (str, encoding){
var crypto = require("crypto");
return crypto.createHash('md5').update(str, 'utf8').digest(encoding || 'hex');
};
var A1 = userConfig["ACCOUNT"] + ":" + realm + ":" + userConfig["PASSWORD"];
var HA1 = md5(A1);
var A2 = method + ":" + uri;
var HA2 = md5(A2);
var response_text = HA1 + ":" + nonce + ":" + nc + ":" + cnonce + ":" + qop + ":" + HA2;
var response = md5(response_text);
*/
var HA1 = crypto.createHash("md5").update(userConfig["ACCOUNT"] + ":" + realm + ":" + userConfig["PASSWORD"], "utf8").digest("hex");
var HA2 = crypto.createHash("md5").update(method + ":" + uri, "utf8").digest("hex");
var response = crypto.createHash("md5").update(HA1 + ":" + nonce + ":" + nc + ":" + cnonce + ":" + qop + ":" + HA2, "utf8").digest("hex");
//
// keep a copy to replay while incrementing nc
//
responseCache[uri] = {
"realm": realm,
"qop": qop,
"nonce": nonce,
"nc": nc
}
return 'Digest username="' + userConfig["ACCOUNT"] + '", realm="' + realm + '", nonce="' + nonce + '", uri="' + uri + '", response="' + response + '", qop=' + qop + ', nc=' + nc + ', cnonce="' + cnonce + '"';
} // constructDigestRespnse
function doTransfer(authHeader) {
var tempHeaders = {
"host": userConfig["LISTENING_DOMAIN"] + ":" + userConfig["LISTENING_PORT"],
"user-agent": userConfig["USER_AGENT"] + "/1.0",
"x-forwarded-for": request.connection.remoteAddress,
"accept": request.headers["accept"]
}
if (request.headers["content-type"]) {
tempHeaders["content-type"] = request.headers["content-type"];
}
if (request.headers["origin"]) {
tempHeaders["x-forwarded-host"] = request.headers["origin"];
}
if (request.headers["accept-language"]) {
tempHeaders["accept-language"] = request.headers["accept-language"];
}
if (userConfig["COMPRESSION"]) {
if (request.headers["accept-encoding"]) {
tempHeaders["accept-encoding"] = request.headers["accept-encoding"];
}
// headers["accept-encoding"] = "deflate";
// headers["accept-encoding"] = "gzip";
}
if (authHeader) {
tempHeaders["authorization"] = authHeader;
} else {
tempHeaders["authorization"] = "Basic " + btoa(userConfig["ACCOUNT"] + ":" + userConfig["PASSWORD"]);
}
if (post_body) {
if (post_body.length > 0) {
tempHeaders["content-length"] = Buffer.byteLength(post_body);
}
}
if (DEBUG_HEADERS) {
console.log("SENT_TO_SERVER");
console.dir(tempHeaders);
}
var options = {
host: userConfig["API_DOMAIN"],
port: userConfig["API_PORT"],
path: request.url,
method: request.method,
rejectUnauthorized: false,
headers: tempHeaders
};
var connectionProtocol = http;
if (userConfig["API_PROTOCOL"] == "https") {
connectionProtocol = https;
}
var clientRequest = connectionProtocol.request(options, function(clientResponse) {
if (DEBUG_HEADERS) {
console.log("RECEIVED_FROM_SERVER");
console.dir(clientResponse.headers);
}
processBody(clientResponse, authHeader);
});
if (post_body) {
if (post_body.length > 0) {
clientRequest.write(post_body);
}
}
clientRequest.end();
clientRequest.on('error', function(e) {
//console.trace(e);
errorResponse("MLS Connection " + e)
});
fn = timeout_wrapper(clientRequest);
timeout = setTimeout(fn,userConfig["LISTENING_TIMEOUT"]);
} // doTransfer
function processBody(clientResponse, forceBasic) {
var authType = userConfig["AUTH_TYPE"];
if (forceBasic) {
authType = "Basic";
}
var msg = "";
var useGzip = false;
var useInflate = false;
if (clientResponse.headers["content-encoding"]) {
if (clientResponse.headers["content-encoding"].indexOf("gzip") > -1 ) useGzip = true;
if (clientResponse.headers["content-encoding"].indexOf("deflate") > -1 ) useInflate = true;
}
function digestResponseFromHeader(cnonce) {
if (clientResponse.headers["www-authenticate"]) {
var authorization = clientResponse.headers["www-authenticate"];
//
// Determine if server is configured for Digest authorization
//
if (authorization.split(" ")[0] !== "Digest") {
console.log("Server is not configured for Digest");
} else {
var parts = authorization.substring(authorization.indexOf(" ")).split(",");
//
// determine if the correct number of arguments is in the Digest header
//
if (parts.length !== 3) {
console.log("Server responded to Digest with an unexpected number of arguments");
} else {
var realm;
var nonce;
var qop;
for (var i = parts.length; i--;) {
var pieces = parts[i].split("=");
switch(pieces[0].trim()) {
case "realm":
realm = pieces[1].replace(/["']/g, "");
break;
case "nonce":
nonce = pieces[1].replace(/["']/g, "");
break;
case "qop":
qop = pieces[1].replace(/["']/g, "");
break;
}
}
return constructDigestResponse(realm, request.method, nonce, 0, qop);
}
}
}
return false;
} // digestResponseFromHeader
function completeTransfer() {
clearTimeout(timeout);
switch (authType) {
case "Basic":
if (clientResponse.headers["www-authenticate"]) {
errorResponse("Bad MLS Credentials");
} else {
var headers = {
"access-control-allow-origin": (request.headers["origin"] || request.connection.remoteAddress),
"access-control-max-age": userConfig["CACHE_LATENCY"],
"cache-control": "max-age=" + userConfig["CACHE_LATENCY"],
"connection": clientResponse.headers["connection"]
};
if (clientResponse.headers["content-type"]) {
headers["content-type"] = clientResponse.headers["content-type"];
}
var writeResultWithLength = function(returnedResult, encoding) {
if (encoding) {
headers["content-encoding"] = encoding;
}
if (returnedResult.length > 0) {
headers["content-length"] = returnedResult.length;
}
response.writeHead(clientResponse.statusCode, headers);
response.write(returnedResult);
response.end();
var aURL = unescape(request.url);
if (DEBUG_HEADERS) {
console.log("RETURN_TO_BROWSER");
console.dir(headers);
console.log(request.method + " " + aURL + " received from " + request.connection.remoteAddress + " consuming " + ((new Date().getTime()) - startTime.getTime()) + " ms");
} else {
if (aURL.indexOf("$metadata") !== -1) {
if (userConfig["CACHE_METADATA"]) {
//
// cache metadata requests and don't display the transfer
//
var anOrigin = headers["access-control-allow-origin"];
metadataCache[anOrigin]["headers"] = headers;
metadataCache[anOrigin]["response"] = returnedResult;
}
} else {
//
// display the transfer
//
var methodName = "Unknown";
switch (request.method) {
case "DELETE":
methodName = "Delete";
break;
case "GET":
methodName = "Query";
break;
case "PATCH":
methodName = "Update";
break;
case "POST":
methodName = "Add";
break;
}
console.log((new Date()) + " " + methodName + " from " + request.connection.remoteAddress + " consuming " + ((new Date().getTime()) - startTime.getTime()) + " ms");
}
}
} // writeResultWithLength
if (useGzip || useInflate) {
headers["vary"] = "Accept-Encoding";
var zlib = require('zlib');
if (useGzip) {
zlib.gzip(msg, function(err, result) {
if (!err) {
writeResultWithLength(result, "gzip");
}
});
} else {
zlib.deflate(msg, function(err, result) {
if (!err) {
writeResultWithLength(result, "deflate");
}
});
}
} else {
writeResultWithLength(msg);
}
}
break;
case "Digest":
var authHeader = digestResponseFromHeader(cnonce);
if (authHeader) {
doTransfer(authHeader);
} else {
errorResponse("MLS Credentialing is failing");
}
break;
} // authType switch
} // completeTransfer
var clientMethod;
if (useGzip || useInflate) {
if (useGzip) {
clientMethod = require('zlib').createGunzip();
} else {
clientMethod = require('zlib').createInflate();
}
clientResponse.pipe(clientMethod);
} else {
clientMethod = clientResponse;
}
clientMethod.on("data", function (chunk) {
msg += chunk;
clearTimeout(timeout);
timeout = setTimeout(fn, userConfig["LISTENING_TIMEOUT"]);
});
clientMethod.on("end", function () {
completeTransfer();
});
clientMethod.on("error", function () {
clearTimeout(timeout);
errorResponse("RESPONSE ERROR");
});
} // processBody
switch (request.method) {
case "OPTIONS":
var headers = {
"access-control-allow-origin": request.headers["origin"],
"access-control-allow-credentials": "false",
"access-control-max-age": userConfig["CACHE_LATENCY"],
"cache-control": "max-age=" + userConfig["CACHE_LATENCY"],
"access-control-allow-methods": request.headers["access-control-request-method"],
"access-control-allow-headers": request.headers["access-control-request-headers"]
};
response.writeHead(200, headers);
response.end();
break;
case "DELETE":
case "GET":
if (userConfig["CACHE_METADATA"]) {
if (unescape(request.url).indexOf("$metadata") !== -1) {
var anOrigin = request.headers["origin"] || request.connection.remoteAddress;
if (metadataCache[anOrigin]) {
if (((new Date().getTime()) - metadataCache[anOrigin]["age"]) < (userConfig["CACHE_LATENCY"] * 1000)) {
response.writeHead(200, metadataCache[anOrigin]["headers"]);
response.write(metadataCache[anOrigin]["response"]);
response.end();
break;
}
}
metadataCache[anOrigin] = {
"age": (new Date().getTime())
}
}
}
case "PATCH":
case "POST":
var request_body = "";
request.on("data", function (chunk) {
request_body += chunk;
if (request_body.length > 1e6) {
// FLOOD ATTACK OR FAULTY CLIENT, NUKE REQUEST
request.connection.destroy();
}
});
request.on('end', function () {
post_body = request_body;
delete request.headers["host"];
if (DEBUG_HEADERS) {
console.log("RECEIVED_FROM_BROWSER");
console.dir(request.headers);
}
switch (userConfig["AUTH_TYPE"]) {
case "Basic":
doTransfer();
break;
case "Digest":
function uriFromRequest() {
var pos = request.url.indexOf("/$");
if (pos == -1) {
pos = request.url.indexOf("?$");
}
if (pos == -1) {
pos = request.url.indexOf("('");
if (pos == -1) {
return request.url;
}
}
return request.url.substring(0,pos);
}
uri = uriFromRequest();
if (responseCache[uri]) {
doTransfer(
constructDigestResponse(
responseCache[uri]["realm"],
request.method,
responseCache[uri]["nonce"],
responseCache[uri]["nc"] + 1,
responseCache[uri]["qop"]
)
);
} else {
doTransfer();
}
break;
/*
case "OAuth2":
var uri = uriFromRequest(request);
//console.log("--------------");
//console.log(request.url);
//console.log(uri);
//console.log("--------------");
var cachedAccessToken = accessTokenCache[uri];
if (cachedAccessToken != null) {
console.dir(cachedAccessToken);
// var tokenHeader = tokenResponseFromCache(tokenCache, uri);
//console.dir("--------------");
//console.log(tokenHeader);
//console.dir("--------------");
// if (tokenHeader !== false) {
// completeToken(tokenHeader, request, response);
// }
} else {
function constructProxyHeaders() {
var headers = {
"host": userConfig["LISTENING_DOMAIN"] + ":" + userConfig["LISTENING_PORT"],
"user-agent": userConfig["USER_AGENT"] + "/1.0",
"x-forwarded-for": request.connection.remoteAddress,
"accept": request.headers["accept"]
}
if (request.headers["content-type"]) {
headers["content-type"] = request.headers["content-type"];
}
if (request.headers["origin"]) {
headers["x-forwarded-host"] = request.headers["origin"];
}
if (request.headers["accept-language"]) {
headers["accept-language"] = request.headers["accept-language"];
}
if (userConfig["COMPRESSION"]) {
if (request.headers["accept-encoding"]) {
headers["accept-encoding"] = request.headers["accept-encoding"];
}
// headers["accept-encoding"] = "deflate";
// headers["accept-encoding"] = "gzip";
}
return headers;
}
var tempHeaders = constructProxyHeaders(request);
var options = {
host: userConfig["AUTH_DOMAIN"],
port: userConfig["AUTH_PORT"],
path: request.url,
headers: tempHeaders,
method: request.method
};
var msg = "";
var clientHeaders;
if (userConfig["API_PROTOCOL"] == "https") {
var clientRequest = https.request(options, function(clientResponse) {
clientHeaders = clientResponse.headers;
clientResponse.on("data", function (chunk) {
msg += chunk;
});
clientResponse.on("end", function () {
console.dir(clientHeaders);
});
});
} else {
var clientRequest = http.request(options, function(clientResponse) {
clientHeaders = clientResponse.headers;
clientResponse.on("data", function (chunk) {
msg += chunk;
});
clientResponse.on("end", function () {
console.dir(clientHeaders);
});
});
}
clientRequest.end();
}
break;
*/
} // AUTH_TYPE switch
}); // request on end
break; // DELETE, GET, PATCH, PUT
default:
errorResponse("Unhandled Reqest Type: " + request.method);
} // method.type switch
}).listen(userConfig["LISTENING_PORT"], userConfig["LISTENING_DOMAIN"]); // createServer
var projectName = "RESO API Reverse Proxy";
var serverName = userConfig["SERVER_NAME"] || projectName;
bannerTop();
var packageName = projectName + " Version " + require('./package').version;
if (serverName == projectName ) {
bannerLine(packageName);
} else {
bannerLine(serverName);
bannerLine("(" + packageName + ")");
}
bannerSpacer();
if (userConfig["API_PROTOCOL"] == "https") {
if (userConfig["SELF_SIGNED"] == null) {
userConfig["SELF_SIGNED"] = false;
}
if (userConfig["SELF_SIGNED"]) {
bannerLine("- Server is not production safe because it is using self signed certificates");
}
}
bannerLine("- Using " + userConfig["AUTH_TYPE"] + " Authentication Scheme");
switch (userConfig["AUTH_TYPE"]) {
case "Basic":
break;
case "Digest":
bannerLine(" > Digest cnonce: " + cnonce);
break;
case "OAuth2":
break;
default:
bannerLine("- Unrecognized Authentication Scheme '" + userConfig["AUTH_TYPE"] + "' specified in the configuration file");
process.exit(0);
}
if (userConfig["COMPRESSION"] == null) {
userConfig["COMPRESSION"] = true;
bannerLine("- Configuration value for COMPRESSION not found");
bannerLine(" > Using default of " + userConfig["COMPRESSION"]);
}
if (userConfig["COMPRESSION"]) {
bannerLine("- Output compression will ALWAYS be attempted");
} else {
bannerLine("- Output will NEVER be compressed");
}
if (!userConfig["LISTENING_TIMEOUT"] == null) {
userConfig["LISTENING_TIMEOUT"] = 10000;
bannerLine("- Configuration value for LISTENING_TIMEOUT not found");
bannerLine(" > Using default of 1" + userConfig["LISTENING_TIMEOUT"] + " ms");
}
if (userConfig["USER_AGENT"] == null) {
userConfig["USER_AGENT"] = serverName;
bannerLine("- Configuration value for USER_AGENT not found");
bannerLine(" > Using default of \"" + userConfig["USER_AGENT"] + "\"");
}
if (userConfig["CACHE_LATENCY"]) {
bannerLine("- Recipient information is only current for " + userConfig["CACHE_LATENCY"] + " seconds");
if (userConfig["CACHE_METADATA"]) {
bannerLine(" > Metadata requests will be cached");
} else{
bannerLine(" > Metadata requests are processed even if information is still current");
}
}
bannerLine();
bannerLine("Listening for requests on http://" + userConfig["LISTENING_DOMAIN"] + ":" + userConfig["LISTENING_PORT"]);
bannerLine("Passing requests to " + userConfig["API_PROTOCOL"] + "://" + userConfig["API_DOMAIN"] + ":" + userConfig["API_PORT"]);
bannerBottom();
}; // startReverseProxy
module.exports = createReverseProxy;