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.
- 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
|