camo-2.3.0/000775 000000 000000 00000000000 12522044173 012413 5ustar00rootroot000000 000000 camo-2.3.0/.gitignore000664 000000 000000 00000000073 12522044173 014403 0ustar00rootroot000000 000000 node_modules tmp/camouflage.pid tmp/camo.pid .node-version camo-2.3.0/.travis.yml000664 000000 000000 00000000312 12522044173 014520 0ustar00rootroot000000 000000 language: ruby rvm: - 2.1.0 gemfile: test.gemfile before_install: - npm install -g coffee-script - gem install rake before_script: - node --version - npm --version - coffee server.coffee & script: rake camo-2.3.0/AUTHORS000664 000000 000000 00000000127 12522044173 013463 0ustar00rootroot000000 000000 Rick Olson: https://github.com/technoweenie Corey Donohoe: https://github.com/atmos camo-2.3.0/CHANGELOG.md000664 000000 000000 00000001536 12522044173 014231 0ustar00rootroot000000 000000 1.1.3 ===== * [Address ddos](https://groups.google.com/forum/#!msg/nodejs/NEbweYB0ei0/gWvyzCunYjsJ?mkt_tok=3RkMMJWWfF9wsRonuavPZKXonjHpfsX54%2B8tXaO3lMI%2F0ER3fOvrPUfGjI4ASMFrI%2BSLDwEYGJlv6SgFQrjAMapmyLgLUhE%3D) in earlier versions of node. 1.1.1 ===== * Use pipe() to pause buffers when streaming to slow clients * Fixup tests and Gemfile related stuff * Workaround recent heroku changes that now detect camo as a ruby app due to Gemfile presence * Ensure a location header is present before following redirects, fixes a crash 1.0.5 ===== * Fixup redirect loops where following redirects goes back to camo * Add Fallback Accept headers for type `image/*` * Fixup issues with chunked encoding responses * Explicitly set User-Agent headers when proxying 1.0.2 ===== * Follow 303s and 307s now too 0.5.0 ===== * Follow redirects to a configurable depth camo-2.3.0/LICENSE.md000664 000000 000000 00000002062 12522044173 014017 0ustar00rootroot000000 000000 Copyright (c) 2010-2014 Corey Donohoe, Rick Olson Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. camo-2.3.0/Procfile000664 000000 000000 00000000024 12522044173 014075 0ustar00rootroot000000 000000 web: node server.js camo-2.3.0/README.md000664 000000 000000 00000007370 12522044173 013701 0ustar00rootroot000000 000000 # camo [![Build Status](https://travis-ci.org/atmos/camo.svg?branch=master)](https://travis-ci.org/atmos/camo) Camo is all about making insecure assets look secure. This is an SSL image proxy to prevent mixed content warnings on secure pages served from [GitHub](https://github.com). ![camo](https://f.cloud.github.com/assets/38/2496172/f558bbb4-b312-11e3-88e9-646b77e47e6e.gif) We want to allow people to keep embedding images in comments/issues/READMEs. [There's more info on the GitHub blog](https://github.com/blog/743-sidejack-prevention-phase-3-ssl-proxied-assets). Using a shared key, proxy URLs are encrypted with [hmac](http://en.wikipedia.org/wiki/HMAC) so we can bust caches/ban/rate limit if needed. Camo currently runs on node version 0.10.29 at GitHub on [heroku](http://heroku.com). [![Launch on Heroku](https://www.herokucdn.com/deploy/button.png)](https://heroku.com/deploy?template=https://github.com/atmos/camo) Features -------- * Max size for proxied images * Follow redirects to a certain depth * Restricts proxied images content-types to a whitelist * Forward images regardless of HTTP status code 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). ## URL Formats Camo supports two distinct URL formats: http://example.org/?url= http://example.org// The `` is a 40 character hex encoded HMAC digest generated with a shared secret key and the unescaped `` value. The `` is the absolute URL locating an image. In the first format, the `` should be URL escaped aggressively to ensure the original value isn't mangled in transit. In the second format, each byte of the `` should be hex encoded such that the resulting value includes only characters `[0-9a-f]`. ## Configuration Camo is configured through environment variables. * `PORT`: The port number Camo should listen on. (default: 8081) * `CAMO_HEADER_VIA`: The string for Camo to include in the `Via` and `User-Agent` headers it sends in requests to origin servers. (default: `Camo Asset Proxy `) * `CAMO_KEY`: The shared key used to generate the HMAC digest. * `CAMO_LENGTH_LIMIT`: The maximum `Content-Length` Camo will proxy. (default: 5242880) * `CAMO_LOGGING_ENABLED`: The logging level used for reporting debug or error information. Options are `debug` and `disabled`. (default: `disabled`) * `CAMO_MAX_REDIRECTS`: The maximum number of redirects Camo will follow while fetching an image. (default: 4) * `CAMO_SOCKET_TIMEOUT`: The maximum number of seconds Camo will wait before giving up on fetching an image. (default: 10) * `CAMO_TIMING_ALLOW_ORIGIN`: The string for Camo to include in the [`Timing-Allow-Origin` header](http://www.w3.org/TR/resource-timing/#cross-origin-resources) it sends in responses to clients. The header is omitted if this environment variable is not set. (default: not set) * `CAMO_HOSTNAME`: The `Camo-Host` header value that Camo will send. (default: `unknown`) * `CAMO_KEEP_ALIVE`: Whether or not to enable keep-alive session. (default: `false`) ## Testing Functionality ### Bundle Everything % rake bundle ### Start the server % coffee server.coffee ### In another shell % rake ### Debugging To see the full URL restclient is hitting etc, try this. % RESTCLIENT_LOG=stdout rake ### Deployment You should run this on heroku. To enable useful line numbers in stacktraces you probably want to compile the server.coffee file to native javascript when deploying. % coffee -c server.coffee % /usr/bin/env PORT=9090 CAMO_KEY="" node server.js camo-2.3.0/Rakefile000664 000000 000000 00000000620 12522044173 014056 0ustar00rootroot000000 000000 file 'server.js' => 'server.coffee' do sh "coffee -c -o . server.coffee" end task :build => 'server.js' task :bundle do system("bundle install --gemfile test.gemfile") end desc "Run the tests against localhost" task :test do system("BUNDLE_GEMFILE=test.gemfile bundle exec ruby test/proxy_test.rb") end task :default => [:build, :bundle, :test] Dir["tasks/*.rake"].each do |f| load f end camo-2.3.0/app.json000664 000000 000000 00000002546 12522044173 014075 0ustar00rootroot000000 000000 { "name": "camo", "logo": "https://camo.githubusercontent.com/4d04abe0044d94fefcf9af21332239dbebf01ded/68747470733a2f2f662e636c6f75642e6769746875622e636f6d2f6173736574732f33382f323439363137322f66353538626262342d623331322d313165332d383865392d3634366237376534376536652e676966", "description": "Camo is all about making insecure image assets look secure.", "keywords": [ "ssl", "image", "proxy", "github", "anonymous" ], "website": "http://github.com/atmos/camo", "repository": "https://github.com/atmos/camo", "success_url": "/status", "env": { "CAMO_HOSTNAME": { "description": "The hostname for the camo server.", "required": false }, "CAMO_KEY": { "description": "The fully qualified domain name for camo to run on.", "generator": "secret" }, "CAMO_LENGTH_LIMIT": { "description": "The maximum Content-Length that camo will proxy in bytes", "value": "5242880" }, "CAMO_LOGGING_ENABLED": { "description": "Toggle whether or not to log verbosely('debug' or disabled').", "required": false }, "CAMO_MAX_REDIRECTS": { "description": "The number of redirects that camo should follow", "value": "4" }, "CAMO_SOCKET_TIMEOUT": { "description": "The number of seconds to wait for socket connection errors", "value": "10" } } } camo-2.3.0/log/000775 000000 000000 00000000000 12522044173 013174 5ustar00rootroot000000 000000 camo-2.3.0/log/.gitignore000664 000000 000000 00000000000 12522044173 015152 0ustar00rootroot000000 000000 camo-2.3.0/mime-types.json000664 000000 000000 00000001623 12522044173 015401 0ustar00rootroot000000 000000 [ "image/bmp", "image/cgm", "image/g3fax", "image/gif", "image/ief", "image/jp2", "image/jpeg", "image/jpg", "image/pict", "image/png", "image/prs.btif", "image/svg+xml", "image/tiff", "image/vnd.adobe.photoshop", "image/vnd.djvu", "image/vnd.dwg", "image/vnd.dxf", "image/vnd.fastbidsheet", "image/vnd.fpx", "image/vnd.fst", "image/vnd.fujixerox.edmics-mmr", "image/vnd.fujixerox.edmics-rlc", "image/vnd.microsoft.icon", "image/vnd.ms-modi", "image/vnd.net-fpx", "image/vnd.wap.wbmp", "image/vnd.xiff", "image/webp", "image/x-cmu-raster", "image/x-cmx", "image/x-icon", "image/x-macpaint", "image/x-pcx", "image/x-pict", "image/x-portable-anymap", "image/x-portable-bitmap", "image/x-portable-graymap", "image/x-portable-pixmap", "image/x-quicktime", "image/x-rgb", "image/x-xbitmap", "image/x-xpixmap", "image/x-xwindowdump" ] camo-2.3.0/package.json000664 000000 000000 00000000157 12522044173 014704 0ustar00rootroot000000 000000 { "name": "camo", "version": "2.3.0", "dependencies": { }, "engines": { "node": "^0.10.29" } } camo-2.3.0/server.coffee000664 000000 000000 00000022612 12522044173 015075 0ustar00rootroot000000 000000 Fs = require 'fs' Path = require 'path' Url = require 'url' Http = require 'http' Https = require 'https' Crypto = require 'crypto' QueryString = require 'querystring' port = parseInt process.env.PORT || 8081, 10 version = require(Path.resolve(__dirname, "package.json")).version shared_key = process.env.CAMO_KEY || '0x24FEEDFACEDEADBEEFCAFE' max_redirects = process.env.CAMO_MAX_REDIRECTS || 4 camo_hostname = process.env.CAMO_HOSTNAME || "unknown" socket_timeout = process.env.CAMO_SOCKET_TIMEOUT || 10 logging_enabled = process.env.CAMO_LOGGING_ENABLED || "disabled" keep_alive = process.env.CAMO_KEEP_ALIVE || "false" content_length_limit = parseInt(process.env.CAMO_LENGTH_LIMIT || 5242880, 10) accepted_image_mime_types = JSON.parse(Fs.readFileSync( Path.resolve(__dirname, "mime-types.json"), encoding: 'utf8' )) debug_log = (msg) -> if logging_enabled == "debug" console.log("--------------------------------------------") console.log(msg) console.log("--------------------------------------------") error_log = (msg) -> unless logging_enabled == "disabled" console.error("[#{new Date().toISOString()}] #{msg}") total_connections = 0 current_connections = 0 started_at = new Date default_security_headers = "X-Frame-Options": "deny" "X-XSS-Protection": "1; mode=block" "X-Content-Type-Options": "nosniff" "Content-Security-Policy": "default-src 'none'; img-src data:; style-src 'unsafe-inline'" "Strict-Transport-Security" : "max-age=31536000; includeSubDomains" four_oh_four = (resp, msg, url) -> error_log "#{msg}: #{url?.format() or 'unknown'}" resp.writeHead 404, expires: "0" "Cache-Control": "no-cache, no-store, private, must-revalidate" "X-Frame-Options" : default_security_headers["X-Frame-Options"] "X-XSS-Protection" : default_security_headers["X-XSS-Protection"] "X-Content-Type-Options" : default_security_headers["X-Content-Type-Options"] "Content-Security-Policy" : default_security_headers["Content-Security-Policy"] "Strict-Transport-Security" : default_security_headers["Strict-Transport-Security"] finish resp, "Not Found" finish = (resp, str) -> current_connections -= 1 current_connections = 0 if current_connections < 1 resp.connection && resp.end str process_url = (url, transferredHeaders, resp, remaining_redirects) -> if url.host? if url.protocol is 'https:' Protocol = Https else if url.protocol is 'http:' Protocol = Http else four_oh_four(resp, "Unknown protocol", url) return queryPath = url.pathname if url.query? queryPath += "?#{url.query}" transferredHeaders.host = url.host debug_log transferredHeaders requestOptions = hostname: url.hostname port: url.port path: queryPath headers: transferredHeaders if keep_alive == "false" requestOptions['agent'] = false srcReq = Protocol.get requestOptions, (srcResp) -> is_finished = true debug_log srcResp.headers content_length = srcResp.headers['content-length'] if content_length > content_length_limit srcResp.destroy() four_oh_four(resp, "Content-Length exceeded", url) else newHeaders = 'content-type' : srcResp.headers['content-type'] 'cache-control' : srcResp.headers['cache-control'] || 'public, max-age=31536000' 'Camo-Host' : camo_hostname 'X-Frame-Options' : default_security_headers['X-Frame-Options'] 'X-XSS-Protection' : default_security_headers['X-XSS-Protection'] 'X-Content-Type-Options' : default_security_headers['X-Content-Type-Options'] 'Content-Security-Policy' : default_security_headers['Content-Security-Policy'] 'Strict-Transport-Security' : default_security_headers['Strict-Transport-Security'] if eTag = srcResp.headers['etag'] newHeaders['etag'] = eTag if expiresHeader = srcResp.headers['expires'] newHeaders['expires'] = expiresHeader if lastModified = srcResp.headers['last-modified'] newHeaders['last-modified'] = lastModified if origin = process.env.CAMO_TIMING_ALLOW_ORIGIN newHeaders['Timing-Allow-Origin'] = origin # Handle chunked responses properly if content_length? newHeaders['content-length'] = content_length if srcResp.headers['transfer-encoding'] newHeaders['transfer-encoding'] = srcResp.headers['transfer-encoding'] if srcResp.headers['content-encoding'] newHeaders['content-encoding'] = srcResp.headers['content-encoding'] srcResp.on 'end', -> if is_finished finish resp srcResp.on 'error', -> if is_finished finish resp switch srcResp.statusCode when 301, 302, 303, 307 srcResp.destroy() if remaining_redirects <= 0 four_oh_four(resp, "Exceeded max depth", url) else if !srcResp.headers['location'] four_oh_four(resp, "Redirect with no location", url) else is_finished = false newUrl = Url.parse srcResp.headers['location'] unless newUrl.host? and newUrl.hostname? newUrl.host = newUrl.hostname = url.hostname newUrl.protocol = url.protocol debug_log "Redirected to #{newUrl.format()}" process_url newUrl, transferredHeaders, resp, remaining_redirects - 1 when 304 srcResp.destroy() resp.writeHead srcResp.statusCode, newHeaders else contentType = newHeaders['content-type'] unless contentType? srcResp.destroy() four_oh_four(resp, "No content-type returned", url) return contentTypePrefix = contentType.split(";")[0].toLowerCase() unless contentTypePrefix in accepted_image_mime_types srcResp.destroy() four_oh_four(resp, "Non-Image content-type returned '#{contentTypePrefix}'", url) return debug_log newHeaders resp.writeHead srcResp.statusCode, newHeaders srcResp.pipe resp srcReq.setTimeout (socket_timeout * 1000), -> srcReq.abort() four_oh_four resp, "Socket timeout", url srcReq.on 'error', (error) -> four_oh_four(resp, "Client Request error #{error.stack}", url) resp.on 'close', -> error_log("Request aborted") srcReq.abort() 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) -> if str and str.length > 0 and str.length % 2 == 0 and not str.match(/[^0-9a-f]/) buf = new Buffer(str.length / 2) for i in [0...str.length] by 2 buf[i/2] = parseInt(str[i..i+1], 16) buf.toString() server = Http.createServer (req, resp) -> if req.method != 'GET' || req.url == '/' resp.writeHead 200, default_security_headers resp.end 'hwhat' else if req.url == '/favicon.ico' resp.writeHead 200, default_security_headers resp.end 'ok' else if req.url == '/status' resp.writeHead 200, default_security_headers resp.end "ok #{current_connections}/#{total_connections} since #{started_at.toString()}" else total_connections += 1 current_connections += 1 url = Url.parse req.url user_agent = process.env.CAMO_HEADER_VIA or= "Camo Asset Proxy #{version}" transferredHeaders = 'Via' : user_agent 'User-Agent' : user_agent 'Accept' : req.headers.accept ? 'image/*' 'Accept-Encoding' : req.headers['accept-encoding'] "X-Frame-Options" : default_security_headers["X-Frame-Options"] "X-XSS-Protection" : default_security_headers["X-XSS-Protection"] "X-Content-Type-Options" : default_security_headers["X-Content-Type-Options"] "Content-Security-Policy" : default_security_headers["Content-Security-Policy"] delete(req.headers.cookie) [query_digest, encoded_url] = url.pathname.replace(/^\//, '').split("/", 2) if encoded_url = hexdec(encoded_url) url_type = 'path' dest_url = encoded_url else url_type = 'query' dest_url = QueryString.parse(url.query).url debug_log({ type: url_type url: req.url headers: req.headers dest: dest_url digest: query_digest }) if req.headers['via'] && req.headers['via'].indexOf(user_agent) != -1 return four_oh_four(resp, "Requesting from self") if url.pathname? && dest_url hmac = Crypto.createHmac("sha1", shared_key) try hmac.update(dest_url, 'utf8') catch error return four_oh_four(resp, "could not create checksum") hmac_digest = hmac.digest('hex') if hmac_digest == query_digest url = Url.parse dest_url process_url url, transferredHeaders, resp, max_redirects else four_oh_four(resp, "checksum mismatch #{hmac_digest}:#{query_digest}") else four_oh_four(resp, "No pathname provided on the server") console.log "SSL-Proxy running on #{port} with pid:#{process.pid} version:#{version}." server.listen port camo-2.3.0/tasks/000775 000000 000000 00000000000 12522044173 013540 5ustar00rootroot000000 000000 camo-2.3.0/tasks/.gitignore000664 000000 000000 00000000000 12522044173 015516 0ustar00rootroot000000 000000 camo-2.3.0/test.gemfile000664 000000 000000 00000000156 12522044173 014726 0ustar00rootroot000000 000000 source 'https://rubygems.org' gem 'rack' gem 'rest-client', '~>1.3' gem 'addressable', '~>2.3' gem 'test-unit'camo-2.3.0/test.gemfile.lock000664 000000 000000 00000000502 12522044173 015650 0ustar00rootroot000000 000000 GEM remote: https://rubygems.org/ specs: addressable (2.3.4) mime-types (1.23) power_assert (0.2.2) rack (1.5.2) rest-client (1.6.7) mime-types (>= 1.16) test-unit (3.0.8) power_assert PLATFORMS ruby DEPENDENCIES addressable (~> 2.3) rack rest-client (~> 1.3) test-unit camo-2.3.0/test/000775 000000 000000 00000000000 12522044173 013372 5ustar00rootroot000000 000000 camo-2.3.0/test/app_test.rb000664 000000 000000 00000000414 12522044173 015535 0ustar00rootroot000000 000000 require 'rubygems' require 'json' require 'test/unit' class CamoAppTest < Test::Unit::TestCase def test_heroku_app_json app_file = File.expand_path("../../app.json", __FILE__) assert_nothing_raised do JSON.parse(File.read(app_file)) end end end camo-2.3.0/test/proxy_test.rb000664 000000 000000 00000017775 12522044173 016160 0ustar00rootroot000000 000000 require 'rubygems' require 'json' require 'base64' require 'openssl' require 'rest_client' require 'addressable/uri' require 'test/unit' module CamoProxyTests def config { 'key' => ENV['CAMO_KEY'] || "0x24FEEDFACEDEADBEEFCAFE", 'host' => ENV['CAMO_HOST'] || "http://localhost:8081" } end def spawn_server(path) port = 9292 config = "test/servers/#{path}.ru" host = "localhost:#{port}" pid = fork do STDOUT.reopen "/dev/null" STDERR.reopen "/dev/null" exec "rackup", "--port", port.to_s, config end sleep 2 begin yield host ensure Process.kill(:TERM, pid) Process.wait(pid) end end def test_proxy_localhost_test_server spawn_server(:ok) do |host| response = RestClient.get("http://#{host}/octocat.jpg") assert_equal(200, response.code) response = request("http://#{host}/octocat.jpg") assert_equal(200, response.code) end end def test_proxy_survives_redirect_without_location spawn_server(:redirect_without_location) do |host| assert_raise RestClient::ResourceNotFound do request("http://#{host}") end end response = request('http://media.ebaumsworld.com/picture/Mincemeat/Pimp.jpg') assert_equal(200, response.code) end def test_follows_https_redirect_for_image_links response = request('http://dl.dropbox.com/u/602885/github/soldier-squirrel.jpg') assert_equal(200, response.code) end def test_doesnt_crash_with_non_url_encoded_url assert_raise RestClient::ResourceNotFound do RestClient.get("#{config['host']}/crashme?url=crash&url=me") end end def test_always_sets_security_headers ['/', '/status'].each do |path| response = RestClient.get("#{config['host']}#{path}") assert_equal "deny", response.headers[:x_frame_options] assert_equal "default-src 'none'; img-src data:; style-src 'unsafe-inline'", response.headers[:content_security_policy] assert_equal "nosniff", response.headers[:x_content_type_options] assert_equal "max-age=31536000; includeSubDomains", response.headers[:strict_transport_security] end response = request('http://dl.dropbox.com/u/602885/github/soldier-squirrel.jpg') assert_equal "deny", response.headers[:x_frame_options] assert_equal "default-src 'none'; img-src data:; style-src 'unsafe-inline'", response.headers[:content_security_policy] assert_equal "nosniff", response.headers[:x_content_type_options] assert_equal "max-age=31536000; includeSubDomains", response.headers[:strict_transport_security] end def test_proxy_valid_image_url response = request('http://media.ebaumsworld.com/picture/Mincemeat/Pimp.jpg') assert_equal(200, response.code) end def test_svg_image_with_delimited_content_type_url response = request('https://saucelabs.com/browser-matrix/bootstrap.svg') assert_equal(200, response.code) end def test_png_image_with_delimited_content_type_url response = request('http://uploadir.com/u/cm5el1v7') assert_equal(200, response.code) end def test_proxy_valid_image_url_with_crazy_subdomain response = request('http://27.media.tumblr.com/tumblr_lkp6rdDfRi1qce6mto1_500.jpg') assert_equal(200, response.code) end def test_strict_image_content_type_checking assert_raise RestClient::ResourceNotFound do request("http://calm-shore-1799.herokuapp.com/foo.png") end end def test_proxy_valid_google_chart_url response = request('http://chart.apis.google.com/chart?chs=920x200&chxl=0:%7C2010-08-13%7C2010-09-12%7C2010-10-12%7C2010-11-11%7C1:%7C0%7C0%7C0%7C0%7C0%7C0&chm=B,EBF5FB,0,0,0&chco=008Cd6&chls=3,1,0&chg=8.3,20,1,4&chd=s:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA&chxt=x,y&cht=lc') assert_equal(200, response.code) end def test_proxy_valid_chunked_image_file response = request('http://www.igvita.com/posts/12/spdyproxy-diagram.png') assert_equal(200, response.code) assert_nil(response.headers[:content_length]) end def test_proxy_https_octocat response = request('https://octodex.github.com/images/original.png') assert_equal(200, response.code) end def test_proxy_https_gravatar response = request('https://1.gravatar.com/avatar/a86224d72ce21cd9f5bee6784d4b06c7') assert_equal(200, response.code) end def test_follows_redirects response = request('http://cl.ly/1K0X2Y2F1P0o3z140p0d/boom-headshot.gif') assert_equal(200, response.code) end def test_follows_redirects_formatted_strangely response = request('http://cl.ly/DPcp/Screen%20Shot%202012-01-17%20at%203.42.32%20PM.png') assert_equal(200, response.code) end def test_follows_redirects_with_path_only_location_headers assert_nothing_raised do request('http://blogs.msdn.com/photos/noahric/images/9948044/425x286.aspx') end end def test_forwards_404_with_image spawn_server(:not_found) do |host| uri = request_uri("http://#{host}/octocat.jpg") response = RestClient.get(uri){ |response, request, result| response } assert_equal(404, response.code) assert_equal("image/jpeg", response.headers[:content_type]) end end def test_404s_on_request_error spawn_server(:crash_request) do |host| assert_raise RestClient::ResourceNotFound do request("http://#{host}/cats.png") end end end def test_404s_on_infinidirect assert_raise RestClient::ResourceNotFound do request('http://modeselektor.herokuapp.com/') end end def test_404s_on_urls_without_an_http_host assert_raise RestClient::ResourceNotFound do request('/picture/Mincemeat/Pimp.jpg') end end def test_404s_on_images_greater_than_5_megabytes assert_raise RestClient::ResourceNotFound do request('http://apod.nasa.gov/apod/image/0505/larryslookout_spirit_big.jpg') end end def test_404s_on_host_not_found assert_raise RestClient::ResourceNotFound do request('http://flabergasted.cx') end end def test_404s_on_non_image_content_type assert_raise RestClient::ResourceNotFound do request('https://github.com/atmos/cinderella/raw/master/bootstrap.sh') end end def test_404s_on_connect_timeout assert_raise RestClient::ResourceNotFound do request('http://10.0.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') end end def test_follows_temporary_redirects response = request('http://bit.ly/1l9Fztb') assert_equal(200, response.code) end def test_request_from_self assert_raise RestClient::ResourceNotFound do uri = request_uri("http://camo-localhost-test.herokuapp.com") response = request( uri ) end end def test_404s_send_cache_headers uri = request_uri("http://example.org/") response = RestClient.get(uri){ |response, request, result| response } assert_equal(404, response.code) assert_equal("0", response.headers[:expires]) assert_equal("no-cache, no-store, private, must-revalidate", response.headers[:cache_control]) end end class CamoProxyQueryStringTest < Test::Unit::TestCase include CamoProxyTests def request_uri(image_url) hexdigest = OpenSSL::HMAC.hexdigest( OpenSSL::Digest.new('sha1'), config['key'], image_url) uri = Addressable::URI.parse("#{config['host']}/#{hexdigest}") uri.query_values = { 'url' => image_url, 'repo' => '', 'path' => '' } uri.to_s end def request(image_url) RestClient.get(request_uri(image_url)) end end class CamoProxyPathTest < Test::Unit::TestCase include CamoProxyTests def hexenc(image_url) image_url.to_enum(:each_byte).map { |byte| "%02x" % byte }.join end def request_uri(image_url) hexdigest = OpenSSL::HMAC.hexdigest( OpenSSL::Digest.new('sha1'), config['key'], image_url) encoded_image_url = hexenc(image_url) "#{config['host']}/#{hexdigest}/#{encoded_image_url}" end def request(image_url) RestClient.get(request_uri(image_url)) end end camo-2.3.0/test/servers/000775 000000 000000 00000000000 12522044173 015063 5ustar00rootroot000000 000000 camo-2.3.0/test/servers/crash_request.ru000664 000000 000000 00000000044 12522044173 020301 0ustar00rootroot000000 000000 run lambda { |env| raise "b00m" } camo-2.3.0/test/servers/not_found.ru000664 000000 000000 00000000226 12522044173 017426 0ustar00rootroot000000 000000 run lambda { |env| path = File.expand_path('../octocat.jpg', __FILE__) data = File.read(path) [404, {'Content-Type' => 'image/jpeg'}, [data]] } camo-2.3.0/test/servers/octocat.jpg000664 000000 000000 00000004013 12522044173 017217 0ustar00rootroot000000 000000 JFIF     "" $(4,$&1'-=-157:::#+?D?8C49:7 7%%77777777777777777777777777777777777777777777777777ZZ"C !1AQaq"#2R3Bbr$CS#1Q!2AB ?`>YRf*7 r&Rw 8sHfUissۇ* mNz; s|m*RTA*A7*q iNG2ݪQ0G]RR*fܿ.|71&9xe}^oЀ\s)X%q+imjY0W -kqj[*Zԥya#T%^* *II$!N#?M:/4I@AAߤ.-y g ɯ+kB_#k#ݘA1ZZͿ3@wtH8m)j=}.-ePFk)(<e٦ XѥԐr6"B~&%vu4WꥹbObAVWfLZDxom# jXS/(6$ S肬9;aD>ddu--{ָ&Kk :|VA ~U8ڷ(xDI=[u<Ĭ=]ʭpA*HݻH,G-VCK˺yJ ^[vȋّ~RbiT%71!R;ei,eKyww_0<*?7&%0UԬI$hAܡ BPEN# fU L}l  ($(a6n+,KQʔxFU:[@7X@q q6!@@|_UjK,}QAWk!&(CYJHӜ5ܼ˘kpnᩉJ~AD6q{#}"NM*FRM,Z ^mkŚfqBuD 7Kȹ0Ok.\d(xZnrMIu&|/t$3?&4Vg QEtᓔfM]6s t%?ja9XW>b7CvU87| rg*/MqAG@qMxQ\ӤvA[*[ 8>neIRG0% HOmz>^0"}>J mR%n=={cEkBPFpAAcamo-2.3.0/test/servers/ok.ru000664 000000 000000 00000000226 12522044173 016044 0ustar00rootroot000000 000000 run lambda { |env| path = File.expand_path('../octocat.jpg', __FILE__) data = File.read(path) [200, {'Content-Type' => 'image/jpeg'}, [data]] } camo-2.3.0/test/servers/redirect_without_location.ru000664 000000 000000 00000000174 12522044173 022711 0ustar00rootroot000000 000000 class ProxyTestServer def call(env) [302, {"Content-Type" => "image/foo"}, "test"] end end run ProxyTestServer.new camo-2.3.0/tmp/000775 000000 000000 00000000000 12522044173 013213 5ustar00rootroot000000 000000 camo-2.3.0/tmp/.gitignore000664 000000 000000 00000000000 12522044173 015171 0ustar00rootroot000000 000000