rack-wwwhisper 1.0.2.pre → 1.0.3.pre
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 +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:
|