rack-wwwhisper 1.0.1.pre → 1.0.2.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.
Files changed (3) hide show
  1. data/lib/rack/wwwhisper.rb +119 -112
  2. data/test/test_wwwhisper.rb +27 -18
  3. metadata +5 -2
@@ -33,10 +33,12 @@ class WWWhisper
33
33
 
34
34
  @wwwhisper_iframe = ENV['WWWHISPER_IFRAME'] ||
35
35
  sprintf(@@DEFAULT_IFRAME, wwwhisper_path('auth/overlay.html'))
36
+ @wwwhisper_iframe_bytesize = Rack::Utils::bytesize(@wwwhisper_iframe)
36
37
 
37
38
  @request_config = {
39
+ # TODO: probably now auth can be removed.
38
40
  :auth => {
39
- :forwarded_headers => ['Cookie'],
41
+ :forwarded_headers => ['Accept', 'Accept-Language', 'Cookie'],
40
42
  :http => wwwhisper_http,
41
43
  :uri => wwwhisper_uri,
42
44
  :send_site_url => true,
@@ -76,12 +78,45 @@ class WWWhisper
76
78
  wwwhisper_path "auth/api/is-authorized/?path=#{queried_path}"
77
79
  end
78
80
 
79
- def auth_login_path()
80
- wwwhisper_path 'auth/login.html'
81
+ def call(env)
82
+ req = Rack::Request.new(env)
83
+
84
+ normalize_path req
85
+
86
+ if req.path =~ %r{^#{@@WWWHISPER_PREFIX}auth}
87
+ # Requests to /@@WWWHISPER_PREFIX/auth/ should not be authorized,
88
+ # every visitor can access login pages.
89
+ return dispatch(req)
90
+ end
91
+
92
+ debug req, "sending auth request for #{req.path}"
93
+ auth_resp = wwwhisper_auth_request(req)
94
+
95
+ if auth_resp.code == '200'
96
+ debug req, 'access granted'
97
+ status, headers, body = dispatch(req)
98
+ if should_inject_iframe(status, headers)
99
+ body = inject_iframe(headers, body)
100
+ end
101
+ [status, headers, body]
102
+ else
103
+ debug req, {
104
+ '401' => 'user not authenticated',
105
+ '403' => 'access_denied',
106
+ }[auth_resp.code] || 'auth request failed'
107
+ sub_response_to_rack(req, auth_resp)
108
+ end
81
109
  end
82
110
 
83
- def auth_denied_path()
84
- wwwhisper_path 'auth/not_authorized.html'
111
+ private
112
+
113
+ def debug(req, message)
114
+ req.logger.debug "wwwhisper #{message}" if req.logger
115
+ end
116
+
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)
85
120
  end
86
121
 
87
122
  def parse_uri(uri)
@@ -92,15 +127,6 @@ class WWWhisper
92
127
  parsed_uri
93
128
  end
94
129
 
95
- def http_init(connection_id)
96
- http = Net::HTTP::Persistent.new(connection_id)
97
- store = OpenSSL::X509::Store.new()
98
- store.set_default_paths
99
- http.cert_store = store
100
- http.verify_mode = OpenSSL::SSL::VERIFY_PEER
101
- return http
102
- end
103
-
104
130
  def default_port(proto)
105
131
  {
106
132
  'http' => 80,
@@ -125,143 +151,124 @@ class WWWhisper
125
151
  "#{proto}://#{host}#{port_str}"
126
152
  end
127
153
 
128
- def request_init(config, env, method, path)
154
+ def http_init(connection_id)
155
+ http = Net::HTTP::Persistent.new(connection_id)
156
+ store = OpenSSL::X509::Store.new()
157
+ store.set_default_paths
158
+ http.cert_store = store
159
+ http.verify_mode = OpenSSL::SSL::VERIFY_PEER
160
+ return http
161
+ end
162
+
163
+ def sub_request_init(config, rack_req, method, path)
129
164
  path = @aliases[path] || path
130
- request = Net::HTTP.const_get(method).new(path)
131
- copy_headers(config[:forwarded_headers], env, request)
132
- request['Site-Url'] = site_url(env) if config[:send_site_url]
165
+ sub_req = Net::HTTP.const_get(method).new(path)
166
+ 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]
133
168
  uri = config[:uri]
134
- request.basic_auth(uri.user, uri.password) if uri.user and uri.password
135
- request
169
+ sub_req.basic_auth(uri.user, uri.password) if uri.user and uri.password
170
+ sub_req
136
171
  end
137
172
 
138
173
  def has_value(dict, key)
139
174
  dict[key] != nil and !dict[key].empty?
140
175
  end
141
176
 
142
- def copy_headers(headers_names, env, request)
177
+ def copy_headers(headers_names, env, sub_req)
143
178
  headers_names.each do |header|
144
179
  key = "HTTP_#{header.upcase}".gsub(/-/, '_')
145
- request[header] = env[key] if has_value(env, key)
146
- #puts "Sending header #{header} #{request[header]} #{key} #{env[key]}"
180
+ sub_req[header] = env[key] if has_value(env, key)
147
181
  end
148
182
  end
149
183
 
150
- def copy_body(src_request, dst_request)
151
- if dst_request.request_body_permitted? and src_request.body
152
- dst_request.body_stream = src_request.body
153
- dst_request.content_length = src_request.content_length
154
- dst_request.content_type =
155
- src_request.content_type if src_request.content_type
184
+ def copy_body(rack_req, sub_req)
185
+ if sub_req.request_body_permitted? and rack_req.body and
186
+ (rack_req.content_length or
187
+ rack_req.env['HTTP_TRANSFER_ENCODING'] == 'chunked')
188
+ sub_req.body_stream = rack_req.body
189
+ sub_req.content_length =
190
+ rack_req.content_length if rack_req.content_length
191
+ sub_req.content_type = rack_req.content_type if rack_req.content_type
156
192
  end
157
193
  end
158
194
 
159
- def extract_headers(env, response)
160
- headers = Rack::Utils::HeaderHash.new()
161
- response.each_capitalized do |k,v|
162
- #puts "Header #{k} VAL #{v}"
163
- if k.to_s =~ /location/i
164
- location = Addressable::URI.parse(v)
165
- location.scheme, location.host, location.port = proto_host_port(env)
166
- v = location.to_s
195
+ def sub_response_headers_to_rack(rack_req, sub_resp)
196
+ rack_headers = Rack::Utils::HeaderHash.new()
197
+ sub_resp.each_capitalized do |header, value|
198
+ if header == 'Location'
199
+ location = Addressable::URI.parse(value)
200
+ location.scheme, location.host, location.port =
201
+ proto_host_port(rack_req.env)
202
+ value = location.to_s
167
203
  end
168
- # Transfer encoding and content-length are set correctly by Rack.
169
- # TODO: what is transfer encoding?
170
- headers[k] = v unless k.to_s =~ /transfer-encoding|content-length/i
171
- end
172
- return headers
173
- end
174
-
175
- def dispatch(env)
176
- orig_request = Rack::Request.new(env)
177
- if orig_request.path =~ %r{^#{@@WWWHISPER_PREFIX}}
178
- debug orig_request, "passing request to wwwhisper service"
179
-
180
- config =
181
- if orig_request.path =~ %r{^#{@@WWWHISPER_PREFIX}(auth|admin)/api/}
182
- @request_config[:api]
183
- else
184
- @request_config[:assets]
185
- end
186
-
187
- method = orig_request.request_method.capitalize
188
- request = request_init(config, env, method, orig_request.fullpath)
189
- copy_body(orig_request, request)
190
-
191
- response = config[:http].request(config[:uri], request)
192
- net_http_response_to_rack(env, response)
193
- else
194
- debug orig_request, "passing request to Rack stack"
195
- @app.call(env)
204
+ rack_headers[header] = value
196
205
  end
206
+ return rack_headers
197
207
  end
198
208
 
199
- def net_http_response_to_rack(env, response)
209
+ def sub_response_to_rack(rack_req, sub_resp)
200
210
  [
201
- response.code.to_i,
202
- extract_headers(env, response),
203
- [(response.read_body() or '')]
211
+ sub_resp.code.to_i,
212
+ sub_response_headers_to_rack(rack_req, sub_resp),
213
+ [(sub_resp.read_body() or '')]
204
214
  ]
205
215
  end
206
216
 
207
- def wwwhisper_auth_request(env, req)
217
+ def wwwhisper_auth_request(req)
208
218
  config = @request_config[:auth]
209
- auth_request = request_init(config, env, 'Get', auth_query(req.path))
210
- auth_response = config[:http].request(config[:uri], auth_request)
211
- net_http_response_to_rack(env, auth_response)
219
+ auth_req = sub_request_init(config, req, 'Get', auth_query(req.path))
220
+ config[:http].request(config[:uri], auth_req)
212
221
  end
213
222
 
214
223
  def should_inject_iframe(status, headers)
215
- status == 200 and headers['Content-Type'] =~ /text\/html/i
224
+ # Do not attempt to inject iframe if result is already chunked,
225
+ # compressed or checksummed.
226
+ (status == 200 and
227
+ headers['Content-Type'] =~ /text\/html/i and
228
+ not headers['Transfer-Encoding'] and
229
+ not headers['Content-Range'] and
230
+ not headers['Content-Encoding'] and
231
+ not headers['Content-MD5']
232
+ )
216
233
  end
217
234
 
218
235
  def inject_iframe(headers, body)
219
- # If Content-Length is missing, Rack sets correct one.
220
- headers.delete('Content-Length')
221
- #todo: iterate?
222
- body[0] = body[0].sub(/<\/body>/, "#{@wwwhisper_iframe}</body>")
236
+ total = []
237
+ body.each { |part|
238
+ total << part
239
+ }
240
+ total = total.join()
241
+ if idx = total.rindex('</body>')
242
+ total.insert(idx, @wwwhisper_iframe)
243
+ headers['Content-Length'] &&= (headers['Content-Length'].to_i +
244
+ @wwwhisper_iframe_bytesize).to_s
245
+ end
246
+ [total]
223
247
  end
224
248
 
225
- def call(env)
226
- req = Rack::Request.new(env)
249
+ def dispatch(orig_req)
250
+ if orig_req.path =~ %r{^#{@@WWWHISPER_PREFIX}}
251
+ debug orig_req, "passing request to wwwhisper service #{orig_req.path}"
227
252
 
228
- req.path_info = Addressable::URI.normalize_path(req.path)
229
- if req.path =~ %r{^#{@@WWWHISPER_PREFIX}auth}
230
- # Requests to /@@WWWHISPER_PREFIX/auth/ should not be authorized,
231
- # every visitor can access login pages.
232
- return dispatch(env)
233
- end
234
- debug req, "sending auth request for #{req.path}"
235
- auth_status, auth_headers, auth_body = wwwhisper_auth_request(env, req)
253
+ config =
254
+ if orig_req.path =~ %r{^#{@@WWWHISPER_PREFIX}(auth|admin)/api/}
255
+ @request_config[:api]
256
+ else
257
+ @request_config[:assets]
258
+ end
236
259
 
237
- case auth_status
238
- when 200
239
- debug req, "access granted"
240
- status, headers, body = dispatch(env)
241
- inject_iframe(headers, body) if should_inject_iframe(status, headers)
242
- [status, headers, body]
243
- when 401, 403
244
- login_needed = (auth_status == 401)
245
- debug req, login_needed ? "user not authenticated" : "access_denied"
246
- req.path_info = login_needed ? auth_login_path() : auth_denied_path()
247
- status, headers, body = dispatch(env)
248
- auth_headers['Content-Type'] = headers['Content-Type']
249
- # TODO: only here?
250
- auth_headers['Content-Encoding'] = headers['Content-Encoding']
251
- [auth_status, auth_headers, body]
260
+ method = orig_req.request_method.capitalize
261
+ sub_req = sub_request_init(config, orig_req, method, orig_req.fullpath)
262
+ copy_body(orig_req, sub_req)
263
+
264
+ sub_resp = config[:http].request(config[:uri], sub_req)
265
+ sub_response_to_rack(orig_req, sub_resp)
252
266
  else
253
- debug req, "auth request failed"
254
- [auth_status, auth_headers, auth_body]
267
+ debug orig_req, 'passing request to Rack stack'
268
+ @app.call(orig_req.env)
255
269
  end
256
270
  end
257
271
 
258
- # TODO: more private
259
- private
260
-
261
- def debug(req, message)
262
- req.logger.debug "wwwhisper #{message}" if req.logger
263
- end
264
-
265
272
  end
266
273
 
267
274
  end
@@ -83,31 +83,25 @@ class TestWWWhisper < Test::Unit::TestCase
83
83
  def test_login_required
84
84
  path = '/foo/bar'
85
85
  stub_request(:get, full_url(@wwwhisper.auth_query(path))).
86
- to_return(:status => 401, :body => '', :headers => {})
87
- stub_request(:get, full_assets_url(@wwwhisper.auth_login_path())).
88
- to_return(:status => 200, :body => 'Login required', :headers => {})
86
+ to_return(:status => 401, :body => 'Login required', :headers => {})
89
87
 
90
88
  get path
91
89
  assert !last_response.ok?
92
90
  assert_equal 401, last_response.status
93
91
  assert_equal 'Login required', last_response.body
94
92
  assert_requested :get, full_url(@wwwhisper.auth_query(path))
95
- assert_requested :get, full_assets_url(@wwwhisper.auth_login_path())
96
93
  end
97
94
 
98
95
  def test_request_denied
99
96
  path = '/foo/bar'
100
97
  stub_request(:get, full_url(@wwwhisper.auth_query(path))).
101
- to_return(:status => 403, :body => '', :headers => {})
102
- stub_request(:get, full_assets_url(@wwwhisper.auth_denied_path())).
103
- to_return(:status => 200, :body => 'Not authorized', :headers => {})
98
+ to_return(:status => 403, :body => 'Not authorized', :headers => {})
104
99
 
105
100
  get path
106
101
  assert !last_response.ok?
107
102
  assert_equal 403, last_response.status
108
103
  assert_equal 'Not authorized', last_response.body
109
104
  assert_requested :get, full_url(@wwwhisper.auth_query(path))
110
- assert_requested :get, full_assets_url(@wwwhisper.auth_denied_path())
111
105
  end
112
106
 
113
107
  def test_iframe_injected_to_html_response
@@ -161,11 +155,12 @@ class TestWWWhisper < Test::Unit::TestCase
161
155
  assert_requested :get, full_url(@wwwhisper.auth_query(path))
162
156
  end
163
157
 
164
- def assert_path_normalized(normalized, requested)
158
+ def assert_path_normalized(normalized, requested, script_name=nil)
165
159
  stub_request(:get, full_url(@wwwhisper.auth_query(normalized))).
166
160
  to_return(:status => 200, :body => '', :headers => {})
167
161
 
168
- get requested
162
+ env = script_name ? { 'SCRIPT_NAME' => script_name } : {}
163
+ get(requested, {}, env)
169
164
  assert last_response.ok?
170
165
  assert_equal 'Hello World', last_response.body
171
166
  assert_requested :get, full_url(@wwwhisper.auth_query(normalized))
@@ -192,6 +187,19 @@ class TestWWWhisper < Test::Unit::TestCase
192
187
  assert_path_normalized '//', '/./././/'
193
188
  end
194
189
 
190
+ def test_path_normalization_with_script_name
191
+ assert_path_normalized '/foo/', '/', '/foo'
192
+ assert_path_normalized '/foo/bar/hello', '/bar/hello', '/foo'
193
+
194
+ assert_path_normalized '/baz/bar/hello', '/bar/hello', '/foo/../baz'
195
+
196
+ 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/..'
201
+ end
202
+
195
203
  def test_admin_request
196
204
  path = '/wwwhisper/admin/api/users/xyz'
197
205
  stub_request(:get, full_url(@wwwhisper.auth_query(path))).
@@ -218,22 +226,23 @@ class TestWWWhisper < Test::Unit::TestCase
218
226
  end
219
227
 
220
228
  def test_site_url
221
- path = '/foo/bar'
229
+ path = '/wwwhisper/admin/index.html'
230
+
222
231
  # Site-Url header should be sent to wwwhisper backend but not to
223
232
  # assets server.
224
233
  stub_request(:get, full_url(@wwwhisper.auth_query(path))).
225
234
  with(:headers => {'Site-Url' => "#{SITE_PROTO}://#{SITE_HOST}"}).
226
- to_return(:status => 401, :body => '', :headers => {})
227
- stub_request(:get, full_assets_url(@wwwhisper.auth_login_path())).
235
+ to_return(:status => 200, :body => '', :headers => {})
236
+ stub_request(:get, full_assets_url(path)).
228
237
  with { |request| request.headers['Site-Url'] == nil}.
229
- to_return(:status => 200, :body => 'Login required', :headers => {})
238
+ to_return(:status => 200, :body => 'Admin page', :headers => {})
230
239
 
231
240
  get path
232
- assert !last_response.ok?
233
- assert_equal 401, last_response.status
234
- assert_equal 'Login required', last_response.body
241
+ assert last_response.ok?
242
+ assert_equal 200, last_response.status
243
+ assert_equal 'Admin page', last_response.body
235
244
  assert_requested :get, full_url(@wwwhisper.auth_query(path))
236
- assert_requested :get, full_assets_url(@wwwhisper.auth_login_path())
245
+ assert_requested :get, full_assets_url(path)
237
246
  end
238
247
 
239
248
  def test_site_url_with_non_default_port
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.1.pre
4
+ version: 1.0.2.pre
5
5
  prerelease: 6
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-01-09 00:00:00.000000000 Z
12
+ date: 2013-01-11 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: rack
@@ -129,6 +129,9 @@ required_ruby_version: !ruby/object:Gem::Requirement
129
129
  - - ! '>='
130
130
  - !ruby/object:Gem::Version
131
131
  version: '0'
132
+ segments:
133
+ - 0
134
+ hash: 3933889389209313993
132
135
  required_rubygems_version: !ruby/object:Gem::Requirement
133
136
  none: false
134
137
  requirements: