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