rack-wwwhisper 1.0.2.pre → 1.0.3.pre
Sign up to get free protection for your applications and to get access to all the features.
- data/lib/rack/wwwhisper.rb +69 -42
- data/test/test_wwwhisper.rb +62 -20
- metadata +2 -2
data/lib/rack/wwwhisper.rb
CHANGED
@@ -24,6 +24,14 @@ class WWWhisper
|
|
24
24
|
|
25
25
|
def initialize(app)
|
26
26
|
@app = app
|
27
|
+
|
28
|
+
if ENV['WWWHISPER_DISABLE'] == "1"
|
29
|
+
def self.call(env)
|
30
|
+
@app.call(env)
|
31
|
+
end
|
32
|
+
return
|
33
|
+
end
|
34
|
+
|
27
35
|
if not ENV['WWWHISPER_URL']
|
28
36
|
raise StandardError, 'WWWHISPER_URL environment variable not set'
|
29
37
|
end
|
@@ -79,9 +87,7 @@ class WWWhisper
|
|
79
87
|
end
|
80
88
|
|
81
89
|
def call(env)
|
82
|
-
req =
|
83
|
-
|
84
|
-
normalize_path req
|
90
|
+
req = Request.new(env)
|
85
91
|
|
86
92
|
if req.path =~ %r{^#{@@WWWHISPER_PREFIX}auth}
|
87
93
|
# Requests to /@@WWWHISPER_PREFIX/auth/ should not be authorized,
|
@@ -94,10 +100,12 @@ class WWWhisper
|
|
94
100
|
|
95
101
|
if auth_resp.code == '200'
|
96
102
|
debug req, 'access granted'
|
103
|
+
env['REMOTE_USER'] = auth_resp['User']
|
97
104
|
status, headers, body = dispatch(req)
|
98
105
|
if should_inject_iframe(status, headers)
|
99
106
|
body = inject_iframe(headers, body)
|
100
107
|
end
|
108
|
+
headers['User'] = auth_resp['User']
|
101
109
|
[status, headers, body]
|
102
110
|
else
|
103
111
|
debug req, {
|
@@ -110,13 +118,51 @@ class WWWhisper
|
|
110
118
|
|
111
119
|
private
|
112
120
|
|
113
|
-
|
114
|
-
|
121
|
+
class Request < Rack::Request
|
122
|
+
def initialize(env)
|
123
|
+
super(env)
|
124
|
+
normalize_path
|
125
|
+
end
|
126
|
+
|
127
|
+
def scheme
|
128
|
+
env['HTTP_X_FORWARDED_PROTO'] || 'http'
|
129
|
+
end
|
130
|
+
|
131
|
+
def host
|
132
|
+
env['HTTP_HOST']
|
133
|
+
end
|
134
|
+
|
135
|
+
def port
|
136
|
+
env['HTTP_X_FORWARDED_PORT'] || default_port(scheme)
|
137
|
+
end
|
138
|
+
|
139
|
+
def site_url
|
140
|
+
port_str = port != default_port(scheme) ? ":#{port}" : ""
|
141
|
+
"#{scheme}://#{host}#{port_str}"
|
142
|
+
end
|
143
|
+
|
144
|
+
private
|
145
|
+
def normalize_path()
|
146
|
+
self.script_name =
|
147
|
+
Addressable::URI.normalize_path(script_name).squeeze('/')
|
148
|
+
self.path_info =
|
149
|
+
Addressable::URI.normalize_path(path_info).squeeze('/')
|
150
|
+
# Avoid /foo/ /bar being combined into /foo//bar
|
151
|
+
if self.path_info[0] == ?/
|
152
|
+
self.script_name.chomp!('/')
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
156
|
+
def default_port(proto)
|
157
|
+
{
|
158
|
+
'http' => 80,
|
159
|
+
'https' => 443,
|
160
|
+
}[proto]
|
161
|
+
end
|
115
162
|
end
|
116
163
|
|
117
|
-
def
|
118
|
-
req.
|
119
|
-
req.path_info = Addressable::URI.normalize_path(req.path_info)
|
164
|
+
def debug(req, message)
|
165
|
+
req.logger.debug "wwwhisper #{message}" if req.logger
|
120
166
|
end
|
121
167
|
|
122
168
|
def parse_uri(uri)
|
@@ -127,30 +173,6 @@ class WWWhisper
|
|
127
173
|
parsed_uri
|
128
174
|
end
|
129
175
|
|
130
|
-
def default_port(proto)
|
131
|
-
{
|
132
|
-
'http' => 80,
|
133
|
-
'https' => 443,
|
134
|
-
}[proto]
|
135
|
-
end
|
136
|
-
|
137
|
-
def proto_host_port(env)
|
138
|
-
proto = env['HTTP_X_FORWARDED_PROTO'] || 'http'
|
139
|
-
return proto,
|
140
|
-
env['HTTP_HOST'],
|
141
|
-
env['HTTP_X_FORWARDED_PORT'] || default_port(proto)
|
142
|
-
end
|
143
|
-
|
144
|
-
def site_url(env)
|
145
|
-
proto, host, port = proto_host_port(env)
|
146
|
-
port_str = if port != default_port(proto)
|
147
|
-
":#{port}"
|
148
|
-
else
|
149
|
-
''
|
150
|
-
end
|
151
|
-
"#{proto}://#{host}#{port_str}"
|
152
|
-
end
|
153
|
-
|
154
176
|
def http_init(connection_id)
|
155
177
|
http = Net::HTTP::Persistent.new(connection_id)
|
156
178
|
store = OpenSSL::X509::Store.new()
|
@@ -164,7 +186,7 @@ class WWWhisper
|
|
164
186
|
path = @aliases[path] || path
|
165
187
|
sub_req = Net::HTTP.const_get(method).new(path)
|
166
188
|
copy_headers(config[:forwarded_headers], rack_req.env, sub_req)
|
167
|
-
sub_req['Site-Url'] =
|
189
|
+
sub_req['Site-Url'] = rack_req.site_url if config[:send_site_url]
|
168
190
|
uri = config[:uri]
|
169
191
|
sub_req.basic_auth(uri.user, uri.password) if uri.user and uri.password
|
170
192
|
sub_req
|
@@ -198,20 +220,23 @@ class WWWhisper
|
|
198
220
|
if header == 'Location'
|
199
221
|
location = Addressable::URI.parse(value)
|
200
222
|
location.scheme, location.host, location.port =
|
201
|
-
|
223
|
+
rack_req.scheme, rack_req.host, rack_req.port
|
202
224
|
value = location.to_s
|
203
225
|
end
|
204
|
-
|
226
|
+
# If sub request returned chunked response, remove the header
|
227
|
+
# (chunks will be combined and returned with 'Content-Length).
|
228
|
+
rack_headers[header] = value if header != 'Transfer-Encoding'
|
205
229
|
end
|
206
230
|
return rack_headers
|
207
231
|
end
|
208
232
|
|
209
233
|
def sub_response_to_rack(rack_req, sub_resp)
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
234
|
+
headers = sub_response_headers_to_rack(rack_req, sub_resp)
|
235
|
+
body = sub_resp.read_body() || ''
|
236
|
+
if body.length and not headers['Content-Length']
|
237
|
+
headers['Content-Length'] = Rack::Utils::bytesize(body).to_s
|
238
|
+
end
|
239
|
+
[ sub_resp.code.to_i, headers, [body] ]
|
215
240
|
end
|
216
241
|
|
217
242
|
def wwwhisper_auth_request(req)
|
@@ -237,6 +262,8 @@ class WWWhisper
|
|
237
262
|
body.each { |part|
|
238
263
|
total << part
|
239
264
|
}
|
265
|
+
body.close if body.respond_to?(:close)
|
266
|
+
|
240
267
|
total = total.join()
|
241
268
|
if idx = total.rindex('</body>')
|
242
269
|
total.insert(idx, @wwwhisper_iframe)
|
@@ -269,6 +296,6 @@ class WWWhisper
|
|
269
296
|
end
|
270
297
|
end
|
271
298
|
|
272
|
-
end
|
299
|
+
end # class WWWhisper
|
273
300
|
|
274
|
-
end
|
301
|
+
end # module
|
data/test/test_wwwhisper.rb
CHANGED
@@ -11,14 +11,20 @@ require 'rack/wwwhisper'
|
|
11
11
|
|
12
12
|
ENV['RACK_ENV'] = 'test'
|
13
13
|
|
14
|
+
TEST_USER = 'foo@example.com'
|
15
|
+
|
14
16
|
class MockBackend
|
17
|
+
include Test::Unit::Assertions
|
15
18
|
attr_accessor :response
|
16
19
|
|
17
|
-
def initialize()
|
20
|
+
def initialize(email)
|
18
21
|
@response = [200, {'Content-Type' => 'text/html'}, ['Hello World']]
|
22
|
+
@expected_email = email
|
19
23
|
end
|
20
24
|
|
21
25
|
def call(env)
|
26
|
+
# Make sure remote user is set by wwwhisper.
|
27
|
+
assert_equal @expected_email, env['REMOTE_USER']
|
22
28
|
@response
|
23
29
|
end
|
24
30
|
end
|
@@ -32,7 +38,8 @@ class TestWWWhisper < Test::Unit::TestCase
|
|
32
38
|
SITE_PORT = 443
|
33
39
|
|
34
40
|
def setup()
|
35
|
-
@backend = MockBackend.new()
|
41
|
+
@backend = MockBackend.new(TEST_USER)
|
42
|
+
ENV.delete('WWWHISPER_DISABLE')
|
36
43
|
ENV['WWWHISPER_URL'] = WWWHISPER_URL
|
37
44
|
ENV['WWWHISPER_ASSETS_URL'] = WWWHISPER_ASSETS_URL
|
38
45
|
@wwwhisper = Rack::WWWhisper.new(@backend)
|
@@ -69,14 +76,19 @@ class TestWWWhisper < Test::Unit::TestCase
|
|
69
76
|
@wwwhisper.auth_query('/foo/bar'))
|
70
77
|
end
|
71
78
|
|
79
|
+
def granted()
|
80
|
+
{:status => 200, :body => '', :headers => {'User' => TEST_USER}}
|
81
|
+
end
|
82
|
+
|
72
83
|
def test_request_allowed
|
73
84
|
path = '/foo/bar'
|
74
85
|
stub_request(:get, full_url(@wwwhisper.auth_query(path))).
|
75
|
-
to_return(
|
86
|
+
to_return(granted())
|
76
87
|
|
77
88
|
get path
|
78
89
|
assert last_response.ok?
|
79
90
|
assert_equal 'Hello World', last_response.body
|
91
|
+
assert_equal TEST_USER, last_response['User']
|
80
92
|
assert_requested :get, full_url(@wwwhisper.auth_query(path))
|
81
93
|
end
|
82
94
|
|
@@ -107,7 +119,7 @@ class TestWWWhisper < Test::Unit::TestCase
|
|
107
119
|
def test_iframe_injected_to_html_response
|
108
120
|
path = '/foo/bar'
|
109
121
|
stub_request(:get, full_url(@wwwhisper.auth_query(path))).
|
110
|
-
to_return(
|
122
|
+
to_return(granted())
|
111
123
|
# wwwhisper iframe is injected only when response is a html document
|
112
124
|
# with <body></body>
|
113
125
|
body = '<html><body>Hello World</body></html>'
|
@@ -122,7 +134,7 @@ class TestWWWhisper < Test::Unit::TestCase
|
|
122
134
|
def test_iframe_not_injected_to_non_html_response
|
123
135
|
path = '/foo/bar'
|
124
136
|
stub_request(:get, full_url(@wwwhisper.auth_query(path))).
|
125
|
-
to_return(
|
137
|
+
to_return(granted())
|
126
138
|
body = '<html><body>Hello World</body></html>'
|
127
139
|
@backend.response= [200, {'Content-Type' => 'text/plain'}, [body]]
|
128
140
|
|
@@ -132,6 +144,18 @@ class TestWWWhisper < Test::Unit::TestCase
|
|
132
144
|
assert_requested :get, full_url(@wwwhisper.auth_query(path))
|
133
145
|
end
|
134
146
|
|
147
|
+
def test_response_body_combined
|
148
|
+
path = '/foo/bar'
|
149
|
+
stub_request(:get, full_url(@wwwhisper.auth_query(path))).
|
150
|
+
to_return(granted())
|
151
|
+
@backend.response= [200, {'Content-Type' => 'text/html'},
|
152
|
+
['abc', 'def', 'ghi']]
|
153
|
+
get path
|
154
|
+
assert last_response.ok?
|
155
|
+
assert_equal('abcdefghi', last_response.body)
|
156
|
+
assert_requested :get, full_url(@wwwhisper.auth_query(path))
|
157
|
+
end
|
158
|
+
|
135
159
|
def test_auth_query_not_sent_for_login_request
|
136
160
|
path = '/wwwhisper/auth/api/login'
|
137
161
|
stub_request(:get, full_url(path)).
|
@@ -146,7 +170,7 @@ class TestWWWhisper < Test::Unit::TestCase
|
|
146
170
|
path = '/foo/bar'
|
147
171
|
stub_request(:get, full_url(@wwwhisper.auth_query(path))).
|
148
172
|
with(:headers => {'Cookie' => /wwwhisper_auth.+wwwhisper_csrf.+/}).
|
149
|
-
to_return(
|
173
|
+
to_return(granted())
|
150
174
|
|
151
175
|
get(path, {},
|
152
176
|
{'HTTP_COOKIE' => 'wwwhisper_auth=xyz; wwwhisper_csrf_token=abc'})
|
@@ -157,7 +181,7 @@ class TestWWWhisper < Test::Unit::TestCase
|
|
157
181
|
|
158
182
|
def assert_path_normalized(normalized, requested, script_name=nil)
|
159
183
|
stub_request(:get, full_url(@wwwhisper.auth_query(normalized))).
|
160
|
-
to_return(
|
184
|
+
to_return(granted())
|
161
185
|
|
162
186
|
env = script_name ? { 'SCRIPT_NAME' => script_name } : {}
|
163
187
|
get(requested, {}, env)
|
@@ -179,12 +203,8 @@ class TestWWWhisper < Test::Unit::TestCase
|
|
179
203
|
assert_path_normalized '/', '/./././'
|
180
204
|
assert_path_normalized '/bar', '/foo/./bar/../../bar'
|
181
205
|
assert_path_normalized '/foo/', '/foo/bar/..'
|
182
|
-
|
183
|
-
|
184
|
-
# but this is not a big issue, because wwwhisper rejects such
|
185
|
-
# paths.
|
186
|
-
assert_path_normalized '/foo//', '/foo//'
|
187
|
-
assert_path_normalized '//', '/./././/'
|
206
|
+
assert_path_normalized '/foo/', '/foo/'
|
207
|
+
assert_path_normalized '/', '/./././/'
|
188
208
|
end
|
189
209
|
|
190
210
|
def test_path_normalization_with_script_name
|
@@ -194,16 +214,14 @@ class TestWWWhisper < Test::Unit::TestCase
|
|
194
214
|
assert_path_normalized '/baz/bar/hello', '/bar/hello', '/foo/../baz'
|
195
215
|
|
196
216
|
assert_path_normalized '/foo/baz/bar/hello', 'bar/hello', '/foo/./baz'
|
197
|
-
|
198
|
-
|
199
|
-
assert_path_normalized '/foo//', '/', '/foo/'
|
200
|
-
assert_path_normalized '//bar/hello', '/bar/hello', '/foo/..'
|
217
|
+
assert_path_normalized '/foo/', '/', '/foo/'
|
218
|
+
assert_path_normalized '/bar/hello', '/bar/hello', '/foo/..'
|
201
219
|
end
|
202
220
|
|
203
221
|
def test_admin_request
|
204
222
|
path = '/wwwhisper/admin/api/users/xyz'
|
205
223
|
stub_request(:get, full_url(@wwwhisper.auth_query(path))).
|
206
|
-
to_return(
|
224
|
+
to_return(granted())
|
207
225
|
stub_request(:delete, full_url(path)).
|
208
226
|
# Test that a header with multiple '-' is correctly passed
|
209
227
|
with(:headers => {'X-Requested-With' => 'XMLHttpRequest'}).
|
@@ -232,7 +250,7 @@ class TestWWWhisper < Test::Unit::TestCase
|
|
232
250
|
# assets server.
|
233
251
|
stub_request(:get, full_url(@wwwhisper.auth_query(path))).
|
234
252
|
with(:headers => {'Site-Url' => "#{SITE_PROTO}://#{SITE_HOST}"}).
|
235
|
-
to_return(
|
253
|
+
to_return(granted())
|
236
254
|
stub_request(:get, full_assets_url(path)).
|
237
255
|
with { |request| request.headers['Site-Url'] == nil}.
|
238
256
|
to_return(:status => 200, :body => 'Admin page', :headers => {})
|
@@ -274,7 +292,7 @@ class TestWWWhisper < Test::Unit::TestCase
|
|
274
292
|
def test_redirects
|
275
293
|
path = '/wwwhisper/admin/index.html'
|
276
294
|
stub_request(:get, full_url(@wwwhisper.auth_query(path))).
|
277
|
-
to_return(
|
295
|
+
to_return(granted())
|
278
296
|
stub_request(:get, full_assets_url(path)).
|
279
297
|
to_return(:status => 303, :body => 'Admin page moved',
|
280
298
|
:headers => {'Location' => 'https://new.location/foo/bar'})
|
@@ -289,4 +307,28 @@ class TestWWWhisper < Test::Unit::TestCase
|
|
289
307
|
assert_requested :get, full_assets_url(path)
|
290
308
|
end
|
291
309
|
|
310
|
+
def test_disable_wwwhisper
|
311
|
+
ENV.delete('WWWHISPER_URL')
|
312
|
+
ENV['WWWHISPER_DISABLE'] = "1"
|
313
|
+
# Configure MockBackend to make sure REMOTE_USER is not set.
|
314
|
+
@wwwhisper = Rack::WWWhisper.new(MockBackend.new(nil))
|
315
|
+
|
316
|
+
path = '/foo/bar'
|
317
|
+
get path
|
318
|
+
assert last_response.ok?
|
319
|
+
assert_equal 'Hello World', last_response.body
|
320
|
+
assert_nil last_response['User']
|
321
|
+
end
|
322
|
+
|
323
|
+
def test_chunked_encoding_from_wwwhisper_removed
|
324
|
+
path = '/foo/bar'
|
325
|
+
stub_request(:get, full_url(@wwwhisper.auth_query(path))).
|
326
|
+
to_return(:status => 401, :body => 'Login required',
|
327
|
+
:headers => {'Transfer-Encoding' => 'chunked'})
|
328
|
+
get path
|
329
|
+
assert_equal 401, last_response.status
|
330
|
+
assert_nil last_response['Transfer-Encoding']
|
331
|
+
assert_not_nil last_response['Content-Length']
|
332
|
+
end
|
333
|
+
|
292
334
|
end
|
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.0.3.pre
|
5
5
|
prerelease: 6
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -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: 1268452761752086879
|
135
135
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
136
136
|
none: false
|
137
137
|
requirements:
|