From 3b81bd9f7b9481f3be0d7f00b1fd570ac824c8b8 Mon Sep 17 00:00:00 2001 From: Jim Heising Date: Thu, 21 Aug 2014 15:03:09 -0700 Subject: [PATCH] Initial commit --- .gitignore | 30 +++++++++++ config.js | 6 +++ package.json | 12 +++++ thingproxy.js | 137 ++++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 185 insertions(+) create mode 100644 .gitignore create mode 100644 config.js create mode 100644 package.json create mode 100644 thingproxy.js diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5d46e4e --- /dev/null +++ b/.gitignore @@ -0,0 +1,30 @@ +# Logs +logs +*.log + +# Runtime data +pids +*.pid +*.seed + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage + +# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Compiled binary addons (http://nodejs.org/api/addons.html) +build/Release + +# Dependency directory +# Commenting this out is preferred by some people, see +# https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git- +node_modules + +# Users Environment Variables +.lock-wscript + +.idea \ No newline at end of file diff --git a/config.js b/config.js new file mode 100644 index 0000000..e9eaa5d --- /dev/null +++ b/config.js @@ -0,0 +1,6 @@ +exports.port = process.env.PORT || 3000; +exports.fetch_regex = /^\/fetch\/(.*)$/; // The URL to look for when parsing the request. +exports.proxy_request_timeout_ms = 10000; // The lenght of time we'll wait for a proxy server to respond before timing out. +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 = 1; // The maximum number of requests per second to allow from a given IP. \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..9e85ce1 --- /dev/null +++ b/package.json @@ -0,0 +1,12 @@ +{ + "name": "thingproxy.freeboard.io", + "version": "0.0.1", + "description": "A simple forward proxy server for processing API calls to servers that don't send CORS headers or support HTTPS.", + "main": "thingproxy.js", + "author": "Jim Heising ", + "license": "MIT", + "dependencies": { + "request": "^2.40.0", + "tokenthrottle": "^1.1.0" + } +} diff --git a/thingproxy.js b/thingproxy.js new file mode 100644 index 0000000..3d74fd1 --- /dev/null +++ b/thingproxy.js @@ -0,0 +1,137 @@ +var http = require('http'); +var config = require("./config"); +var url = require("url"); +var request = require("request"); +var throttle = require("tokenthrottle")({rate: config.max_requests_per_second}); + +function addCORSHeaders(res) { + res.setHeader("Access-Control-Allow-Origin", "*"); + res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS, PUT, DELETE"); + res.setHeader("Access-Control-Allow-Credentials", "true"); + res.setHeader("Access-Control-Allow-Headers", "Origin,Content-Type,Accept"); + res.setHeader("Content-Type", "text/plain"); +} + +function writeResponse(res, httpCode, body) { + res.statusCode = httpCode; + res.end(body); +} + +function sendInvalidURLResponse(res) { + return writeResponse(res, 400, "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."); +} + +function getClientAddress(req) { + return (req.headers['x-forwarded-for'] || '').split(',')[0] + || req.connection.remoteAddress; +} + +function processRequest(req, res) +{ + addCORSHeaders(res); + + // Return options pre-flight requests right away + if (req.method.toUpperCase() === "OPTIONS") { + return writeResponse(res, 204); + } + + var result = config.fetch_regex.exec(req.url); + + if (result && result.length == 2 && result[1]) { + var remoteURL; + + try { + remoteURL = url.parse(decodeURI(result[1])); + } + catch (e) { + return sendInvalidURLResponse(res); + } + + // We only support http and https + if (remoteURL.protocol != "http:" && remoteURL.protocol !== "https:") { + return writeResponse(res, 400, "only http and https are supported"); + } + + var proxyRequest = request({ + url: remoteURL, + headers: req.headers, + method: req.method, + timeout: config.proxy_request_timeout_ms, + strictSSL : false + }); + + proxyRequest.on('error', function(err){ + + console.log(err); + + if(err.code === "ENOTFOUND") + { + return writeResponse(res, 502, "host cannot be found.") + } + else + { + return writeResponse(res, 500); + } + + }); + + var requestSize = 0; + var proxyResponseSize = 0; + + req.pipe(proxyRequest).on('data', function(data){ + + requestSize += data.length; + + if(requestSize >= config.max_request_length) + { + proxyRequest.end(); + return sendTooBigResponse(res); + } + }); + + proxyRequest.pipe(res).on('data', function (data) { + + proxyResponseSize += data.length; + + if(proxyResponseSize >= config.max_request_length) + { + proxyRequest.end(); + return sendTooBigResponse(res); + } + }); + } + else { + return sendInvalidURLResponse(res); + } +} + +http.createServer(function (req, res) { + + var remoteIP = getClientAddress(req); + + // Log our request + console.log("%s %s %s", (new Date()).toJSON(), remoteIP, req.method, req.url); + + if(config.enable_rate_limiting) + { + throttle.rateLimit(remoteIP, function(err, limited) { + if (limited) + { + return writeResponse(res, 429, "enhance your calm"); + } + + processRequest(req, res); + }) + } + else + { + processRequest(req, res); + } + +}).listen(config.port); + +console.log("thingproxy.freeboard.io started");