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.
- 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:
|