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.
Files changed (3) hide show
  1. data/lib/rack/wwwhisper.rb +69 -42
  2. data/test/test_wwwhisper.rb +62 -20
  3. metadata +2 -2
@@ -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 = Rack::Request.new(env)
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
- def debug(req, message)
114
- req.logger.debug "wwwhisper #{message}" if req.logger
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 normalize_path(req)
118
- req.script_name = Addressable::URI.normalize_path(req.script_name)
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'] = site_url(rack_req.env) if config[:send_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
- proto_host_port(rack_req.env)
223
+ rack_req.scheme, rack_req.host, rack_req.port
202
224
  value = location.to_s
203
225
  end
204
- rack_headers[header] = value
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
- sub_resp.code.to_i,
212
- sub_response_headers_to_rack(rack_req, sub_resp),
213
- [(sub_resp.read_body() or '')]
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
@@ -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(:status => 200, :body => '', :headers => {})
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(:status => 200, :body => '', :headers => {})
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(:status => 200, :body => '', :headers => {})
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(:status => 200, :body => '', :headers => {})
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(:status => 200, :body => '', :headers => {})
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
- # These two do not seem to be handled correctly and consistency,
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
- # Not handled too well (see comment above).
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(:status => 200, :body => '', :headers => {})
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(:status => 200, :body => '', :headers => {})
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(:status => 200, :body => '', :headers => {})
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.2.pre
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: 3933889389209313993
134
+ hash: 1268452761752086879
135
135
  required_rubygems_version: !ruby/object:Gem::Requirement
136
136
  none: false
137
137
  requirements: