rack-wwwhisper 1.0.10 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. data/lib/rack/wwwhisper.rb +22 -80
  2. data/test/test_wwwhisper.rb +12 -63
  3. metadata +4 -4
@@ -36,7 +36,6 @@ class NoPublicCache
36
36
  end
37
37
  [status, headers, body]
38
38
  end
39
-
40
39
  end
41
40
 
42
41
  # Communicates with the wwwhisper service to authorize each incomming
@@ -52,22 +51,16 @@ end
52
51
  # page is returned.
53
52
  # [403] the user is not authorized, request is denied, error is returned.
54
53
  # [any other] error while communicating with wwwhisper, request is denied.
55
- #
56
- # For Persona assertion verification the middleware depends on a
57
- # 'Host' header being verified by a frontend server. This is true on
58
- # Heroku, where the 'Host' header is rewritten if a request sets it to
59
- # incorrect value. If the frontend server does does not perform such
60
- # verification, SITE_URL environment variable must be set to enforce a
61
- # valid url (for example `export SITE_URL="\https://example.com"`).
62
54
  class WWWhisper
63
- # Requests to locations starting with this prefix are passed to wwwhisper.
55
+ # Path prefix of requests that are passed to wwwhisper.
64
56
  @@WWWHISPER_PREFIX = '/wwwhisper/'
65
- # Cookies starting with this prefix are passed to wwwhisper.
57
+ # Name prefix of cookies that are passed to wwwhisper.
66
58
  @@AUTH_COOKIES_PREFIX = 'wwwhisper'
67
59
  # Headers that are passed to wwwhisper ('Cookie' is handled
68
60
  # in a special way: only wwwhisper related cookies are passed).
61
+ # In addition, the original Host header is passed as X-Forwarded-Host.
69
62
  @@FORWARDED_HEADERS = ['Accept', 'Accept-Language', 'Cookie', 'Origin',
70
- 'X-CSRFToken', 'X-Requested-With']
63
+ 'X-CSRFToken', 'X-Forwarded-Proto', 'X-Requested-With']
71
64
  @@DEFAULT_IFRAME = \
72
65
  %Q[<iframe id="wwwhisper-iframe" src="%s" width="340" height="29"
73
66
  frameborder="0" scrolling="no" style="position:fixed; overflow:hidden;
@@ -88,9 +81,6 @@ class WWWhisper
88
81
  #
89
82
  # 3. WWWHISPER_IFRAME: an HTML snippet that should be injected to
90
83
  # returned HTML documents (has a default value).
91
- #
92
- # 4. SITE_URL: must be set if the frontend server does not validate
93
- # the Host header.
94
84
  def initialize(app)
95
85
  @app = app
96
86
  if ENV['WWWHISPER_DISABLE'] == "1"
@@ -126,13 +116,14 @@ class WWWhisper
126
116
 
127
117
  def call(env)
128
118
  req = Request.new(env)
119
+ normalize_path(req)
129
120
 
130
121
  # Requests to /@@WWWHISPER_PREFIX/auth/ should not be authorized,
131
122
  # every visitor can access login pages.
132
123
  return dispatch(req) if req.path =~ %r{^#{wwwhisper_path('auth')}}
133
124
 
134
125
  debug req, "sending auth request for #{req.path}"
135
- auth_resp = wwwhisper_auth_request(req)
126
+ auth_resp = auth_request(req)
136
127
 
137
128
  if auth_resp.code == '200'
138
129
  debug req, 'access granted'
@@ -153,59 +144,6 @@ class WWWhisper
153
144
  end
154
145
 
155
146
  private
156
- # Extends Rack::Request with more conservative scheme, host and port
157
- # setting rules. Rack::Request tries to obtain these values from
158
- # mutiple sources, whereas for wwwhisper it is crucial that the
159
- # values are not spoofed by the client.
160
- #
161
- # If SITE_URL environemnt variable is set: scheme, host and port are
162
- # always taken directly from it.
163
- #
164
- # If SITE_URL is not set, scheme is taken from the X-Forwarded-Proto
165
- # header, host is taken from the 'Host' header and port is taken
166
- # from the X-Forwarded-Port header. The frontend must ensure these
167
- # values can not be spoofed by client (a request to example.com,
168
- # that carries a Host header 'example.org' should be dropped or
169
- # rewritten).
170
- class Request < Rack::Request
171
- attr_reader :scheme, :host, :port, :site_url
172
-
173
- def initialize(env)
174
- super(env)
175
- normalize_path
176
- if (@site_url = ENV['SITE_URL'])
177
- uri = Addressable::URI.parse(@site_url)
178
- @scheme = uri.scheme
179
- @host = uri.host
180
- @port = uri.port || default_port(@scheme)
181
- else
182
- @scheme = env['HTTP_X_FORWARDED_PROTO'] || 'http'
183
- @host, port_from_host = env['HTTP_HOST'].split(/:/)
184
- @port = (env['HTTP_X_FORWARDED_PORT'] || port_from_host ||
185
- default_port(@scheme)).to_i
186
- port_str = @port != default_port(@scheme) ? ":#{@port}" : ""
187
- @site_url = "#{@scheme}://#{@host}#{port_str}"
188
- end
189
- end
190
-
191
- private
192
- def normalize_path
193
- self.script_name =
194
- Addressable::URI.normalize_path(script_name).squeeze('/')
195
- self.path_info =
196
- Addressable::URI.normalize_path(path_info).squeeze('/')
197
- # Avoid /foo/ /bar being combined into /foo//bar
198
- self.script_name.chomp!('/') if self.path_info[0] == ?/
199
- end
200
-
201
- def default_port(proto)
202
- {
203
- 'http' => 80,
204
- 'https' => 443,
205
- }[proto]
206
- end
207
- end
208
-
209
147
  def debug(req, message)
210
148
  req.logger.debug "wwwhisper #{message}" if req.logger
211
149
  end
@@ -227,18 +165,28 @@ class WWWhisper
227
165
  return http
228
166
  end
229
167
 
168
+ def normalize_path(req)
169
+ req.script_name =
170
+ Addressable::URI.normalize_path(req.script_name).squeeze('/')
171
+ req.path_info =
172
+ Addressable::URI.normalize_path(req.path_info).squeeze('/')
173
+ # Avoid /foo/ and /bar being combined into /foo//bar
174
+ req.script_name.chomp!('/') if req.path_info[0] == ?/
175
+ end
176
+
230
177
  def sub_request_init(rack_req, method, path)
231
178
  sub_req = Net::HTTP.const_get(method).new(path)
232
- copy_headers(@@FORWARDED_HEADERS, rack_req.env, sub_req)
233
- sub_req['Site-Url'] = rack_req.site_url
179
+ copy_headers(rack_req.env, sub_req)
180
+ sub_req['X-Forwarded-Host'] = rack_req.env['HTTP_HOST']
181
+ sub_req['X-Forwarded-Proto'] ||= rack_req.scheme
234
182
  if @wwwhisper_uri.user and @wwwhisper_uri.password
235
183
  sub_req.basic_auth(@wwwhisper_uri.user, @wwwhisper_uri.password)
236
184
  end
237
185
  sub_req
238
186
  end
239
187
 
240
- def copy_headers(headers_names, env, sub_req)
241
- headers_names.each do |header|
188
+ def copy_headers(env, sub_req)
189
+ @@FORWARDED_HEADERS.each do |header|
242
190
  key = "HTTP_#{header.upcase}".gsub(/-/, '_')
243
191
  value = env[key]
244
192
  if value and key == 'HTTP_COOKIE'
@@ -263,12 +211,6 @@ class WWWhisper
263
211
  def sub_response_headers_to_rack(rack_req, sub_resp)
264
212
  rack_headers = Rack::Utils::HeaderHash.new()
265
213
  sub_resp.each_capitalized do |header, value|
266
- if header == 'Location'
267
- location = Addressable::URI.parse(value)
268
- location.scheme, location.host, location.port =
269
- rack_req.scheme, rack_req.host, rack_req.port
270
- value = location.to_s
271
- end
272
214
  # If sub request returned chunked response, remove the header
273
215
  # (chunks will be combined and returned with 'Content-Length).
274
216
  rack_headers[header] = value if header !~ /Transfer-Encoding|Set-Cookie/
@@ -285,7 +227,7 @@ class WWWhisper
285
227
  headers = sub_response_headers_to_rack(rack_req, sub_resp)
286
228
  body = sub_resp.read_body() || ''
287
229
  if code < 200 or [204, 205, 304].include?(code)
288
- # See Rack SPEC.
230
+ # To make sure Rack SPEC is respected.
289
231
  headers.delete('Content-Length')
290
232
  headers.delete('Content-Type')
291
233
  elsif (body.length || 0) != 0 and not headers['Content-Length']
@@ -294,7 +236,7 @@ class WWWhisper
294
236
  [ code, headers, [body] ]
295
237
  end
296
238
 
297
- def wwwhisper_auth_request(req)
239
+ def auth_request(req)
298
240
  auth_req = sub_request_init(req, 'Get', auth_query(req.path))
299
241
  @http.request(@wwwhisper_uri, auth_req)
300
242
  end
@@ -40,7 +40,6 @@ class TestWWWhisper < Test::Unit::TestCase
40
40
  WWWHISPER_URL = 'https://wwwhisper.org'
41
41
  SITE_PROTO = 'https'
42
42
  SITE_HOST = 'bar.io'
43
- SITE_PORT = 443
44
43
 
45
44
  def setup()
46
45
  @backend = MockBackend.new(TEST_USER)
@@ -61,7 +60,6 @@ class TestWWWhisper < Test::Unit::TestCase
61
60
  def get path, params={}, rack_env={}
62
61
  rack_env['HTTP_HOST'] ||= SITE_HOST
63
62
  rack_env['HTTP_X_FORWARDED_PROTO'] ||= SITE_PROTO
64
- rack_env['HTTP_X_FORWARDED_PORT'] ||= SITE_PORT
65
63
  super path, params, rack_env
66
64
  end
67
65
 
@@ -262,35 +260,21 @@ class TestWWWhisper < Test::Unit::TestCase
262
260
  assert_equal 'invalid request', last_response.body
263
261
  end
264
262
 
265
- def test_site_url
263
+ def test_x_forwarded_headers
266
264
  path = '/wwwhisper/admin/index.html'
267
265
 
268
- # Site-Url header should be sent to wwwhisper backend.
266
+ # X-Forwarded headers must be sent to wwwhisper backend.
269
267
  stub_request(:get, full_url(@wwwhisper.auth_query(path))).
270
- with(:headers => {'Site-Url' => "#{SITE_PROTO}://#{SITE_HOST}"}).
271
- to_return(granted())
272
- stub_request(:get, full_url(path)).
273
- with(:headers => {'Site-Url' => "#{SITE_PROTO}://#{SITE_HOST}"}).
274
- to_return(:status => 200, :body => 'Admin page', :headers => {})
275
-
276
- get path
277
- assert last_response.ok?
278
- assert_equal 200, last_response.status
279
- assert_equal 'Admin page', last_response.body
280
- assert_requested :get, full_url(@wwwhisper.auth_query(path))
281
- assert_requested :get, full_url(path)
282
- end
283
-
284
- def test_explicit_site_url_takes_precedense
285
- path = '/wwwhisper/admin/index.html'
286
-
287
- site_url = 'https://foo.bar.com'
288
- ENV['SITE_URL'] = site_url
289
- stub_request(:get, full_url(@wwwhisper.auth_query(path))).
290
- with(:headers => {'Site-Url' => site_url}).
268
+ with(:headers => {
269
+ 'X-Forwarded-Host' => SITE_HOST,
270
+ 'X-Forwarded-Proto' => SITE_PROTO,
271
+ }).
291
272
  to_return(granted())
292
273
  stub_request(:get, full_url(path)).
293
- with(:headers => {'Site-Url' => site_url}).
274
+ with(:headers => {
275
+ 'X-Forwarded-Host' => SITE_HOST,
276
+ 'X-Forwarded-Proto' => SITE_PROTO,
277
+ }).
294
278
  to_return(:status => 200, :body => 'Admin page', :headers => {})
295
279
 
296
280
  get path
@@ -299,7 +283,6 @@ class TestWWWhisper < Test::Unit::TestCase
299
283
  assert_equal 'Admin page', last_response.body
300
284
  assert_requested :get, full_url(@wwwhisper.auth_query(path))
301
285
  assert_requested :get, full_url(path)
302
- ENV['SITE_URL'] = site_url
303
286
  end
304
287
 
305
288
  def test_host_with_port
@@ -307,48 +290,14 @@ class TestWWWhisper < Test::Unit::TestCase
307
290
  host = 'localhost:5000'
308
291
  stub_request(:get, full_url(@wwwhisper.auth_query(path))).
309
292
  # Test makes sure that port is not repeated in Site-Url.
310
- with(:headers => {'Site-Url' => "#{SITE_PROTO}://#{host}"}).
293
+ with(:headers => {'X-Forwarded-Host' => host}).
311
294
  to_return(:status => 401, :body => 'Login required', :headers => {})
312
295
 
313
- # TODO: start here
314
- get(path, {}, {'HTTP_HOST' => host, 'HTTP_X_FORWARDED_PORT' => '5000'})
296
+ get(path, {}, {'HTTP_HOST' => host})
315
297
  assert_equal 401, last_response.status
316
298
  assert_requested :get, full_url(@wwwhisper.auth_query(path))
317
299
  end
318
300
 
319
- def test_site_url_with_non_default_port
320
- path = '/foo/bar'
321
- port = 11235
322
- # Site-Url header should be sent to wwwhisper backend but not to
323
- # assets server.
324
- stub_request(:get, full_url(@wwwhisper.auth_query(path))).
325
- with(:headers => {'Site-Url' => "#{SITE_PROTO}://#{SITE_HOST}:#{port}"}).
326
- to_return(:status => 400, :body => '', :headers => {})
327
-
328
- get(path, {}, {'HTTP_X_FORWARDED_PORT' => port.to_s})
329
- assert !last_response.ok?
330
- assert_equal 400, last_response.status
331
- assert_requested :get, full_url(@wwwhisper.auth_query(path))
332
- end
333
-
334
- def test_redirects
335
- path = '/wwwhisper/admin/index.html'
336
- stub_request(:get, full_url(@wwwhisper.auth_query(path))).
337
- to_return(granted())
338
- stub_request(:get, full_url(path)).
339
- to_return(:status => 303, :body => 'Admin page moved',
340
- :headers => {'Location' => 'https://new.location/foo/bar'})
341
-
342
- get path
343
- assert !last_response.ok?
344
- assert_equal 303, last_response.status
345
- assert_equal 'Admin page moved', last_response.body
346
- assert_equal("#{SITE_PROTO}://#{SITE_HOST}:#{SITE_PORT}/foo/bar",
347
- last_response['Location'])
348
- assert_requested :get, full_url(@wwwhisper.auth_query(path))
349
- assert_requested :get, full_url(path)
350
- end
351
-
352
301
  def test_disable_wwwhisper
353
302
  ENV.delete('WWWHISPER_URL')
354
303
  ENV['WWWHISPER_DISABLE'] = "1"
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rack-wwwhisper
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.10
4
+ version: 1.1.0
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2013-02-12 00:00:00.000000000 Z
12
+ date: 2013-02-14 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: rack
@@ -131,7 +131,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
131
131
  version: '0'
132
132
  segments:
133
133
  - 0
134
- hash: 2690692544216972631
134
+ hash: 3463097767127012702
135
135
  required_rubygems_version: !ruby/object:Gem::Requirement
136
136
  none: false
137
137
  requirements:
@@ -140,7 +140,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
140
140
  version: '0'
141
141
  segments:
142
142
  - 0
143
- hash: 2690692544216972631
143
+ hash: 3463097767127012702
144
144
  requirements: []
145
145
  rubyforge_project:
146
146
  rubygems_version: 1.8.24