Merge lp:~ianchou821/ubuntu/utopic/camo/bug-1355443 into lp:ubuntu/utopic/camo
- Utopic (14.10)
- bug-1355443
- Merge into utopic
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 | ||||
Related bugs: |
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Ubuntu branches | Pending | ||
Review via email: mp+233033@code.launchpad.net |
Commit message
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
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' |
1336 | Binary 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 |
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/UtopicUnico rn/ReleaseSched ule) 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/FeatureFree ze and https:/ /wiki.ubuntu. com/FreezeExcep tionProcess 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.