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.
- data/lib/rack/wwwhisper.rb +119 -112
- data/test/test_wwwhisper.rb +27 -18
- metadata +5 -2
data/lib/rack/wwwhisper.rb
CHANGED
@@ -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
|
80
|
-
|
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
|
-
|
84
|
-
|
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
|
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
|
-
|
131
|
-
copy_headers(config[:forwarded_headers], env,
|
132
|
-
|
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
|
-
|
135
|
-
|
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,
|
177
|
+
def copy_headers(headers_names, env, sub_req)
|
143
178
|
headers_names.each do |header|
|
144
179
|
key = "HTTP_#{header.upcase}".gsub(/-/, '_')
|
145
|
-
|
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(
|
151
|
-
if
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
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
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
location
|
165
|
-
|
166
|
-
|
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
|
-
|
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
|
209
|
+
def sub_response_to_rack(rack_req, sub_resp)
|
200
210
|
[
|
201
|
-
|
202
|
-
|
203
|
-
[(
|
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(
|
217
|
+
def wwwhisper_auth_request(req)
|
208
218
|
config = @request_config[:auth]
|
209
|
-
|
210
|
-
|
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
|
-
|
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
|
-
|
220
|
-
|
221
|
-
|
222
|
-
|
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
|
226
|
-
|
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
|
-
|
229
|
-
|
230
|
-
|
231
|
-
|
232
|
-
|
233
|
-
|
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
|
-
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
|
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
|
254
|
-
|
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
|
data/test/test_wwwhisper.rb
CHANGED
@@ -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
|
-
|
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 = '/
|
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 =>
|
227
|
-
stub_request(:get, full_assets_url(
|
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 => '
|
238
|
+
to_return(:status => 200, :body => 'Admin page', :headers => {})
|
230
239
|
|
231
240
|
get path
|
232
|
-
assert
|
233
|
-
assert_equal
|
234
|
-
assert_equal '
|
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(
|
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.
|
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-
|
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:
|