Merge lp:~ianchou821/ubuntu/utopic/camo/bug-1355443 into lp:ubuntu/utopic/camo

Proposed by Yu-Cheng Chou
Status: Work in progress
Proposed branch: lp:~ianchou821/ubuntu/utopic/camo/bug-1355443
Merge into: lp:ubuntu/utopic/camo
Diff against target: 1357 lines (+726/-243)
24 files modified
.gitignore (+1/-0)
.pc/0001-Don-t-use-bundle-in-the-test-suite.patch/Rakefile (+0/-31)
.pc/applied-patches (+0/-1)
.travis.yml (+12/-0)
LICENSE (+0/-20)
LICENSE.md (+20/-0)
README.md (+24/-9)
Rakefile (+4/-16)
app.json (+51/-0)
debian/changelog (+6/-0)
debian/patches/0001-Don-t-use-bundle-in-the-test-suite.patch (+13/-14)
debian/rules (+1/-1)
mime-types.json (+44/-0)
package.json (+2/-2)
server.coffee (+108/-105)
server.js (+312/-0)
test.gemfile (+1/-1)
test.gemfile.lock (+1/-7)
test/app_test.rb (+12/-0)
test/proxy_test.rb (+99/-25)
test/proxy_test_server.rb (+0/-11)
test/servers/crash_request.ru (+3/-0)
test/servers/ok.ru (+5/-0)
test/servers/redirect_without_location.ru (+7/-0)
To merge this branch: bzr merge lp:~ianchou821/ubuntu/utopic/camo/bug-1355443
Reviewer Review Type Date Requested Status
Ubuntu branches Pending
Review via email: mp+233033@code.launchpad.net

Description of the change

new upstream(2.1.0) release

To post a comment you must log in.
4. By Yu-Cheng Chou

update rules

Revision history for this message
Robie Basak (racb) wrote :

Thank you for your work.

I haven't reviewed this in detail, but have a few comments, mainly about process.

First, Utopic entered feature freeze on 21 August (see https://wiki.ubuntu.com/UtopicUnicorn/ReleaseSchedule) so updating camo in Utopic now needs to either wait until the next cycle or needs a feature freeze exception. See https://wiki.ubuntu.com/FeatureFreeze and https://wiki.ubuntu.com/FreezeExceptionProcess for details).

Second, please try and submit this update to Debian first - I've linked the Debian bug from the Launchpad bug - so that Ubuntu doesn't diverge from Debian needlessly. So if this is going to go in next cycle, there might well be enough time for Debian to upload the new version so that Ubuntu can just sync.

Final, minor point: if we did upload this to Ubuntu ahead of Debian, the version 2.1.0-1ubuntu1 would be incorrect. It must be 2.1.0-0ubuntu1 in that case so that we can later update to a future version published by Debian.

Unmerged revisions

4. By Yu-Cheng Chou

update rules

3. By Yu-Cheng Chou

New upstream release. (LP: #1355443)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file '.gitignore'
2--- .gitignore 2013-11-26 13:45:34 +0000
3+++ .gitignore 2014-09-02 12:26:21 +0000
4@@ -1,3 +1,4 @@
5 node_modules
6 tmp/camouflage.pid
7 tmp/camo.pid
8+.node-version
9
10=== removed directory '.pc/0001-Don-t-use-bundle-in-the-test-suite.patch'
11=== removed file '.pc/0001-Don-t-use-bundle-in-the-test-suite.patch/Rakefile'
12--- .pc/0001-Don-t-use-bundle-in-the-test-suite.patch/Rakefile 2013-11-26 13:45:34 +0000
13+++ .pc/0001-Don-t-use-bundle-in-the-test-suite.patch/Rakefile 1970-01-01 00:00:00 +0000
14@@ -1,31 +0,0 @@
15-file 'server.js' => 'server.coffee' do
16- sh "coffee -c -o . server.coffee"
17-end
18-task :build => 'server.js'
19-
20-task :bundle do
21- system("bundle install --gemfile test.gemfile")
22-end
23-
24-namespace :test do
25- desc "Start test server"
26- task :server do |t|
27- $SERVER_PID = Process.spawn("ruby test/proxy_test_server.rb")
28- end
29-
30- desc "Run the tests against localhost"
31- task :check do |t|
32- system("BUNDLE_GEMFILE=test.gemfile bundle exec ruby test/proxy_test.rb")
33- end
34-
35- desc "Kill test server"
36- task :kill_server do |t|
37- Process.kill(:QUIT, $SERVER_PID) && Process.wait
38- end
39-end
40-
41-task :default => [:build, :bundle, "test:server", "test:check", "test:kill_server"]
42-
43-Dir["tasks/*.rake"].each do |f|
44- load f
45-end
46
47=== removed file '.pc/applied-patches'
48--- .pc/applied-patches 2013-11-26 13:45:34 +0000
49+++ .pc/applied-patches 1970-01-01 00:00:00 +0000
50@@ -1,1 +0,0 @@
51-0001-Don-t-use-bundle-in-the-test-suite.patch
52
53=== added file '.travis.yml'
54--- .travis.yml 1970-01-01 00:00:00 +0000
55+++ .travis.yml 2014-09-02 12:26:21 +0000
56@@ -0,0 +1,12 @@
57+language: ruby
58+rvm:
59+- 2.1.0
60+gemfile: test.gemfile
61+before_install:
62+- npm install -g coffee-script
63+- gem install rake
64+before_script:
65+- node --version
66+- npm --version
67+- coffee server.coffee &
68+script: rake
69
70=== removed file 'LICENSE'
71--- LICENSE 2013-11-26 13:45:34 +0000
72+++ LICENSE 1970-01-01 00:00:00 +0000
73@@ -1,20 +0,0 @@
74-Copyright (c) 2010 Corey Donohoe, Rick Olson
75-
76-Permission is hereby granted, free of charge, to any person obtaining
77-a copy of this software and associated documentation files (the
78-"Software"), to deal in the Software without restriction, including
79-without limitation the rights to use, copy, modify, merge, publish,
80-distribute, sublicense, and/or sell copies of the Software, and to
81-permit persons to whom the Software is furnished to do so, subject to
82-the following conditions:
83-
84-The above copyright notice and this permission notice shall be
85-included in all copies or substantial portions of the Software.
86-
87-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
88-EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
89-MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
90-NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
91-LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
92-OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
93-WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
94
95=== added file 'LICENSE.md'
96--- LICENSE.md 1970-01-01 00:00:00 +0000
97+++ LICENSE.md 2014-09-02 12:26:21 +0000
98@@ -0,0 +1,20 @@
99+Copyright (c) 2010-2014 Corey Donohoe, Rick Olson
100+
101+Permission is hereby granted, free of charge, to any person obtaining
102+a copy of this software and associated documentation files (the
103+"Software"), to deal in the Software without restriction, including
104+without limitation the rights to use, copy, modify, merge, publish,
105+distribute, sublicense, and/or sell copies of the Software, and to
106+permit persons to whom the Software is furnished to do so, subject to
107+the following conditions:
108+
109+The above copyright notice and this permission notice shall be
110+included in all copies or substantial portions of the Software.
111+
112+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
113+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
114+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
115+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
116+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
117+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
118+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
119
120=== modified file 'README.md'
121--- README.md 2013-11-26 13:45:34 +0000
122+++ README.md 2014-09-02 12:26:21 +0000
123@@ -1,24 +1,26 @@
124-![camo](http://farm5.static.flickr.com/4116/4857328881_fefb8e2134_z.jpg)
125+# camo [![Build Status](https://travis-ci.org/atmos/camo.svg?branch=master)](https://travis-ci.org/atmos/camo)
126
127 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).
128
129-We want to allow people to keep embedding images in comments/issues/READMEs/google charting.
130+![camo](https://f.cloud.github.com/assets/38/2496172/f558bbb4-b312-11e3-88e9-646b77e47e6e.gif)
131+
132+We want to allow people to keep embedding images in comments/issues/READMEs.
133
134 [There's more info on the GitHub blog](https://github.com/blog/743-sidejack-prevention-phase-3-ssl-proxied-assets).
135
136 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.
137
138-Camo currently runs on node version 0.10.13 at GitHub on [heroku](http://heroku.com).
139+Camo currently runs on node version 0.10.26 at GitHub on [heroku](http://heroku.com).
140+
141+[![Launch on Heroku](https://www.herokucdn.com/deploy/button.png)](https://www.heroku.com/deploy/?template=https://github.com/atmos/camo)
142
143 Features
144 --------
145
146-* Proxy google charts
147-* Proxy images under 5 MB
148-* Follow redirects to a configurable depth
149-* Proxy remote images with a content-type of `image/*`
150+* Max size for proxied images
151+* Follow redirects to a certain depth
152+* Restricts proxied images content-types to a whitelist
153 * 404s for anything other than a 200, 301, 302, 303, 304 or 307 HTTP response
154-* Disallows proxying to private IP ranges
155
156 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).
157
158@@ -36,6 +38,19 @@
159 In the second format, each byte of the `<image-url>` should be hex encoded such
160 that the resulting value includes only characters `[0-9a-f]`.
161
162+## Configuration
163+
164+Camo is configured through environment variables.
165+
166+* `PORT`: The port number Camo should listen on. (default: 8081)
167+* `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 <version>`)
168+* `CAMO_KEY`: The shared key used to generate the HMAC digest.
169+* `CAMO_LENGTH_LIMIT`: The maximum `Content-Length` Camo will proxy. (default: 5242880)
170+* `CAMO_LOGGING_ENABLED`: The logging level used for reporting debug or error information. Options are `debug` and `disabled`. (default: `disabled`)
171+* `CAMO_MAX_REDIRECTS`: The maximum number of redirects Camo will follow while fetching an image. (default: 4)
172+* `CAMO_SOCKET_TIMEOUT`: The maximum number of seconds Camo will wait before giving up on fetching an image. (default: 10)
173+* `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)
174+
175 ## Testing Functionality
176
177 ### Bundle Everything
178@@ -58,7 +73,7 @@
179
180 ### Deployment
181
182-You can see an example [god config](https://gist.github.com/675038) here.
183+You should run this on heroku.
184
185 To enable useful line numbers in stacktraces you probably want to compile the server.coffee file to native javascript when deploying.
186
187
188=== modified file 'Rakefile'
189--- Rakefile 2013-11-26 13:45:34 +0000
190+++ Rakefile 2014-09-02 12:26:21 +0000
191@@ -7,24 +7,12 @@
192 system("bundle install --gemfile test.gemfile")
193 end
194
195-namespace :test do
196- desc "Start test server"
197- task :server do |t|
198- $SERVER_PID = Process.spawn("ruby test/proxy_test_server.rb")
199- end
200-
201- desc "Run the tests against localhost"
202- task :check do |t|
203- system("ruby test/proxy_test.rb")
204- end
205-
206- desc "Kill test server"
207- task :kill_server do |t|
208- Process.kill(:QUIT, $SERVER_PID) && Process.wait
209- end
210+desc "Run the tests against localhost"
211+task :test do
212+ system("BUNDLE_GEMFILE=test.gemfile bundle exec ruby test/proxy_test.rb")
213 end
214
215-task :default => [:build, :bundle, "test:server", "test:check", "test:kill_server"]
216+task :default => [:build, :bundle, :test]
217
218 Dir["tasks/*.rake"].each do |f|
219 load f
220
221=== added file 'app.json'
222--- app.json 1970-01-01 00:00:00 +0000
223+++ app.json 2014-09-02 12:26:21 +0000
224@@ -0,0 +1,51 @@
225+{
226+ "name": "camo",
227+ "logo": "https://camo.githubusercontent.com/4d04abe0044d94fefcf9af21332239dbebf01ded/68747470733a2f2f662e636c6f75642e6769746875622e636f6d2f6173736574732f33382f323439363137322f66353538626262342d623331322d313165332d383865392d3634366237376534376536652e676966",
228+ "description": "Camo is all about making insecure image assets look secure.",
229+
230+ "keywords": [
231+ "ssl",
232+ "image",
233+ "proxy",
234+ "github",
235+ "anonymous"
236+ ],
237+
238+ "website": "http://github.com/atmos/camo",
239+ "repository": "https://github.com/atmos/camo",
240+ "success_url": "/stats",
241+
242+ "env": {
243+ "CAMO_HOSTNAME": {
244+ "description": "The hostname for the camo server."
245+ },
246+ "CAMO_KEY": {
247+ "description": "The fully qualified domain name for camo to run on.",
248+ "generator": "secret"
249+ },
250+ "CAMO_LENGTH_LIMIT": {
251+ "description": "The maximum Content-Length that camo will proxy in bytes",
252+ "value": "5242880",
253+ "required": true
254+ },
255+ "CAMO_LOGGING_ENABLED": {
256+ "description": "Toggle whether or not to log verbosely('debug' or disabled')."
257+ },
258+ "CAMO_MAX_REDIRECTS": {
259+ "description": "The number of redirects that camo should follow",
260+ "value": "4"
261+ },
262+ "CAMO_SOCKET_TIMEOUT": {
263+ "description": "The number of seconds to wait for socket connection errors",
264+ "value": "10"
265+ },
266+ "RAILS_ENV": {
267+ "description": "The RAILS environment for this installation",
268+ "value": "production"
269+ }
270+ },
271+
272+ "scripts": {
273+ "postdeploy": "bundle exec rake db:migrate"
274+ }
275+}
276
277=== modified file 'debian/changelog'
278--- debian/changelog 2013-11-26 13:45:34 +0000
279+++ debian/changelog 2014-09-02 12:26:21 +0000
280@@ -1,3 +1,9 @@
281+camo (2.1.0-1ubuntu1) utopic; urgency=low
282+
283+ * New upstream release. (LP: #1355443)
284+
285+ -- Yu-Cheng Chou <ianchou821@gmail.com> Tue, 02 Sep 2014 19:38:36 +0800
286+
287 camo (1.3.0+dfsg-1) unstable; urgency=low
288
289 * Initial release (Closes: #721731)
290
291=== modified file 'debian/patches/0001-Don-t-use-bundle-in-the-test-suite.patch'
292--- debian/patches/0001-Don-t-use-bundle-in-the-test-suite.patch 2013-11-26 13:45:34 +0000
293+++ debian/patches/0001-Don-t-use-bundle-in-the-test-suite.patch 2014-09-02 12:26:21 +0000
294@@ -10,17 +10,16 @@
295 Rakefile | 2 +-
296 1 file changed, 1 insertion(+), 1 deletion(-)
297
298-diff --git a/Rakefile b/Rakefile
299-index c82f435..fea80b1 100644
300---- a/Rakefile
301-+++ b/Rakefile
302-@@ -15,7 +15,7 @@ namespace :test do
303-
304- desc "Run the tests against localhost"
305- task :check do |t|
306-- system("BUNDLE_GEMFILE=test.gemfile bundle exec ruby test/proxy_test.rb")
307-+ system("ruby test/proxy_test.rb")
308- end
309-
310- desc "Kill test server"
311---
312+Index: camo/Rakefile
313+===================================================================
314+--- camo.orig/Rakefile
315++++ camo/Rakefile
316+@@ -9,7 +9,7 @@ end
317+
318+ desc "Run the tests against localhost"
319+ task :test do
320+- system("BUNDLE_GEMFILE=test.gemfile bundle exec ruby test/proxy_test.rb")
321++ system("ruby test/proxy_test.rb")
322+ end
323+
324+ task :default => [:build, :bundle, :test]
325
326=== modified file 'debian/rules'
327--- debian/rules 2013-11-26 13:45:34 +0000
328+++ debian/rules 2014-09-02 12:26:21 +0000
329@@ -30,7 +30,7 @@
330 rake --trace build
331 override_dh_auto_test:
332 nodejs server.js &
333- rake --trace test:check
334+ rake --trace test
335 pkill -KILL -xf "nodejs server.js"
336
337 override_dh_auto_clean:
338
339=== added file 'mime-types.json'
340--- mime-types.json 1970-01-01 00:00:00 +0000
341+++ mime-types.json 2014-09-02 12:26:21 +0000
342@@ -0,0 +1,44 @@
343+[
344+ "image/bmp",
345+ "image/cgm",
346+ "image/g3fax",
347+ "image/gif",
348+ "image/ief",
349+ "image/jp2",
350+ "image/jpeg",
351+ "image/pict",
352+ "image/png",
353+ "image/prs.btif",
354+ "image/svg+xml",
355+ "image/tiff",
356+ "image/vnd.adobe.photoshop",
357+ "image/vnd.djvu",
358+ "image/vnd.dwg",
359+ "image/vnd.dxf",
360+ "image/vnd.fastbidsheet",
361+ "image/vnd.fpx",
362+ "image/vnd.fst",
363+ "image/vnd.fujixerox.edmics-mmr",
364+ "image/vnd.fujixerox.edmics-rlc",
365+ "image/vnd.microsoft.icon",
366+ "image/vnd.ms-modi",
367+ "image/vnd.net-fpx",
368+ "image/vnd.wap.wbmp",
369+ "image/vnd.xiff",
370+ "image/webp",
371+ "image/x-cmu-raster",
372+ "image/x-cmx",
373+ "image/x-icon",
374+ "image/x-macpaint",
375+ "image/x-pcx",
376+ "image/x-pict",
377+ "image/x-portable-anymap",
378+ "image/x-portable-bitmap",
379+ "image/x-portable-graymap",
380+ "image/x-portable-pixmap",
381+ "image/x-quicktime",
382+ "image/x-rgb",
383+ "image/x-xbitmap",
384+ "image/x-xpixmap",
385+ "image/x-xwindowdump"
386+]
387
388=== modified file 'package.json'
389--- package.json 2013-11-26 13:45:34 +0000
390+++ package.json 2014-09-02 12:26:21 +0000
391@@ -1,9 +1,9 @@
392 {
393 "name": "camo",
394- "version": "1.3.0",
395+ "version": "2.1.0",
396 "dependencies": {
397 },
398 "engines": {
399- "node": ">=0.10.21"
400+ "node": "^0.10.29"
401 }
402 }
403
404=== modified file 'server.coffee'
405--- server.coffee 2013-11-26 13:45:34 +0000
406+++ server.coffee 2014-09-02 12:26:21 +0000
407@@ -1,12 +1,14 @@
408 Fs = require 'fs'
409-Dns = require 'dns'
410+Path = require 'path'
411 Url = require 'url'
412+Path = require 'path'
413 Http = require 'http'
414+Https = require 'https'
415 Crypto = require 'crypto'
416 QueryString = require 'querystring'
417
418-port = parseInt process.env.PORT || 8081
419-version = "1.3.0"
420+port = parseInt process.env.PORT || 8081, 10
421+version = require(Path.resolve(__dirname, "package.json")).version
422 shared_key = process.env.CAMO_KEY || '0x24FEEDFACEDEADBEEFCAFE'
423 max_redirects = process.env.CAMO_MAX_REDIRECTS || 4
424 camo_hostname = process.env.CAMO_HOSTNAME || "unknown"
425@@ -14,6 +16,11 @@
426 logging_enabled = process.env.CAMO_LOGGING_ENABLED || "disabled"
427 content_length_limit = parseInt(process.env.CAMO_LENGTH_LIMIT || 5242880, 10)
428
429+accepted_image_mime_types = JSON.parse(Fs.readFileSync(
430+ Path.resolve(__dirname, "mime-types.json"),
431+ encoding: 'utf8'
432+))
433+
434 debug_log = (msg) ->
435 if logging_enabled == "debug"
436 console.log("--------------------------------------------")
437@@ -24,15 +31,28 @@
438 unless logging_enabled == "disabled"
439 console.error("[#{new Date().toISOString()}] #{msg}")
440
441-RESTRICTED_IPS = /^((10\.)|(127\.)|(169\.254)|(192\.168)|(172\.((1[6-9])|(2[0-9])|(3[0-1]))))/
442-
443 total_connections = 0
444 current_connections = 0
445 started_at = new Date
446
447+default_security_headers =
448+ "X-Frame-Options": "deny"
449+ "X-XSS-Protection": "1; mode=block"
450+ "X-Content-Type-Options": "nosniff"
451+ "Content-Security-Policy": "default-src 'none'; style-src 'unsafe-inline'"
452+ "Strict-Transport-Security" : "max-age=31536000; includeSubDomains"
453+
454 four_oh_four = (resp, msg, url) ->
455 error_log "#{msg}: #{url?.format() or 'unknown'}"
456- resp.writeHead 404
457+ resp.writeHead 404,
458+ expires: "0"
459+ "Cache-Control": "no-cache, no-store, private, must-revalidate"
460+ "X-Frame-Options" : default_security_headers["X-Frame-Options"]
461+ "X-XSS-Protection" : default_security_headers["X-XSS-Protection"]
462+ "X-Content-Type-Options" : default_security_headers["X-Content-Type-Options"]
463+ "Content-Security-Policy" : default_security_headers["Content-Security-Policy"]
464+ "Strict-Transport-Security" : default_security_headers["Strict-Transport-Security"]
465+
466 finish resp, "Not Found"
467
468 finish = (resp, str) ->
469@@ -40,73 +60,30 @@
470 current_connections = 0 if current_connections < 1
471 resp.connection && resp.end str
472
473-# A Transform Stream that limits the piped data to the specified length
474-Stream = require('stream')
475-class LimitStream extends Stream.Transform
476- constructor: (length) ->
477- super()
478- @remaining = length
479-
480- _transform: (chunk, encoding, cb) ->
481- if @remaining > 0
482- if @remaining < chunk.length
483- chunk = chunk.slice(0, @remaining)
484- @push(chunk)
485- @remaining -= chunk.length
486- if @remaining <= 0
487- @emit('length_limited')
488- @end()
489- cb()
490-
491- write: (chunk, encoding, cb) ->
492- if @remaining > 0
493- super
494+process_url = (url, transferredHeaders, resp, remaining_redirects) ->
495+ if url.host?
496+ if url.protocol is 'https:'
497+ Protocol = Https
498+ else if url.protocol is 'http:'
499+ Protocol = Http
500 else
501- false
502-
503-process_url = (url, transferred_headers, resp, remaining_redirects) ->
504- if !url.host?
505- return four_oh_four(resp, "Invalid host", url)
506-
507- if url.protocol == 'https:'
508- error_log("Redirecting https URL to origin: #{url.format()}")
509- resp.writeHead 301, {'Location': url.format()}
510- finish resp
511- return
512- else if url.protocol != 'http:'
513- four_oh_four(resp, "Unknown protocol", url)
514- return
515-
516- Dns.lookup url.hostname, (err, address, family) ->
517- if err
518- return four_oh_four(resp, "No host found: #{err}", url)
519-
520- if address.match(RESTRICTED_IPS)
521- return four_oh_four(resp, "Hitting excluded IP", url)
522-
523- fetch_url address, url, transferred_headers, resp, remaining_redirects
524-
525- fetch_url = (ip_address, url, transferred_headers, resp, remaining_redirects) ->
526- src = Http.createClient url.port || 80, url.hostname
527-
528- src.on 'error', (error) ->
529- four_oh_four(resp, "Client Request error #{error.stack}", url)
530-
531- query_path = url.pathname
532+ four_oh_four(resp, "Unknown protocol", url)
533+ return
534+
535+ queryPath = url.pathname
536 if url.query?
537- query_path += "?#{url.query}"
538-
539- transferred_headers.host = url.host
540-
541- debug_log transferred_headers
542-
543- srcReq = src.request 'GET', query_path, transferred_headers
544-
545- srcReq.setTimeout (socket_timeout * 1000), ()->
546- srcReq.abort()
547- four_oh_four resp, "Socket timeout", url
548-
549- srcReq.on 'response', (srcResp) ->
550+ queryPath += "?#{url.query}"
551+
552+ transferredHeaders.host = url.host
553+ debug_log transferredHeaders
554+
555+ requestOptions =
556+ hostname: url.hostname
557+ port: url.port
558+ path: queryPath
559+ headers: transferredHeaders
560+
561+ srcReq = Protocol.get requestOptions, (srcResp) ->
562 is_finished = true
563
564 debug_log srcResp.headers
565@@ -118,10 +95,26 @@
566 four_oh_four(resp, "Content-Length exceeded", url)
567 else
568 newHeaders =
569- 'content-type' : srcResp.headers['content-type']
570- 'cache-control' : srcResp.headers['cache-control'] || 'public, max-age=31536000'
571- 'Camo-Host' : camo_hostname
572- 'X-Content-Type-Options' : 'nosniff'
573+ 'content-type' : srcResp.headers['content-type']
574+ 'cache-control' : srcResp.headers['cache-control'] || 'public, max-age=31536000'
575+ 'Camo-Host' : camo_hostname
576+ 'X-Frame-Options' : default_security_headers['X-Frame-Options']
577+ 'X-XSS-Protection' : default_security_headers['X-XSS-Protection']
578+ 'X-Content-Type-Options' : default_security_headers['X-Content-Type-Options']
579+ 'Content-Security-Policy' : default_security_headers['Content-Security-Policy']
580+ 'Strict-Transport-Security' : default_security_headers['Strict-Transport-Security']
581+
582+ if eTag = srcResp.headers['etag']
583+ newHeaders['etag'] = eTag
584+
585+ if expiresHeader = srcResp.headers['expires']
586+ newHeaders['expires'] = expiresHeader
587+
588+ if lastModified = srcResp.headers['last-modified']
589+ newHeaders['last-modified'] = lastModified
590+
591+ if origin = process.env.CAMO_TIMING_ALLOW_ORIGIN
592+ newHeaders['Timing-Allow-Origin'] = origin
593
594 # Handle chunked responses properly
595 if content_length?
596@@ -140,23 +133,24 @@
597
598 switch srcResp.statusCode
599 when 200
600- if newHeaders['content-type'] && newHeaders['content-type'].slice(0, 5) != 'image'
601- srcResp.destroy()
602- four_oh_four(resp, "Non-Image content-type returned", url)
603+ contentType = newHeaders['content-type']
604+
605+ unless contentType?
606+ srcResp.destroy()
607+ four_oh_four(resp, "No content-type returned", url)
608+ return
609+
610+ contentTypePrefix = contentType.split(";")[0]
611+
612+ unless contentTypePrefix in accepted_image_mime_types
613+ srcResp.destroy()
614+ four_oh_four(resp, "Non-Image content-type returned '#{contentTypePrefix}'", url)
615 return
616
617 debug_log newHeaders
618
619 resp.writeHead srcResp.statusCode, newHeaders
620-
621- limit = new LimitStream(content_length_limit)
622- srcResp.pipe(limit)
623- limit.pipe(resp)
624-
625- limit.on 'length_limited', ->
626- srcResp.destroy()
627- error_log("Killed connection at content_length_limit: #{url.format()}")
628-
629+ srcResp.pipe resp
630 when 301, 302, 303, 307
631 srcResp.destroy()
632 if remaining_redirects <= 0
633@@ -171,7 +165,7 @@
634 newUrl.protocol = url.protocol
635
636 debug_log "Redirected to #{newUrl.format()}"
637- process_url newUrl, transferred_headers, resp, remaining_redirects - 1
638+ process_url newUrl, transferredHeaders, resp, remaining_redirects - 1
639 when 304
640 srcResp.destroy()
641 resp.writeHead srcResp.statusCode, newHeaders
642@@ -179,10 +173,12 @@
643 srcResp.destroy()
644 four_oh_four(resp, "Origin responded with #{srcResp.statusCode}", url)
645
646- srcReq.on 'error', ->
647- finish resp
648+ srcReq.setTimeout (socket_timeout * 1000), ->
649+ srcReq.abort()
650+ four_oh_four resp, "Socket timeout", url
651
652- srcReq.end()
653+ srcReq.on 'error', (error) ->
654+ four_oh_four(resp, "Client Request error #{error.stack}", url)
655
656 resp.on 'close', ->
657 error_log("Request aborted")
658@@ -191,6 +187,8 @@
659 resp.on 'error', (e) ->
660 error_log("Request error: #{e}")
661 srcReq.abort()
662+ else
663+ four_oh_four(resp, "No host found " + url.host, url)
664
665 # decode a string of two char hex digits
666 hexdec = (str) ->
667@@ -202,13 +200,13 @@
668
669 server = Http.createServer (req, resp) ->
670 if req.method != 'GET' || req.url == '/'
671- resp.writeHead 200
672+ resp.writeHead 200, default_security_headers
673 resp.end 'hwhat'
674 else if req.url == '/favicon.ico'
675- resp.writeHead 200
676+ resp.writeHead 200, default_security_headers
677 resp.end 'ok'
678 else if req.url == '/status'
679- resp.writeHead 200
680+ resp.writeHead 200, default_security_headers
681 resp.end "ok #{current_connections}/#{total_connections} since #{started_at.toString()}"
682 else
683 total_connections += 1
684@@ -216,13 +214,15 @@
685 url = Url.parse req.url
686 user_agent = process.env.CAMO_HEADER_VIA or= "Camo Asset Proxy #{version}"
687
688- transferred_headers =
689- 'Via' : user_agent
690- 'User-Agent' : user_agent
691- 'Accept' : req.headers.accept ? 'image/*'
692- 'Accept-Encoding' : req.headers['accept-encoding']
693- 'x-forwarded-for' : req.headers['x-forwarded-for']
694- 'x-content-type-options' : 'nosniff'
695+ transferredHeaders =
696+ 'Via' : user_agent
697+ 'User-Agent' : user_agent
698+ 'Accept' : req.headers.accept ? 'image/*'
699+ 'Accept-Encoding' : req.headers['accept-encoding']
700+ "X-Frame-Options" : default_security_headers["X-Frame-Options"]
701+ "X-XSS-Protection" : default_security_headers["X-XSS-Protection"]
702+ "X-Content-Type-Options" : default_security_headers["X-Content-Type-Options"]
703+ "Content-Security-Policy" : default_security_headers["Content-Security-Policy"]
704
705 delete(req.headers.cookie)
706
707@@ -247,20 +247,23 @@
708
709 if url.pathname? && dest_url
710 hmac = Crypto.createHmac("sha1", shared_key)
711- hmac.update(dest_url, 'utf8')
712+
713+ try
714+ hmac.update(dest_url, 'utf8')
715+ catch error
716+ return four_oh_four(resp, "could not create checksum")
717
718 hmac_digest = hmac.digest('hex')
719
720 if hmac_digest == query_digest
721 url = Url.parse dest_url
722
723- process_url url, transferred_headers, resp, max_redirects
724+ process_url url, transferredHeaders, resp, max_redirects
725 else
726 four_oh_four(resp, "checksum mismatch #{hmac_digest}:#{query_digest}")
727 else
728 four_oh_four(resp, "No pathname provided on the server")
729
730-console.log "SSL-Proxy running on #{port} with pid:#{process.pid}."
731-console.log "Using the secret key #{shared_key}"
732+console.log "SSL-Proxy running on #{port} with pid:#{process.pid} version:#{version}."
733
734 server.listen port
735
736=== added file 'server.js'
737--- server.js 1970-01-01 00:00:00 +0000
738+++ server.js 2014-09-02 12:26:21 +0000
739@@ -0,0 +1,312 @@
740+// Generated by CoffeeScript 1.7.1
741+(function() {
742+ var Crypto, Fs, Http, Https, Path, QueryString, Url, accepted_image_mime_types, camo_hostname, content_length_limit, current_connections, debug_log, default_security_headers, error_log, finish, four_oh_four, hexdec, logging_enabled, max_redirects, port, process_url, server, shared_key, socket_timeout, started_at, total_connections, version,
743+ __indexOf = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i++) { if (i in this && this[i] === item) return i; } return -1; };
744+
745+ Fs = require('fs');
746+
747+ Path = require('path');
748+
749+ Url = require('url');
750+
751+ Path = require('path');
752+
753+ Http = require('http');
754+
755+ Https = require('https');
756+
757+ Crypto = require('crypto');
758+
759+ QueryString = require('querystring');
760+
761+ port = parseInt(process.env.PORT || 8081, 10);
762+
763+ version = require(Path.resolve(__dirname, "package.json")).version;
764+
765+ shared_key = process.env.CAMO_KEY || '0x24FEEDFACEDEADBEEFCAFE';
766+
767+ max_redirects = process.env.CAMO_MAX_REDIRECTS || 4;
768+
769+ camo_hostname = process.env.CAMO_HOSTNAME || "unknown";
770+
771+ socket_timeout = process.env.CAMO_SOCKET_TIMEOUT || 10;
772+
773+ logging_enabled = process.env.CAMO_LOGGING_ENABLED || "disabled";
774+
775+ content_length_limit = parseInt(process.env.CAMO_LENGTH_LIMIT || 5242880, 10);
776+
777+ accepted_image_mime_types = JSON.parse(Fs.readFileSync(Path.resolve(__dirname, "mime-types.json"), {
778+ encoding: 'utf8'
779+ }));
780+
781+ debug_log = function(msg) {
782+ if (logging_enabled === "debug") {
783+ console.log("--------------------------------------------");
784+ console.log(msg);
785+ return console.log("--------------------------------------------");
786+ }
787+ };
788+
789+ error_log = function(msg) {
790+ if (logging_enabled !== "disabled") {
791+ return console.error("[" + (new Date().toISOString()) + "] " + msg);
792+ }
793+ };
794+
795+ total_connections = 0;
796+
797+ current_connections = 0;
798+
799+ started_at = new Date;
800+
801+ default_security_headers = {
802+ "X-Frame-Options": "deny",
803+ "X-XSS-Protection": "1; mode=block",
804+ "X-Content-Type-Options": "nosniff",
805+ "Content-Security-Policy": "default-src 'none'; style-src 'unsafe-inline'",
806+ "Strict-Transport-Security": "max-age=31536000; includeSubDomains"
807+ };
808+
809+ four_oh_four = function(resp, msg, url) {
810+ error_log("" + msg + ": " + ((url != null ? url.format() : void 0) || 'unknown'));
811+ resp.writeHead(404, {
812+ expires: "0",
813+ "Cache-Control": "no-cache, no-store, private, must-revalidate",
814+ "X-Frame-Options": default_security_headers["X-Frame-Options"],
815+ "X-XSS-Protection": default_security_headers["X-XSS-Protection"],
816+ "X-Content-Type-Options": default_security_headers["X-Content-Type-Options"],
817+ "Content-Security-Policy": default_security_headers["Content-Security-Policy"],
818+ "Strict-Transport-Security": default_security_headers["Strict-Transport-Security"]
819+ });
820+ return finish(resp, "Not Found");
821+ };
822+
823+ finish = function(resp, str) {
824+ current_connections -= 1;
825+ if (current_connections < 1) {
826+ current_connections = 0;
827+ }
828+ return resp.connection && resp.end(str);
829+ };
830+
831+ process_url = function(url, transferredHeaders, resp, remaining_redirects) {
832+ var Protocol, queryPath, requestOptions, srcReq;
833+ if (url.host != null) {
834+ if (url.protocol === 'https:') {
835+ Protocol = Https;
836+ } else if (url.protocol === 'http:') {
837+ Protocol = Http;
838+ } else {
839+ four_oh_four(resp, "Unknown protocol", url);
840+ return;
841+ }
842+ queryPath = url.pathname;
843+ if (url.query != null) {
844+ queryPath += "?" + url.query;
845+ }
846+ transferredHeaders.host = url.host;
847+ debug_log(transferredHeaders);
848+ requestOptions = {
849+ hostname: url.hostname,
850+ port: url.port,
851+ path: queryPath,
852+ headers: transferredHeaders
853+ };
854+ srcReq = Protocol.get(requestOptions, function(srcResp) {
855+ var contentType, contentTypePrefix, content_length, eTag, expiresHeader, is_finished, lastModified, newHeaders, newUrl, origin;
856+ is_finished = true;
857+ debug_log(srcResp.headers);
858+ content_length = srcResp.headers['content-length'];
859+ if (content_length > content_length_limit) {
860+ srcResp.destroy();
861+ return four_oh_four(resp, "Content-Length exceeded", url);
862+ } else {
863+ newHeaders = {
864+ 'content-type': srcResp.headers['content-type'],
865+ 'cache-control': srcResp.headers['cache-control'] || 'public, max-age=31536000',
866+ 'Camo-Host': camo_hostname,
867+ 'X-Frame-Options': default_security_headers['X-Frame-Options'],
868+ 'X-XSS-Protection': default_security_headers['X-XSS-Protection'],
869+ 'X-Content-Type-Options': default_security_headers['X-Content-Type-Options'],
870+ 'Content-Security-Policy': default_security_headers['Content-Security-Policy'],
871+ 'Strict-Transport-Security': default_security_headers['Strict-Transport-Security']
872+ };
873+ if (eTag = srcResp.headers['etag']) {
874+ newHeaders['etag'] = eTag;
875+ }
876+ if (expiresHeader = srcResp.headers['expires']) {
877+ newHeaders['expires'] = expiresHeader;
878+ }
879+ if (lastModified = srcResp.headers['last-modified']) {
880+ newHeaders['last-modified'] = lastModified;
881+ }
882+ if (origin = process.env.CAMO_TIMING_ALLOW_ORIGIN) {
883+ newHeaders['Timing-Allow-Origin'] = origin;
884+ }
885+ if (content_length != null) {
886+ newHeaders['content-length'] = content_length;
887+ }
888+ if (srcResp.headers['transfer-encoding']) {
889+ newHeaders['transfer-encoding'] = srcResp.headers['transfer-encoding'];
890+ }
891+ if (srcResp.headers['content-encoding']) {
892+ newHeaders['content-encoding'] = srcResp.headers['content-encoding'];
893+ }
894+ srcResp.on('end', function() {
895+ if (is_finished) {
896+ return finish(resp);
897+ }
898+ });
899+ srcResp.on('error', function() {
900+ if (is_finished) {
901+ return finish(resp);
902+ }
903+ });
904+ switch (srcResp.statusCode) {
905+ case 200:
906+ contentType = newHeaders['content-type'];
907+ if (contentType == null) {
908+ srcResp.destroy();
909+ four_oh_four(resp, "No content-type returned", url);
910+ return;
911+ }
912+ contentTypePrefix = contentType.split(";")[0];
913+ if (__indexOf.call(accepted_image_mime_types, contentTypePrefix) < 0) {
914+ srcResp.destroy();
915+ four_oh_four(resp, "Non-Image content-type returned '" + contentTypePrefix + "'", url);
916+ return;
917+ }
918+ debug_log(newHeaders);
919+ resp.writeHead(srcResp.statusCode, newHeaders);
920+ return srcResp.pipe(resp);
921+ case 301:
922+ case 302:
923+ case 303:
924+ case 307:
925+ srcResp.destroy();
926+ if (remaining_redirects <= 0) {
927+ return four_oh_four(resp, "Exceeded max depth", url);
928+ } else if (!srcResp.headers['location']) {
929+ return four_oh_four(resp, "Redirect with no location", url);
930+ } else {
931+ is_finished = false;
932+ newUrl = Url.parse(srcResp.headers['location']);
933+ if (!((newUrl.host != null) && (newUrl.hostname != null))) {
934+ newUrl.host = newUrl.hostname = url.hostname;
935+ newUrl.protocol = url.protocol;
936+ }
937+ debug_log("Redirected to " + (newUrl.format()));
938+ return process_url(newUrl, transferredHeaders, resp, remaining_redirects - 1);
939+ }
940+ break;
941+ case 304:
942+ srcResp.destroy();
943+ return resp.writeHead(srcResp.statusCode, newHeaders);
944+ default:
945+ srcResp.destroy();
946+ return four_oh_four(resp, "Origin responded with " + srcResp.statusCode, url);
947+ }
948+ }
949+ });
950+ srcReq.setTimeout(socket_timeout * 1000, function() {
951+ srcReq.abort();
952+ return four_oh_four(resp, "Socket timeout", url);
953+ });
954+ srcReq.on('error', function(error) {
955+ return four_oh_four(resp, "Client Request error " + error.stack, url);
956+ });
957+ resp.on('close', function() {
958+ error_log("Request aborted");
959+ return srcReq.abort();
960+ });
961+ return resp.on('error', function(e) {
962+ error_log("Request error: " + e);
963+ return srcReq.abort();
964+ });
965+ } else {
966+ return four_oh_four(resp, "No host found " + url.host, url);
967+ }
968+ };
969+
970+ hexdec = function(str) {
971+ var buf, i, _i, _ref;
972+ if (str && str.length > 0 && str.length % 2 === 0 && !str.match(/[^0-9a-f]/)) {
973+ buf = new Buffer(str.length / 2);
974+ for (i = _i = 0, _ref = str.length; _i < _ref; i = _i += 2) {
975+ buf[i / 2] = parseInt(str.slice(i, +(i + 1) + 1 || 9e9), 16);
976+ }
977+ return buf.toString();
978+ }
979+ };
980+
981+ server = Http.createServer(function(req, resp) {
982+ var dest_url, encoded_url, error, hmac, hmac_digest, query_digest, transferredHeaders, url, url_type, user_agent, _base, _ref, _ref1;
983+ if (req.method !== 'GET' || req.url === '/') {
984+ resp.writeHead(200, default_security_headers);
985+ return resp.end('hwhat');
986+ } else if (req.url === '/favicon.ico') {
987+ resp.writeHead(200, default_security_headers);
988+ return resp.end('ok');
989+ } else if (req.url === '/status') {
990+ resp.writeHead(200, default_security_headers);
991+ return resp.end("ok " + current_connections + "/" + total_connections + " since " + (started_at.toString()));
992+ } else {
993+ total_connections += 1;
994+ current_connections += 1;
995+ url = Url.parse(req.url);
996+ user_agent = (_base = process.env).CAMO_HEADER_VIA || (_base.CAMO_HEADER_VIA = "Camo Asset Proxy " + version);
997+ transferredHeaders = {
998+ 'Via': user_agent,
999+ 'User-Agent': user_agent,
1000+ 'Accept': (_ref = req.headers.accept) != null ? _ref : 'image/*',
1001+ 'Accept-Encoding': req.headers['accept-encoding'],
1002+ "X-Frame-Options": default_security_headers["X-Frame-Options"],
1003+ "X-XSS-Protection": default_security_headers["X-XSS-Protection"],
1004+ "X-Content-Type-Options": default_security_headers["X-Content-Type-Options"],
1005+ "Content-Security-Policy": default_security_headers["Content-Security-Policy"]
1006+ };
1007+ delete req.headers.cookie;
1008+ _ref1 = url.pathname.replace(/^\//, '').split("/", 2), query_digest = _ref1[0], encoded_url = _ref1[1];
1009+ if (encoded_url = hexdec(encoded_url)) {
1010+ url_type = 'path';
1011+ dest_url = encoded_url;
1012+ } else {
1013+ url_type = 'query';
1014+ dest_url = QueryString.parse(url.query).url;
1015+ }
1016+ debug_log({
1017+ type: url_type,
1018+ url: req.url,
1019+ headers: req.headers,
1020+ dest: dest_url,
1021+ digest: query_digest
1022+ });
1023+ if (req.headers['via'] && req.headers['via'].indexOf(user_agent) !== -1) {
1024+ return four_oh_four(resp, "Requesting from self");
1025+ }
1026+ if ((url.pathname != null) && dest_url) {
1027+ hmac = Crypto.createHmac("sha1", shared_key);
1028+ try {
1029+ hmac.update(dest_url, 'utf8');
1030+ } catch (_error) {
1031+ error = _error;
1032+ return four_oh_four(resp, "could not create checksum");
1033+ }
1034+ hmac_digest = hmac.digest('hex');
1035+ if (hmac_digest === query_digest) {
1036+ url = Url.parse(dest_url);
1037+ return process_url(url, transferredHeaders, resp, max_redirects);
1038+ } else {
1039+ return four_oh_four(resp, "checksum mismatch " + hmac_digest + ":" + query_digest);
1040+ }
1041+ } else {
1042+ return four_oh_four(resp, "No pathname provided on the server");
1043+ }
1044+ }
1045+ });
1046+
1047+ console.log("SSL-Proxy running on " + port + " with pid:" + process.pid + " version:" + version + ".");
1048+
1049+ server.listen(port);
1050+
1051+}).call(this);
1052
1053=== modified file 'test.gemfile'
1054--- test.gemfile 2013-11-26 13:45:34 +0000
1055+++ test.gemfile 2014-09-02 12:26:21 +0000
1056@@ -1,4 +1,4 @@
1057 source 'https://rubygems.org'
1058+gem 'rack'
1059 gem 'rest-client', '~>1.3'
1060 gem 'addressable', '~>2.3'
1061-gem 'thin'
1062\ No newline at end of file
1063
1064=== modified file 'test.gemfile.lock'
1065--- test.gemfile.lock 2013-11-26 13:45:34 +0000
1066+++ test.gemfile.lock 2014-09-02 12:26:21 +0000
1067@@ -2,21 +2,15 @@
1068 remote: https://rubygems.org/
1069 specs:
1070 addressable (2.3.4)
1071- daemons (1.1.9)
1072- eventmachine (1.0.3)
1073 mime-types (1.23)
1074 rack (1.5.2)
1075 rest-client (1.6.7)
1076 mime-types (>= 1.16)
1077- thin (1.5.1)
1078- daemons (>= 1.0.9)
1079- eventmachine (>= 0.12.6)
1080- rack (>= 1.0.0)
1081
1082 PLATFORMS
1083 ruby
1084
1085 DEPENDENCIES
1086 addressable (~> 2.3)
1087+ rack
1088 rest-client (~> 1.3)
1089- thin
1090
1091=== added file 'test/app_test.rb'
1092--- test/app_test.rb 1970-01-01 00:00:00 +0000
1093+++ test/app_test.rb 2014-09-02 12:26:21 +0000
1094@@ -0,0 +1,12 @@
1095+require 'rubygems'
1096+require 'json'
1097+require 'test/unit'
1098+
1099+class CamoAppTest < Test::Unit::TestCase
1100+ def test_heroku_app_json
1101+ app_file = File.expand_path("../../app.json", __FILE__)
1102+ assert_nothing_raised do
1103+ JSON.parse(File.read(app_file))
1104+ end
1105+ end
1106+end
1107
1108=== modified file 'test/proxy_test.rb'
1109--- test/proxy_test.rb 2013-11-26 13:45:34 +0000
1110+++ test/proxy_test.rb 2014-09-02 12:26:21 +0000
1111@@ -4,7 +4,6 @@
1112 require 'openssl'
1113 require 'rest_client'
1114 require 'addressable/uri'
1115-require 'thin'
1116
1117 require 'test/unit'
1118
1119@@ -14,10 +13,41 @@
1120 'host' => ENV['CAMO_HOST'] || "http://localhost:8081" }
1121 end
1122
1123+ def spawn_server(path)
1124+ port = 9292
1125+ config = "test/servers/#{path}.ru"
1126+ host = "localhost:#{port}"
1127+ pid = fork do
1128+ STDOUT.reopen "/dev/null"
1129+ STDERR.reopen "/dev/null"
1130+ exec "rackup", "--port", port.to_s, config
1131+ end
1132+ sleep 2
1133+ begin
1134+ yield host
1135+ ensure
1136+ Process.kill(:TERM, pid)
1137+ Process.wait(pid)
1138+ end
1139+ end
1140+
1141+ def test_proxy_localhost_test_server
1142+ spawn_server(:ok) do |host|
1143+ response = RestClient.get("http://#{host}/octocat.jpg")
1144+ assert_equal(200, response.code)
1145+
1146+ response = request("http://#{host}/octocat.jpg")
1147+ assert_equal(200, response.code)
1148+ end
1149+ end
1150+
1151 def test_proxy_survives_redirect_without_location
1152- assert_raise RestClient::ResourceNotFound do
1153- request('http://localhost:9292')
1154+ spawn_server(:redirect_without_location) do |host|
1155+ assert_raise RestClient::ResourceNotFound do
1156+ request("http://#{host}")
1157+ end
1158 end
1159+
1160 response = request('http://media.ebaumsworld.com/picture/Mincemeat/Pimp.jpg')
1161 assert_equal(200, response.code)
1162 end
1163@@ -27,16 +57,54 @@
1164 assert_equal(200, response.code)
1165 end
1166
1167+ def test_doesnt_crash_with_non_url_encoded_url
1168+ assert_raise RestClient::ResourceNotFound do
1169+ RestClient.get("#{config['host']}/crashme?url=crash&url=me")
1170+ end
1171+ end
1172+
1173+ def test_always_sets_security_headers
1174+ ['/', '/status'].each do |path|
1175+ response = RestClient.get("#{config['host']}#{path}")
1176+ assert_equal "deny", response.headers[:x_frame_options]
1177+ assert_equal "default-src 'none'; style-src 'unsafe-inline'", response.headers[:content_security_policy]
1178+ assert_equal "nosniff", response.headers[:x_content_type_options]
1179+ assert_equal "max-age=31536000; includeSubDomains", response.headers[:strict_transport_security]
1180+ end
1181+
1182+ response = request('http://dl.dropbox.com/u/602885/github/soldier-squirrel.jpg')
1183+ assert_equal "deny", response.headers[:x_frame_options]
1184+ assert_equal "default-src 'none'; style-src 'unsafe-inline'", response.headers[:content_security_policy]
1185+ assert_equal "nosniff", response.headers[:x_content_type_options]
1186+ assert_equal "max-age=31536000; includeSubDomains", response.headers[:strict_transport_security]
1187+ end
1188+
1189 def test_proxy_valid_image_url
1190 response = request('http://media.ebaumsworld.com/picture/Mincemeat/Pimp.jpg')
1191 assert_equal(200, response.code)
1192 end
1193
1194+ def test_svg_image_with_delimited_content_type_url
1195+ response = request('https://saucelabs.com/browser-matrix/bootstrap.svg')
1196+ assert_equal(200, response.code)
1197+ end
1198+
1199+ def test_png_image_with_delimited_content_type_url
1200+ response = request('http://uploadir.com/u/cm5el1v7')
1201+ assert_equal(200, response.code)
1202+ end
1203+
1204 def test_proxy_valid_image_url_with_crazy_subdomain
1205 response = request('http://27.media.tumblr.com/tumblr_lkp6rdDfRi1qce6mto1_500.jpg')
1206 assert_equal(200, response.code)
1207 end
1208
1209+ def test_strict_image_content_type_checking
1210+ assert_raise RestClient::ResourceNotFound do
1211+ request("http://calm-shore-1799.herokuapp.com/foo.png")
1212+ end
1213+ end
1214+
1215 def test_proxy_valid_google_chart_url
1216 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')
1217 assert_equal(200, response.code)
1218@@ -48,6 +116,16 @@
1219 assert_nil(response.headers[:content_length])
1220 end
1221
1222+ def test_proxy_https_octocat
1223+ response = request('https://octodex.github.com/images/original.png')
1224+ assert_equal(200, response.code)
1225+ end
1226+
1227+ def test_proxy_https_gravatar
1228+ response = request('https://1.gravatar.com/avatar/a86224d72ce21cd9f5bee6784d4b06c7')
1229+ assert_equal(200, response.code)
1230+ end
1231+
1232 def test_follows_redirects
1233 response = request('http://cl.ly/1K0X2Y2F1P0o3z140p0d/boom-headshot.gif')
1234 assert_equal(200, response.code)
1235@@ -64,6 +142,14 @@
1236 end
1237 end
1238
1239+ def test_404s_on_request_error
1240+ spawn_server(:crash_request) do |host|
1241+ assert_raise RestClient::ResourceNotFound do
1242+ request("http://#{host}/cats.png")
1243+ end
1244+ end
1245+ end
1246+
1247 def test_404s_on_infinidirect
1248 assert_raise RestClient::ResourceNotFound do
1249 request('http://modeselektor.herokuapp.com/')
1250@@ -94,32 +180,12 @@
1251 end
1252 end
1253
1254- def test_404s_on_10_0_ip_range
1255+ def test_404s_on_connect_timeout
1256 assert_raise RestClient::ResourceNotFound do
1257 request('http://10.0.0.1/foo.cgi')
1258 end
1259 end
1260
1261- 16.upto(31) do |i|
1262- define_method :"test_404s_on_172_#{i}_ip_range" do
1263- assert_raise RestClient::ResourceNotFound do
1264- request("http://172.#{i}.0.1/foo.cgi")
1265- end
1266- end
1267- end
1268-
1269- def test_404s_on_169_254_ip_range
1270- assert_raise RestClient::ResourceNotFound do
1271- request('http://169.254.0.1/foo.cgi')
1272- end
1273- end
1274-
1275- def test_404s_on_192_168_ip_range
1276- assert_raise RestClient::ResourceNotFound do
1277- request('http://192.168.0.1/foo.cgi')
1278- end
1279- end
1280-
1281 def test_404s_on_environmental_excludes
1282 assert_raise RestClient::ResourceNotFound do
1283 request('http://iphone.internal.example.org/foo.cgi')
1284@@ -127,7 +193,7 @@
1285 end
1286
1287 def test_follows_temporary_redirects
1288- response = request('http://d.pr/i/rr7F+')
1289+ response = request('http://bit.ly/1l9Fztb')
1290 assert_equal(200, response.code)
1291 end
1292
1293@@ -137,6 +203,14 @@
1294 response = request( uri )
1295 end
1296 end
1297+
1298+ def test_404s_send_cache_headers
1299+ uri = request_uri("http://example.org/")
1300+ response = RestClient.get(uri){ |response, request, result| response }
1301+ assert_equal(404, response.code)
1302+ assert_equal("0", response.headers[:expires])
1303+ assert_equal("no-cache, no-store, private, must-revalidate", response.headers[:cache_control])
1304+ end
1305 end
1306
1307 class CamoProxyQueryStringTest < Test::Unit::TestCase
1308
1309=== removed file 'test/proxy_test_server.rb'
1310--- test/proxy_test_server.rb 2013-11-26 13:45:34 +0000
1311+++ test/proxy_test_server.rb 1970-01-01 00:00:00 +0000
1312@@ -1,11 +0,0 @@
1313-require 'thin'
1314-
1315-class ProxyTestServer
1316- def call(env)
1317- [302, {"Content-Type" => "image/foo"}, "test"]
1318- end
1319-end
1320-
1321-Thin::Server.start('127.0.0.1', 9292) do
1322- run ProxyTestServer.new
1323-end
1324\ No newline at end of file
1325
1326=== added directory 'test/servers'
1327=== added file 'test/servers/crash_request.ru'
1328--- test/servers/crash_request.ru 1970-01-01 00:00:00 +0000
1329+++ test/servers/crash_request.ru 2014-09-02 12:26:21 +0000
1330@@ -0,0 +1,3 @@
1331+run lambda { |env|
1332+ raise "b00m"
1333+}
1334
1335=== added file 'test/servers/octocat.jpg'
1336Binary files test/servers/octocat.jpg 1970-01-01 00:00:00 +0000 and test/servers/octocat.jpg 2014-09-02 12:26:21 +0000 differ
1337=== added file 'test/servers/ok.ru'
1338--- test/servers/ok.ru 1970-01-01 00:00:00 +0000
1339+++ test/servers/ok.ru 2014-09-02 12:26:21 +0000
1340@@ -0,0 +1,5 @@
1341+run lambda { |env|
1342+ path = File.expand_path('../octocat.jpg', __FILE__)
1343+ data = File.read(path)
1344+ [200, {'Content-Type' => 'image/jpeg'}, [data]]
1345+}
1346
1347=== added file 'test/servers/redirect_without_location.ru'
1348--- test/servers/redirect_without_location.ru 1970-01-01 00:00:00 +0000
1349+++ test/servers/redirect_without_location.ru 2014-09-02 12:26:21 +0000
1350@@ -0,0 +1,7 @@
1351+class ProxyTestServer
1352+ def call(env)
1353+ [302, {"Content-Type" => "image/foo"}, "test"]
1354+ end
1355+end
1356+
1357+run ProxyTestServer.new

Subscribers

People subscribed via source and target branches

to all changes: