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.
- data/lib/rack/wwwhisper.rb +22 -80
- data/test/test_wwwhisper.rb +12 -63
- metadata +4 -4
data/lib/rack/wwwhisper.rb
CHANGED
@@ -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
|
-
#
|
55
|
+
# Path prefix of requests that are passed to wwwhisper.
|
64
56
|
@@WWWHISPER_PREFIX = '/wwwhisper/'
|
65
|
-
#
|
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 =
|
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(
|
233
|
-
sub_req['
|
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(
|
241
|
-
|
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
|
-
#
|
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
|
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
|
data/test/test_wwwhisper.rb
CHANGED
@@ -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
|
263
|
+
def test_x_forwarded_headers
|
266
264
|
path = '/wwwhisper/admin/index.html'
|
267
265
|
|
268
|
-
#
|
266
|
+
# X-Forwarded headers must be sent to wwwhisper backend.
|
269
267
|
stub_request(:get, full_url(@wwwhisper.auth_query(path))).
|
270
|
-
with(:headers => {
|
271
|
-
|
272
|
-
|
273
|
-
|
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 => {
|
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 => {'
|
293
|
+
with(:headers => {'X-Forwarded-Host' => host}).
|
311
294
|
to_return(:status => 401, :body => 'Login required', :headers => {})
|
312
295
|
|
313
|
-
|
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
|
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
|
+
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:
|
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:
|
143
|
+
hash: 3463097767127012702
|
144
144
|
requirements: []
|
145
145
|
rubyforge_project:
|
146
146
|
rubygems_version: 1.8.24
|