diff --git a/README.md b/README.md index 569de0b..00b3d31 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,6 @@ Features * Follow redirects to a configurable depth * Proxy remote images with a content-type of `image/*` * 404s for anything other than a 200, 301, 302, 303, 304 or 307 HTTP response -* Disallows proxying to private IP ranges At GitHub we render markdown and replace all of the `src` attributes on the `img` tags with the appropriate URL to hit the proxies. There's example code for creating URLs in [the tests](https://github.com/atmos/camo/blob/master/test/proxy_test.rb). diff --git a/package.json b/package.json index 6c29d15..2977f37 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "camo", - "version": "1.3.0", + "version": "1.2.1", "dependencies": { }, "engines": { diff --git a/server.coffee b/server.coffee index 7b491b2..e3cb198 100644 --- a/server.coffee +++ b/server.coffee @@ -1,12 +1,11 @@ Fs = require 'fs' -Dns = require 'dns' Url = require 'url' Http = require 'http' Crypto = require 'crypto' QueryString = require 'querystring' port = parseInt process.env.PORT || 8081 -version = "1.3.0" +version = "1.2.1" shared_key = process.env.CAMO_KEY || '0x24FEEDFACEDEADBEEFCAFE' max_redirects = process.env.CAMO_MAX_REDIRECTS || 4 camo_hostname = process.env.CAMO_HOSTNAME || "unknown" @@ -14,12 +13,6 @@ socket_timeout = process.env.CAMO_SOCKET_TIMEOUT || 10 logging_enabled = process.env.CAMO_LOGGING_ENABLED || "disabled" content_length_limit = parseInt(process.env.CAMO_LENGTH_LIMIT || 5242880, 10) -# Enable test mode if no custom keys is configured. -# This maybe a weak assumption, but no one should be using camo with the -# default key configuration. Test mode only has the affect of allowing -# proxying to 127.0.0.1 test servers. -testMode = !process.env.CAMO_KEY? - debug_log = (msg) -> if logging_enabled == "debug" console.log("--------------------------------------------") @@ -30,8 +23,6 @@ error_log = (msg) -> unless logging_enabled == "disabled" console.error("[#{new Date().toISOString()}] #{msg}") -RESTRICTED_IPS = /^((10\.)|(127\.)|(169\.254)|(192\.168)|(172\.((1[6-9])|(2[0-9])|(3[0-1]))))/ - total_connections = 0 current_connections = 0 started_at = new Date @@ -46,56 +37,17 @@ finish = (resp, str) -> current_connections = 0 if current_connections < 1 resp.connection && resp.end str -# A Transform Stream that limits the piped data to the specified length -Stream = require('stream') -class LimitStream extends Stream.Transform - constructor: (length) -> - super() - @remaining = length - - _transform: (chunk, encoding, cb) -> - if @remaining > 0 - if @remaining < chunk.length - chunk = chunk.slice(0, @remaining) - @push(chunk) - @remaining -= chunk.length - if @remaining <= 0 - @emit('length_limited') - @end() - cb() - - write: (chunk, encoding, cb) -> - if @remaining > 0 - super - else - false - process_url = (url, transferredHeaders, resp, remaining_redirects) -> - if !url.host? - return four_oh_four(resp, "Invalid host", url) - - if url.protocol == 'https:' - error_log("Redirecting https URL to origin: #{url.format()}") - resp.writeHead 301, {'Location': url.format()} - finish resp - return - else if url.protocol != 'http:' - four_oh_four(resp, "Unknown protocol", url) - return - - Dns.lookup url.hostname, (err, address, family) -> - if err - return four_oh_four(resp, "No host found: #{err}", url) - - if address.match(RESTRICTED_IPS) - if testMode and address is '127.0.0.1' - # don't block localhost server for testing - else - return four_oh_four(resp, "Hitting excluded IP", url) - - fetch_url address, url, transferredHeaders, resp, remaining_redirects + if url.host? + if url.protocol == 'https:' + error_log("Redirecting https URL to origin: #{url.format()}") + resp.writeHead 301, {'Location': url.format()} + finish resp + return + else if url.protocol != 'http:' + four_oh_four(resp, "Unknown protocol", url) + return - fetch_url = (ip_address, url, transferredHeaders, resp, remaining_redirects) -> queryPath = url.pathname if url.query? queryPath += "?#{url.query}" @@ -151,15 +103,7 @@ process_url = (url, transferredHeaders, resp, remaining_redirects) -> debug_log newHeaders resp.writeHead srcResp.statusCode, newHeaders - - limit = new LimitStream(content_length_limit) - srcResp.pipe(limit) - limit.pipe(resp) - - limit.on 'length_limited', -> - srcResp.destroy() - error_log("Killed connection at content_length_limit: #{url.format()}") - + srcResp.pipe resp when 301, 302, 303, 307 srcResp.destroy() if remaining_redirects <= 0 @@ -196,6 +140,8 @@ process_url = (url, transferredHeaders, resp, remaining_redirects) -> resp.on 'error', (e) -> error_log("Request error: #{e}") srcReq.abort() + else + four_oh_four(resp, "No host found " + url.host, url) # decode a string of two char hex digits hexdec = (str) -> diff --git a/server.js b/server.js index 53a3168..0059cd7 100644 --- a/server.js +++ b/server.js @@ -1,13 +1,9 @@ // Generated by CoffeeScript 1.6.3 (function() { - var Crypto, Dns, Fs, Http, LimitStream, QueryString, RESTRICTED_IPS, Stream, Url, camo_hostname, content_length_limit, current_connections, debug_log, error_log, finish, four_oh_four, hexdec, logging_enabled, max_redirects, port, process_url, server, shared_key, socket_timeout, started_at, testMode, total_connections, version, - __hasProp = {}.hasOwnProperty, - __extends = function(child, parent) { for (var key in parent) { if (__hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; }; + var Crypto, Fs, Http, QueryString, Url, camo_hostname, content_length_limit, current_connections, debug_log, error_log, finish, four_oh_four, hexdec, logging_enabled, max_redirects, port, process_url, server, shared_key, socket_timeout, started_at, total_connections, version; Fs = require('fs'); - Dns = require('dns'); - Url = require('url'); Http = require('http'); @@ -18,7 +14,7 @@ port = parseInt(process.env.PORT || 8081); - version = "1.3.0"; + version = "1.2.1"; shared_key = process.env.CAMO_KEY || '0x24FEEDFACEDEADBEEFCAFE'; @@ -32,8 +28,6 @@ content_length_limit = parseInt(process.env.CAMO_LENGTH_LIMIT || 5242880, 10); - testMode = process.env.CAMO_KEY == null; - debug_log = function(msg) { if (logging_enabled === "debug") { console.log("--------------------------------------------"); @@ -48,8 +42,6 @@ } }; - RESTRICTED_IPS = /^((10\.)|(127\.)|(169\.254)|(192\.168)|(172\.((1[6-9])|(2[0-9])|(3[0-1]))))/; - total_connections = 0; current_connections = 0; @@ -70,74 +62,20 @@ return resp.connection && resp.end(str); }; - Stream = require('stream'); - - LimitStream = (function(_super) { - __extends(LimitStream, _super); - - function LimitStream(length) { - LimitStream.__super__.constructor.call(this); - this.remaining = length; - } - - LimitStream.prototype._transform = function(chunk, encoding, cb) { - if (this.remaining > 0) { - if (this.remaining < chunk.length) { - chunk = chunk.slice(0, this.remaining); - } - this.push(chunk); - this.remaining -= chunk.length; - if (this.remaining <= 0) { - this.emit('length_limited'); - this.end(); - } - } - return cb(); - }; - - LimitStream.prototype.write = function(chunk, encoding, cb) { - if (this.remaining > 0) { - return LimitStream.__super__.write.apply(this, arguments); - } else { - return false; - } - }; - - return LimitStream; - - })(Stream.Transform); - process_url = function(url, transferredHeaders, resp, remaining_redirects) { - var fetch_url; - if (url.host == null) { - return four_oh_four(resp, "Invalid host", url); - } - if (url.protocol === 'https:') { - error_log("Redirecting https URL to origin: " + (url.format())); - resp.writeHead(301, { - 'Location': url.format() - }); - finish(resp); - return; - } else if (url.protocol !== 'http:') { - four_oh_four(resp, "Unknown protocol", url); - return; - } - Dns.lookup(url.hostname, function(err, address, family) { - if (err) { - return four_oh_four(resp, "No host found: " + err, url); + var queryPath, requestOptions, srcReq, _ref; + if (url.host != null) { + if (url.protocol === 'https:') { + error_log("Redirecting https URL to origin: " + (url.format())); + resp.writeHead(301, { + 'Location': url.format() + }); + finish(resp); + return; + } else if (url.protocol !== 'http:') { + four_oh_four(resp, "Unknown protocol", url); + return; } - if (address.match(RESTRICTED_IPS)) { - if (testMode && address === '127.0.0.1') { - - } else { - return four_oh_four(resp, "Hitting excluded IP", url); - } - } - return fetch_url(address, url, transferredHeaders, resp, remaining_redirects); - }); - return fetch_url = function(ip_address, url, transferredHeaders, resp, remaining_redirects) { - var queryPath, requestOptions, srcReq, _ref; queryPath = url.pathname; if (url.query != null) { queryPath += "?" + url.query; @@ -151,7 +89,7 @@ headers: transferredHeaders }; srcReq = Http.get(requestOptions, function(srcResp) { - var content_length, is_finished, limit, newHeaders, newUrl; + var content_length, is_finished, newHeaders, newUrl; is_finished = true; debug_log(srcResp.headers); content_length = srcResp.headers['content-length']; @@ -193,13 +131,7 @@ } debug_log(newHeaders); resp.writeHead(srcResp.statusCode, newHeaders); - limit = new LimitStream(content_length_limit); - srcResp.pipe(limit); - limit.pipe(resp); - return limit.on('length_limited', function() { - srcResp.destroy(); - return error_log("Killed connection at content_length_limit: " + (url.format())); - }); + return srcResp.pipe(resp); case 301: case 302: case 303: @@ -244,7 +176,9 @@ error_log("Request error: " + e); return srcReq.abort(); }); - }; + } else { + return four_oh_four(resp, "No host found " + url.host, url); + } }; hexdec = function(str) { diff --git a/test/proxy_test.rb b/test/proxy_test.rb index 6e5ee98..8796a79 100644 --- a/test/proxy_test.rb +++ b/test/proxy_test.rb @@ -142,32 +142,12 @@ def test_404s_on_non_image_content_type end end - def test_404s_on_10_0_ip_range + def test_404s_on_connect_timeout assert_raise RestClient::ResourceNotFound do request('http://10.0.0.1/foo.cgi') end end - 16.upto(31) do |i| - define_method :"test_404s_on_172_#{i}_ip_range" do - assert_raise RestClient::ResourceNotFound do - request("http://172.#{i}.0.1/foo.cgi") - end - end - end - - def test_404s_on_169_254_ip_range - assert_raise RestClient::ResourceNotFound do - request('http://169.254.0.1/foo.cgi') - end - end - - def test_404s_on_192_168_ip_range - assert_raise RestClient::ResourceNotFound do - request('http://192.168.0.1/foo.cgi') - end - end - def test_404s_on_environmental_excludes assert_raise RestClient::ResourceNotFound do request('http://iphone.internal.example.org/foo.cgi')