From 2fcc382b384ca13445f6cc49d19035804311efae Mon Sep 17 00:00:00 2001 From: Jim Heising Date: Mon, 26 Dec 2016 15:00:28 -0800 Subject: [PATCH] Added cluster support --- config.js | 3 +- server.js | 276 ++++++++++++++++++++++++++---------------------------- 2 files changed, 133 insertions(+), 146 deletions(-) diff --git a/config.js b/config.js index c163ade..692bf1b 100644 --- a/config.js +++ b/config.js @@ -5,4 +5,5 @@ exports.proxy_request_timeout_ms = 10000; // The lenght of time we'll wait for a exports.max_request_length = 100000; // The maximum length of characters allowed for a request or a response. exports.enable_rate_limiting = true; exports.max_requests_per_second = 10; // The maximum number of requests per second to allow from a given IP. -exports.blacklist_hostname_regex = /^(10\.|192\.|127\.|localhost$)/i; // Good for limiting access to internal IP addresses and hosts. \ No newline at end of file +exports.blacklist_hostname_regex = /^(10\.|192\.|127\.|localhost$)/i; // Good for limiting access to internal IP addresses and hosts. +exports.cluster_process_count = Number(process.env.CLUSTER_PROCESS_COUNT) || require("os").cpus().length; \ No newline at end of file diff --git a/server.js b/server.js index 228b603..f454017 100644 --- a/server.js +++ b/server.js @@ -2,205 +2,191 @@ var http = require('http'); var config = require("./config"); var url = require("url"); var request = require("request"); +var cluster = require('cluster'); var throttle = require("tokenthrottle")({rate: config.max_requests_per_second}); var publicAddressFinder = require("public-address"); var publicIP; // Get our public IP address -publicAddressFinder(function(err, data){ - if(!err && data) - { - publicIP = data.address; - } +publicAddressFinder(function (err, data) { + if (!err && data) { + publicIP = data.address; + } }); -function addCORSHeaders(req, res) -{ - if (req.method.toUpperCase() === "OPTIONS") - { - if(req.headers["access-control-request-headers"]) - { - res.setHeader("Access-Control-Allow-Headers", req.headers["access-control-request-headers"]); - } +function addCORSHeaders(req, res) { + if (req.method.toUpperCase() === "OPTIONS") { + if (req.headers["access-control-request-headers"]) { + res.setHeader("Access-Control-Allow-Headers", req.headers["access-control-request-headers"]); + } - if(req.headers["access-control-request-method"]) - { - res.setHeader("Access-Control-Allow-Methods", req.headers["access-control-request-method"]); - } - } + if (req.headers["access-control-request-method"]) { + res.setHeader("Access-Control-Allow-Methods", req.headers["access-control-request-method"]); + } + } - if(req.headers["origin"]) - { - res.setHeader("Access-Control-Allow-Origin", req.headers["origin"]); - } - else - { - res.setHeader("Access-Control-Allow-Origin", "*"); - } + if (req.headers["origin"]) { + res.setHeader("Access-Control-Allow-Origin", req.headers["origin"]); + } + else { + res.setHeader("Access-Control-Allow-Origin", "*"); + } } function writeResponse(res, httpCode, body) { - res.statusCode = httpCode; - res.end(body); + res.statusCode = httpCode; + res.end(body); } function sendInvalidURLResponse(res) { - return writeResponse(res, 404, "url must be in the form of /fetch/{some_url_here}"); + return writeResponse(res, 404, "url must be in the form of /fetch/{some_url_here}"); } function sendTooBigResponse(res) { - return writeResponse(res, 413, "the content in the request or response cannot exceed " + config.max_request_length + " characters."); + return writeResponse(res, 413, "the content in the request or response cannot exceed " + config.max_request_length + " characters."); } function getClientAddress(req) { - return (req.headers['x-forwarded-for'] || '').split(',')[0] - || req.connection.remoteAddress; + return (req.headers['x-forwarded-for'] || '').split(',')[0] + || req.connection.remoteAddress; } -function processRequest(req, res) -{ - addCORSHeaders(req, res); +function processRequest(req, res) { + addCORSHeaders(req, res); - // Return options pre-flight requests right away - if (req.method.toUpperCase() === "OPTIONS") - { - return writeResponse(res, 204); - } + // Return options pre-flight requests right away + if (req.method.toUpperCase() === "OPTIONS") { + return writeResponse(res, 204); + } - var result = config.fetch_regex.exec(req.url); + var result = config.fetch_regex.exec(req.url); - if (result && result.length == 2 && result[1]) { - var remoteURL; + if (result && result.length == 2 && result[1]) { + var remoteURL; - try { - remoteURL = url.parse(decodeURI(result[1])); - } - catch (e) { - return sendInvalidURLResponse(res); - } + try { + remoteURL = url.parse(decodeURI(result[1])); + } + catch (e) { + return sendInvalidURLResponse(res); + } - // We don't support relative links - if(!remoteURL.host) - { - return writeResponse(res, 404, "relative URLS are not supported"); - } + // We don't support relative links + if (!remoteURL.host) { + return writeResponse(res, 404, "relative URLS are not supported"); + } - // Naughty, naughty— deny requests to blacklisted hosts - if(config.blacklist_hostname_regex.test(remoteURL.hostname)) - { - return writeResponse(res, 400, "naughty, naughty..."); - } + // Naughty, naughty— deny requests to blacklisted hosts + if (config.blacklist_hostname_regex.test(remoteURL.hostname)) { + return writeResponse(res, 400, "naughty, naughty..."); + } - // We only support http and https - if (remoteURL.protocol != "http:" && remoteURL.protocol !== "https:") { - return writeResponse(res, 400, "only http and https are supported"); - } + // We only support http and https + if (remoteURL.protocol != "http:" && remoteURL.protocol !== "https:") { + return writeResponse(res, 400, "only http and https are supported"); + } - if(publicIP) - { - // Add an X-Forwarded-For header - if(req.headers["x-forwarded-for"]) - { - req.headers["x-forwarded-for"] += ", " + publicIP; - } - else - { - req.headers["x-forwarded-for"] = req.clientIP + ", " + publicIP; - } - } + if (publicIP) { + // Add an X-Forwarded-For header + if (req.headers["x-forwarded-for"]) { + req.headers["x-forwarded-for"] += ", " + publicIP; + } + else { + req.headers["x-forwarded-for"] = req.clientIP + ", " + publicIP; + } + } // Make sure the host header is to the URL we're requesting, not thingproxy - if(req.headers["host"]) { + if (req.headers["host"]) { req.headers["host"] = remoteURL.host; } - var proxyRequest = request({ - url: remoteURL, - headers: req.headers, - method: req.method, - timeout: config.proxy_request_timeout_ms, - strictSSL : false - }); + var proxyRequest = request({ + url: remoteURL, + headers: req.headers, + method: req.method, + timeout: config.proxy_request_timeout_ms, + strictSSL: false + }); - proxyRequest.on('error', function(err){ + proxyRequest.on('error', function (err) { - if(err.code === "ENOTFOUND") - { - return writeResponse(res, 502, "host cannot be found.") - } - else - { - console.log("Proxy Request Error: " + err.toString()); - return writeResponse(res, 500); - } + if (err.code === "ENOTFOUND") { + return writeResponse(res, 502, "host cannot be found.") + } + else { + console.log("Proxy Request Error: " + err.toString()); + return writeResponse(res, 500); + } - }); + }); - var requestSize = 0; - var proxyResponseSize = 0; + var requestSize = 0; + var proxyResponseSize = 0; - req.pipe(proxyRequest).on('data', function(data){ + req.pipe(proxyRequest).on('data', function (data) { - requestSize += data.length; + requestSize += data.length; - if(requestSize >= config.max_request_length) - { - proxyRequest.end(); - return sendTooBigResponse(res); - } - }); + if (requestSize >= config.max_request_length) { + proxyRequest.end(); + return sendTooBigResponse(res); + } + }); - proxyRequest.pipe(res).on('data', function (data) { + proxyRequest.pipe(res).on('data', function (data) { - proxyResponseSize += data.length; + proxyResponseSize += data.length; - if(proxyResponseSize >= config.max_request_length) - { - proxyRequest.end(); - return sendTooBigResponse(res); - } - }); - } - else { - return sendInvalidURLResponse(res); - } + if (proxyResponseSize >= config.max_request_length) { + proxyRequest.end(); + return sendTooBigResponse(res); + } + }); + } + else { + return sendInvalidURLResponse(res); + } } -http.createServer(function (req, res) { +if (cluster.isMaster) { + for (var i = 0; i < config.cluster_process_count; i++) { + cluster.fork(); + } +} +else +{ + http.createServer(function (req, res) { - // Process AWS health checks - if(req.url === "/health") - { - return writeResponse(res, 200); - } + // Process AWS health checks + if (req.url === "/health") { + return writeResponse(res, 200); + } - var clientIP = getClientAddress(req); + var clientIP = getClientAddress(req); - req.clientIP = clientIP; + req.clientIP = clientIP; - // Log our request - if(config.enable_logging) - { - console.log("%s %s %s", (new Date()).toJSON(), clientIP, req.method, req.url); - } + // Log our request + if (config.enable_logging) { + console.log("%s %s %s", (new Date()).toJSON(), clientIP, req.method, req.url); + } - if(config.enable_rate_limiting) - { - throttle.rateLimit(clientIP, function(err, limited) { - if (limited) - { - return writeResponse(res, 429, "enhance your calm"); - } + if (config.enable_rate_limiting) { + throttle.rateLimit(clientIP, function (err, limited) { + if (limited) { + return writeResponse(res, 429, "enhance your calm"); + } - processRequest(req, res); - }) - } - else - { - processRequest(req, res); - } + processRequest(req, res); + }) + } + else { + processRequest(req, res); + } -}).listen(config.port); + }).listen(config.port); -console.log("thingproxy.freeboard.io started"); + console.log("thingproxy.freeboard.io process started (PID " + process.pid + ")"); +}