rack-wwwhisper 1.0.10 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
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