├── .gitignore ├── LICENSE ├── README.md ├── config.js ├── package.json └── server.js /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | 16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 17 | .grunt 18 | 19 | # Compiled binary addons (http://nodejs.org/api/addons.html) 20 | build/Release 21 | 22 | # Dependency directory 23 | # Commenting this out is preferred by some people, see 24 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git- 25 | node_modules 26 | 27 | # Users Environment Variables 28 | .lock-wscript 29 | 30 | .idea 31 | .elasticbeanstalk/ 32 | 33 | # Elastic Beanstalk Files 34 | .elasticbeanstalk/* 35 | !.elasticbeanstalk/*.cfg.yml 36 | !.elasticbeanstalk/*.global.yml 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 freeboard 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | thingproxy 2 | ========== 3 | 4 | A simple forward proxy server for processing API calls to servers that don't send CORS headers or support HTTPS. 5 | 6 | ### what? 7 | 8 | thingproxy allows javascript code on your site to access resources on other domains that would normally be blocked due to the [same-origin policy](http://en.wikipedia.org/wiki/Same_origin_policy). It acts as a proxy between your browser and a remote server and adds the proper CORS headers to the response. 9 | 10 | In addition, some browsers don't allow requests for non-encrypted HTTP data if the page itself is loaded from HTTPS. thingproxy also allows you to access non-secure HTTP API's from a secure HTTPS url. 11 | 12 | We encourage you to run your own thingproxy server with this source code, but freeboard.io offers a free proxy available at: 13 | 14 | http://thingproxy.freeboard.io and https://thingproxy.freeboard.io 15 | 16 | ### why? 17 | 18 | Dashboards created with freeboard normally access APIs directly from ajax calls from javascript. Many API providers do not provide the proper CORS headers, or don't support HTTPS— thingproxy is provided to overcome these limitations. 19 | 20 | ### how? 21 | 22 | Just prefix any url with http(s)://thingproxy.freeboard.io/fetch/ 23 | 24 | For example: 25 | 26 | ``` 27 | https://thingproxy.freeboard.io/fetch/http://my.api.com/get/stuff 28 | ``` 29 | 30 | Any HTTP method, headers and body you send, will be sent to the URL you specify and the response will be sent back to you with the proper CORS headers attached. 31 | 32 | ### caveats 33 | 34 | Don't abuse the thingproxy.freeboard.io server— it is meant for relatively small API calls and not as a proxy server to hide your identity. Right now we limit requests and responses to 100,000 characters each (sorry no file downloads), and throttle each IP to 10 requests/second. 35 | 36 | ### privacy 37 | 38 | thingproxy.freeboard.io does log the date, requester's IP address, and URL for each request sent to it. We do not log headers or request bodies. We will not share or sell this data to anyone, period. 39 | -------------------------------------------------------------------------------- /config.js: -------------------------------------------------------------------------------- 1 | exports.port = process.env.PORT || 3000; 2 | exports.enable_logging = false; 3 | exports.fetch_regex = /^\/fetch\/(.*)$/; // The URL to look for when parsing the request. 4 | exports.proxy_request_timeout_ms = 10000; // The lenght of time we'll wait for a proxy server to respond before timing out. 5 | exports.max_request_length = 100000; // The maximum length of characters allowed for a request or a response. 6 | exports.enable_rate_limiting = true; 7 | exports.max_requests_per_second = 10; // The maximum number of requests per second to allow from a given IP. 8 | exports.blacklist_hostname_regex = /^(10\.|192\.|127\.|localhost$)/i; // Good for limiting access to internal IP addresses and hosts. 9 | exports.cluster_process_count = Number(process.env.CLUSTER_PROCESS_COUNT) || require("os").cpus().length; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "thingproxy.freeboard.io", 3 | "version": "0.0.1", 4 | "description": "A simple forward proxy server for processing API calls to servers that don't send CORS headers or support HTTPS.", 5 | "main": "server.js", 6 | "author": "Jim Heising ", 7 | "license": "MIT", 8 | "dependencies": { 9 | "request": "2.40.0", 10 | "tokenthrottle": "1.1.0", 11 | "public-address": "^0.1.1" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | var http = require('http'); 2 | var https = require('https'); 3 | var config = require("./config"); 4 | var url = require("url"); 5 | var request = require("request"); 6 | var cluster = require('cluster'); 7 | var throttle = require("tokenthrottle")({rate: config.max_requests_per_second}); 8 | 9 | http.globalAgent.maxSockets = Infinity; 10 | https.globalAgent.maxSockets = Infinity; 11 | 12 | var publicAddressFinder = require("public-address"); 13 | var publicIP; 14 | 15 | // Get our public IP address 16 | publicAddressFinder(function (err, data) { 17 | if (!err && data) { 18 | publicIP = data.address; 19 | } 20 | }); 21 | 22 | function addCORSHeaders(req, res) { 23 | if (req.method.toUpperCase() === "OPTIONS") { 24 | if (req.headers["access-control-request-headers"]) { 25 | res.setHeader("Access-Control-Allow-Headers", req.headers["access-control-request-headers"]); 26 | } 27 | 28 | if (req.headers["access-control-request-method"]) { 29 | res.setHeader("Access-Control-Allow-Methods", req.headers["access-control-request-method"]); 30 | } 31 | } 32 | 33 | if (req.headers["origin"]) { 34 | res.setHeader("Access-Control-Allow-Origin", req.headers["origin"]); 35 | } 36 | else { 37 | res.setHeader("Access-Control-Allow-Origin", "*"); 38 | } 39 | } 40 | 41 | function writeResponse(res, httpCode, body) { 42 | res.statusCode = httpCode; 43 | res.end(body); 44 | } 45 | 46 | function sendInvalidURLResponse(res) { 47 | return writeResponse(res, 404, "url must be in the form of /fetch/{some_url_here}"); 48 | } 49 | 50 | function sendTooBigResponse(res) { 51 | return writeResponse(res, 413, "the content in the request or response cannot exceed " + config.max_request_length + " characters."); 52 | } 53 | 54 | function getClientAddress(req) { 55 | return (req.headers['x-forwarded-for'] || '').split(',')[0] 56 | || req.connection.remoteAddress; 57 | } 58 | 59 | function processRequest(req, res) { 60 | addCORSHeaders(req, res); 61 | 62 | // Return options pre-flight requests right away 63 | if (req.method.toUpperCase() === "OPTIONS") { 64 | return writeResponse(res, 204); 65 | } 66 | 67 | var result = config.fetch_regex.exec(req.url); 68 | 69 | if (result && result.length == 2 && result[1]) { 70 | var remoteURL; 71 | 72 | try { 73 | remoteURL = url.parse(decodeURI(result[1])); 74 | } 75 | catch (e) { 76 | return sendInvalidURLResponse(res); 77 | } 78 | 79 | // We don't support relative links 80 | if (!remoteURL.host) { 81 | return writeResponse(res, 404, "relative URLS are not supported"); 82 | } 83 | 84 | // Naughty, naughty— deny requests to blacklisted hosts 85 | if (config.blacklist_hostname_regex.test(remoteURL.hostname)) { 86 | return writeResponse(res, 400, "naughty, naughty..."); 87 | } 88 | 89 | // We only support http and https 90 | if (remoteURL.protocol != "http:" && remoteURL.protocol !== "https:") { 91 | return writeResponse(res, 400, "only http and https are supported"); 92 | } 93 | 94 | if (publicIP) { 95 | // Add an X-Forwarded-For header 96 | if (req.headers["x-forwarded-for"]) { 97 | req.headers["x-forwarded-for"] += ", " + publicIP; 98 | } 99 | else { 100 | req.headers["x-forwarded-for"] = req.clientIP + ", " + publicIP; 101 | } 102 | } 103 | 104 | // Make sure the host header is to the URL we're requesting, not thingproxy 105 | if (req.headers["host"]) { 106 | req.headers["host"] = remoteURL.host; 107 | } 108 | 109 | // Remove origin and referer headers. TODO: This is a bit naughty, we should remove at some point. 110 | delete req.headers["origin"]; 111 | delete req.headers["referer"]; 112 | 113 | var proxyRequest = request({ 114 | url: remoteURL, 115 | headers: req.headers, 116 | method: req.method, 117 | timeout: config.proxy_request_timeout_ms, 118 | strictSSL: false 119 | }); 120 | 121 | proxyRequest.on('error', function (err) { 122 | 123 | if (err.code === "ENOTFOUND") { 124 | return writeResponse(res, 502, "Host for " + url.format(remoteURL) + " cannot be found.") 125 | } 126 | else { 127 | console.log("Proxy Request Error (" + url.format(remoteURL) + "): " + err.toString()); 128 | return writeResponse(res, 500); 129 | } 130 | 131 | }); 132 | 133 | var requestSize = 0; 134 | var proxyResponseSize = 0; 135 | 136 | req.pipe(proxyRequest).on('data', function (data) { 137 | 138 | requestSize += data.length; 139 | 140 | if (requestSize >= config.max_request_length) { 141 | proxyRequest.end(); 142 | return sendTooBigResponse(res); 143 | } 144 | }).on('error', function(err){ 145 | writeResponse(res, 500, "Stream Error"); 146 | }); 147 | 148 | proxyRequest.pipe(res).on('data', function (data) { 149 | 150 | proxyResponseSize += data.length; 151 | 152 | if (proxyResponseSize >= config.max_request_length) { 153 | proxyRequest.end(); 154 | return sendTooBigResponse(res); 155 | } 156 | }).on('error', function(err){ 157 | writeResponse(res, 500, "Stream Error"); 158 | }); 159 | } 160 | else { 161 | return sendInvalidURLResponse(res); 162 | } 163 | } 164 | 165 | if (cluster.isMaster) { 166 | for (var i = 0; i < config.cluster_process_count; i++) { 167 | cluster.fork(); 168 | } 169 | } 170 | else 171 | { 172 | http.createServer(function (req, res) { 173 | 174 | // Process AWS health checks 175 | if (req.url === "/health") { 176 | return writeResponse(res, 200); 177 | } 178 | 179 | var clientIP = getClientAddress(req); 180 | 181 | req.clientIP = clientIP; 182 | 183 | // Log our request 184 | if (config.enable_logging) { 185 | console.log("%s %s %s", (new Date()).toJSON(), clientIP, req.method, req.url); 186 | } 187 | 188 | if (config.enable_rate_limiting) { 189 | throttle.rateLimit(clientIP, function (err, limited) { 190 | if (limited) { 191 | return writeResponse(res, 429, "enhance your calm"); 192 | } 193 | 194 | processRequest(req, res); 195 | }) 196 | } 197 | else { 198 | processRequest(req, res); 199 | } 200 | 201 | }).listen(config.port); 202 | 203 | console.log("thingproxy.freeboard.io process started (PID " + process.pid + ")"); 204 | } 205 | --------------------------------------------------------------------------------