rack-wwwhisper 1.0.0.pre
Sign up to get free protection for your applications and to get access to all the features.
- data/Rakefile +13 -0
- data/lib/rack/wwwhisper.rb +267 -0
- data/test/test_wwwhisper.rb +283 -0
- metadata +145 -0
data/Rakefile
ADDED
@@ -0,0 +1,267 @@
|
|
1
|
+
# Rack middleware that uses wwwhisper service to authorize visitors.
|
2
|
+
# Copyright (C) 2013 Jan Wrobel <wrr@mixedbit.org>
|
3
|
+
#
|
4
|
+
# This program is freely distributable under the terms of the
|
5
|
+
# Simplified BSD License. See COPYING.
|
6
|
+
|
7
|
+
require 'addressable/uri'
|
8
|
+
require 'net/http/persistent'
|
9
|
+
require 'rack/utils'
|
10
|
+
|
11
|
+
module Rack
|
12
|
+
|
13
|
+
class WWWhisper
|
14
|
+
@@DEFAULT_ASSETS_URL = 'https://c693db817dca7e162673-39ba3573e09a1fa9bea151a745461b70.ssl.cf1.rackcdn.com'
|
15
|
+
@@WWWHISPER_PREFIX = '/wwwhisper/'
|
16
|
+
#@@AUTH_COOKIES_PREFIX = 'wwwhisper'
|
17
|
+
|
18
|
+
@@DEFAULT_IFRAME = \
|
19
|
+
%Q[<iframe id="wwwhisper-iframe" src="%s"
|
20
|
+
width="340" height="29" frameborder="0" scrolling="no"
|
21
|
+
style="position:fixed; overflow:hidden; border:0px; bottom:0px;
|
22
|
+
right:0px; z-index:11235; background-color:transparent;"> </iframe>
|
23
|
+
]
|
24
|
+
|
25
|
+
def initialize(app)
|
26
|
+
@app = app
|
27
|
+
if not ENV['WWWHISPER_URL']
|
28
|
+
raise StandardError, 'WWWHISPER_URL environment variable not set'
|
29
|
+
end
|
30
|
+
|
31
|
+
wwwhisper_http = http_init('wwwhisper')
|
32
|
+
wwwhisper_uri = parse_uri(ENV['WWWHISPER_URL'])
|
33
|
+
|
34
|
+
@wwwhisper_iframe = ENV['WWWHISPER_IFRAME'] ||
|
35
|
+
sprintf(@@DEFAULT_IFRAME, wwwhisper_path('auth/overlay.html'))
|
36
|
+
|
37
|
+
@request_config = {
|
38
|
+
:auth => {
|
39
|
+
:forwarded_headers => ['Cookie'],
|
40
|
+
:http => wwwhisper_http,
|
41
|
+
:uri => wwwhisper_uri,
|
42
|
+
:send_site_url => true,
|
43
|
+
},
|
44
|
+
:api => {
|
45
|
+
:forwarded_headers => ['Accept', 'Accept-Language', 'Cookie',
|
46
|
+
'X-CSRFToken', 'X-Requested-With'],
|
47
|
+
:http => wwwhisper_http,
|
48
|
+
:uri => wwwhisper_uri,
|
49
|
+
:send_site_url => true,
|
50
|
+
},
|
51
|
+
:assets => {
|
52
|
+
# Don't pass Accept-Encoding to get uncompressed response (so
|
53
|
+
# iframe can be inserted to it).
|
54
|
+
:forwarded_headers => ['Accept', 'Accept-Language'],
|
55
|
+
:http => http_init('wwwhisper-assets'),
|
56
|
+
:uri => parse_uri(ENV['WWWHISPER_ASSETS_URL'] || @@DEFAULT_ASSETS_URL),
|
57
|
+
:send_site_url => false,
|
58
|
+
},
|
59
|
+
}
|
60
|
+
|
61
|
+
@aliases = {}
|
62
|
+
{
|
63
|
+
'auth/login' => 'auth/login.html',
|
64
|
+
'auth/logout' => 'auth/logout.html',
|
65
|
+
'admin/' => 'admin/index.html',
|
66
|
+
}.each do |k, v|
|
67
|
+
@aliases[wwwhisper_path(k)] = wwwhisper_path(v)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
def wwwhisper_path(suffix)
|
72
|
+
"#{@@WWWHISPER_PREFIX}#{suffix}"
|
73
|
+
end
|
74
|
+
|
75
|
+
def auth_query(queried_path)
|
76
|
+
wwwhisper_path "auth/api/is-authorized/?path=#{queried_path}"
|
77
|
+
end
|
78
|
+
|
79
|
+
def auth_login_path()
|
80
|
+
wwwhisper_path 'auth/login.html'
|
81
|
+
end
|
82
|
+
|
83
|
+
def auth_denied_path()
|
84
|
+
wwwhisper_path 'auth/not_authorized.html'
|
85
|
+
end
|
86
|
+
|
87
|
+
def parse_uri(uri)
|
88
|
+
parsed_uri = Addressable::URI.parse(uri)
|
89
|
+
# If port is not specified, net/http/persistent uses port 80 for
|
90
|
+
# https connections which is counterintuitive.
|
91
|
+
parsed_uri.port ||= parsed_uri.default_port
|
92
|
+
parsed_uri
|
93
|
+
end
|
94
|
+
|
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
|
+
def default_port(proto)
|
105
|
+
{
|
106
|
+
'http' => 80,
|
107
|
+
'https' => 443,
|
108
|
+
}[proto]
|
109
|
+
end
|
110
|
+
|
111
|
+
def proto_host_port(env)
|
112
|
+
proto = env['HTTP_X_FORWARDED_PROTO'] || 'http'
|
113
|
+
return proto,
|
114
|
+
env['HTTP_HOST'],
|
115
|
+
env['HTTP_X_FORWARDED_PORT'] || default_port(proto)
|
116
|
+
end
|
117
|
+
|
118
|
+
def site_url(env)
|
119
|
+
proto, host, port = proto_host_port(env)
|
120
|
+
port_str = if port != default_port(proto)
|
121
|
+
":#{port}"
|
122
|
+
else
|
123
|
+
''
|
124
|
+
end
|
125
|
+
"#{proto}://#{host}#{port_str}"
|
126
|
+
end
|
127
|
+
|
128
|
+
def request_init(config, env, method, path)
|
129
|
+
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]
|
133
|
+
uri = config[:uri]
|
134
|
+
request.basic_auth(uri.user, uri.password) if uri.user and uri.password
|
135
|
+
request
|
136
|
+
end
|
137
|
+
|
138
|
+
def has_value(dict, key)
|
139
|
+
dict[key] != nil and !dict[key].empty?
|
140
|
+
end
|
141
|
+
|
142
|
+
def copy_headers(headers_names, env, request)
|
143
|
+
headers_names.each do |header|
|
144
|
+
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]}"
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
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
|
156
|
+
end
|
157
|
+
end
|
158
|
+
|
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
|
167
|
+
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)
|
196
|
+
end
|
197
|
+
end
|
198
|
+
|
199
|
+
def net_http_response_to_rack(env, response)
|
200
|
+
[
|
201
|
+
response.code.to_i,
|
202
|
+
extract_headers(env, response),
|
203
|
+
[(response.read_body() or '')]
|
204
|
+
]
|
205
|
+
end
|
206
|
+
|
207
|
+
def wwwhisper_auth_request(env, req)
|
208
|
+
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)
|
212
|
+
end
|
213
|
+
|
214
|
+
def should_inject_iframe(status, headers)
|
215
|
+
status == 200 and headers['Content-Type'] =~ /text\/html/i
|
216
|
+
end
|
217
|
+
|
218
|
+
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>")
|
223
|
+
end
|
224
|
+
|
225
|
+
def call(env)
|
226
|
+
req = Rack::Request.new(env)
|
227
|
+
|
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)
|
236
|
+
|
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]
|
252
|
+
else
|
253
|
+
debug req, "auth request failed"
|
254
|
+
[auth_status, auth_headers, auth_body]
|
255
|
+
end
|
256
|
+
end
|
257
|
+
|
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
|
+
end
|
266
|
+
|
267
|
+
end
|
@@ -0,0 +1,283 @@
|
|
1
|
+
# Rack middleware that uses wwwhisper service to authorize visitors.
|
2
|
+
# Copyright (C) 2013 Jan Wrobel <wrr@mixedbit.org>
|
3
|
+
#
|
4
|
+
# This program is freely distributable under the terms of the
|
5
|
+
# Simplified BSD License. See COPYING.
|
6
|
+
|
7
|
+
require 'rack/test'
|
8
|
+
require 'test/unit'
|
9
|
+
require 'webmock/test_unit'
|
10
|
+
require 'rack/wwwhisper'
|
11
|
+
|
12
|
+
ENV['RACK_ENV'] = 'test'
|
13
|
+
|
14
|
+
class MockBackend
|
15
|
+
attr_accessor :response
|
16
|
+
|
17
|
+
def initialize()
|
18
|
+
@response = [200, {'Content-Type' => 'text/html'}, ['Hello World']]
|
19
|
+
end
|
20
|
+
|
21
|
+
def call(env)
|
22
|
+
@response
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
class TestWWWhisper < Test::Unit::TestCase
|
27
|
+
include Rack::Test::Methods
|
28
|
+
WWWHISPER_URL = 'https://example.com'
|
29
|
+
WWWHISPER_ASSETS_URL = 'https://assets.example.com'
|
30
|
+
SITE_PROTO = 'https'
|
31
|
+
SITE_HOST = 'bar.io'
|
32
|
+
SITE_PORT = 443
|
33
|
+
|
34
|
+
def setup()
|
35
|
+
@backend = MockBackend.new()
|
36
|
+
ENV['WWWHISPER_URL'] = WWWHISPER_URL
|
37
|
+
ENV['WWWHISPER_ASSETS_URL'] = WWWHISPER_ASSETS_URL
|
38
|
+
@wwwhisper = Rack::WWWhisper.new(@backend)
|
39
|
+
end
|
40
|
+
|
41
|
+
def app
|
42
|
+
@wwwhisper
|
43
|
+
end
|
44
|
+
|
45
|
+
def full_url(path)
|
46
|
+
"#{WWWHISPER_URL}#{path}"
|
47
|
+
end
|
48
|
+
|
49
|
+
def full_assets_url(path)
|
50
|
+
"#{WWWHISPER_ASSETS_URL}#{path}"
|
51
|
+
end
|
52
|
+
|
53
|
+
def test_wwwhisper_url_required
|
54
|
+
ENV.delete('WWWHISPER_URL')
|
55
|
+
assert_raise(StandardError) {
|
56
|
+
Rack::WWWhisper.new(@backend)
|
57
|
+
}
|
58
|
+
end
|
59
|
+
|
60
|
+
def get path, params={}, rack_env={}
|
61
|
+
rack_env['HTTP_HOST'] ||= SITE_HOST
|
62
|
+
rack_env['HTTP_X_FORWARDED_PROTO'] ||= SITE_PROTO
|
63
|
+
rack_env['HTTP_X_FORWARDED_PORT'] ||= SITE_PORT
|
64
|
+
super path, params, rack_env
|
65
|
+
end
|
66
|
+
|
67
|
+
def test_auth_query_path
|
68
|
+
assert_equal('/wwwhisper/auth/api/is-authorized/?path=/foo/bar',
|
69
|
+
@wwwhisper.auth_query('/foo/bar'))
|
70
|
+
end
|
71
|
+
|
72
|
+
def test_request_allowed
|
73
|
+
path = '/foo/bar'
|
74
|
+
stub_request(:get, full_url(@wwwhisper.auth_query(path))).
|
75
|
+
to_return(:status => 200, :body => '', :headers => {})
|
76
|
+
|
77
|
+
get path
|
78
|
+
assert last_response.ok?
|
79
|
+
assert_equal 'Hello World', last_response.body
|
80
|
+
assert_requested :get, full_url(@wwwhisper.auth_query(path))
|
81
|
+
end
|
82
|
+
|
83
|
+
def test_login_required
|
84
|
+
path = '/foo/bar'
|
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 => {})
|
89
|
+
|
90
|
+
get path
|
91
|
+
assert !last_response.ok?
|
92
|
+
assert_equal 401, last_response.status
|
93
|
+
assert_equal 'Login required', last_response.body
|
94
|
+
assert_requested :get, full_url(@wwwhisper.auth_query(path))
|
95
|
+
assert_requested :get, full_assets_url(@wwwhisper.auth_login_path())
|
96
|
+
end
|
97
|
+
|
98
|
+
def test_request_denied
|
99
|
+
path = '/foo/bar'
|
100
|
+
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 => {})
|
104
|
+
|
105
|
+
get path
|
106
|
+
assert !last_response.ok?
|
107
|
+
assert_equal 403, last_response.status
|
108
|
+
assert_equal 'Not authorized', last_response.body
|
109
|
+
assert_requested :get, full_url(@wwwhisper.auth_query(path))
|
110
|
+
assert_requested :get, full_assets_url(@wwwhisper.auth_denied_path())
|
111
|
+
end
|
112
|
+
|
113
|
+
def test_iframe_injected_to_html_response
|
114
|
+
path = '/foo/bar'
|
115
|
+
stub_request(:get, full_url(@wwwhisper.auth_query(path))).
|
116
|
+
to_return(:status => 200, :body => '', :headers => {})
|
117
|
+
# wwwhisper iframe is injected only when response is a html document
|
118
|
+
# with <body></body>
|
119
|
+
body = '<html><body>Hello World</body></html>'
|
120
|
+
@backend.response= [200, {'Content-Type' => 'text/html'}, [body]]
|
121
|
+
|
122
|
+
get path
|
123
|
+
assert last_response.ok?
|
124
|
+
assert_match(/.*<iframe id="wwwhisper-iframe".*/, last_response.body)
|
125
|
+
assert_requested :get, full_url(@wwwhisper.auth_query(path))
|
126
|
+
end
|
127
|
+
|
128
|
+
def test_iframe_not_injected_to_non_html_response
|
129
|
+
path = '/foo/bar'
|
130
|
+
stub_request(:get, full_url(@wwwhisper.auth_query(path))).
|
131
|
+
to_return(:status => 200, :body => '', :headers => {})
|
132
|
+
body = '<html><body>Hello World</body></html>'
|
133
|
+
@backend.response= [200, {'Content-Type' => 'text/plain'}, [body]]
|
134
|
+
|
135
|
+
get path
|
136
|
+
assert last_response.ok?
|
137
|
+
assert_equal(body, last_response.body)
|
138
|
+
assert_requested :get, full_url(@wwwhisper.auth_query(path))
|
139
|
+
end
|
140
|
+
|
141
|
+
def test_auth_query_not_sent_for_login_request
|
142
|
+
path = '/wwwhisper/auth/api/login'
|
143
|
+
stub_request(:get, full_url(path)).
|
144
|
+
to_return(:status => 200, :body => 'Login', :headers => {})
|
145
|
+
|
146
|
+
get path
|
147
|
+
assert last_response.ok?
|
148
|
+
assert_equal 'Login', last_response.body
|
149
|
+
end
|
150
|
+
|
151
|
+
def test_auth_cookies_passed_to_wwwhisper()
|
152
|
+
path = '/foo/bar'
|
153
|
+
stub_request(:get, full_url(@wwwhisper.auth_query(path))).
|
154
|
+
with(:headers => {'Cookie' => /wwwhisper_auth.+wwwhisper_csrf.+/}).
|
155
|
+
to_return(:status => 200, :body => '', :headers => {})
|
156
|
+
|
157
|
+
get(path, {},
|
158
|
+
{'HTTP_COOKIE' => 'wwwhisper_auth=xyz; wwwhisper_csrf_token=abc'})
|
159
|
+
assert last_response.ok?
|
160
|
+
assert_equal 'Hello World', last_response.body
|
161
|
+
assert_requested :get, full_url(@wwwhisper.auth_query(path))
|
162
|
+
end
|
163
|
+
|
164
|
+
def assert_path_normalized(normalized, requested)
|
165
|
+
stub_request(:get, full_url(@wwwhisper.auth_query(normalized))).
|
166
|
+
to_return(:status => 200, :body => '', :headers => {})
|
167
|
+
|
168
|
+
get requested
|
169
|
+
assert last_response.ok?
|
170
|
+
assert_equal 'Hello World', last_response.body
|
171
|
+
assert_requested :get, full_url(@wwwhisper.auth_query(normalized))
|
172
|
+
WebMock.reset!
|
173
|
+
end
|
174
|
+
|
175
|
+
def test_path_normalization
|
176
|
+
assert_path_normalized '/', '/'
|
177
|
+
assert_path_normalized '/foo/bar', '/foo/bar'
|
178
|
+
assert_path_normalized '/foo/bar/', '/foo/bar/'
|
179
|
+
|
180
|
+
assert_path_normalized '/foo/', '/auth/api/login/../../../foo/'
|
181
|
+
assert_path_normalized '/', '//'
|
182
|
+
assert_path_normalized '/', ''
|
183
|
+
assert_path_normalized '/', '/../'
|
184
|
+
assert_path_normalized '/', '/./././'
|
185
|
+
assert_path_normalized '/bar', '/foo/./bar/../../bar'
|
186
|
+
assert_path_normalized '/foo/', '/foo/bar/..'
|
187
|
+
|
188
|
+
# These two do not seem to be handled correctly and consistency,
|
189
|
+
# but this is not a big issue, because wwwhisper rejects such
|
190
|
+
# paths.
|
191
|
+
assert_path_normalized '/foo//', '/foo//'
|
192
|
+
assert_path_normalized '//', '/./././/'
|
193
|
+
end
|
194
|
+
|
195
|
+
def test_admin_request
|
196
|
+
path = '/wwwhisper/admin/api/users/xyz'
|
197
|
+
stub_request(:get, full_url(@wwwhisper.auth_query(path))).
|
198
|
+
to_return(:status => 200, :body => '', :headers => {})
|
199
|
+
stub_request(:delete, full_url(path)).
|
200
|
+
# Test that a header with multiple '-' is correctly passed
|
201
|
+
with(:headers => {'X-Requested-With' => 'XMLHttpRequest'}).
|
202
|
+
to_return(:status => 200, :body => 'admin page', :headers => {})
|
203
|
+
|
204
|
+
delete(path, {}, {'HTTP_X_REQUESTED_WITH' => 'XMLHttpRequest'})
|
205
|
+
assert last_response.ok?
|
206
|
+
assert_equal 'admin page', last_response.body
|
207
|
+
end
|
208
|
+
|
209
|
+
def test_invalid_auth_request
|
210
|
+
path = '/foo'
|
211
|
+
stub_request(:get, full_url(@wwwhisper.auth_query(path))).
|
212
|
+
to_return(:status => 400, :body => 'invalid request', :headers => {})
|
213
|
+
|
214
|
+
get path
|
215
|
+
assert !last_response.ok?
|
216
|
+
assert_equal 400, last_response.status
|
217
|
+
assert_equal 'invalid request', last_response.body
|
218
|
+
end
|
219
|
+
|
220
|
+
def test_site_url
|
221
|
+
path = '/foo/bar'
|
222
|
+
# Site-Url header should be sent to wwwhisper backend but not to
|
223
|
+
# assets server.
|
224
|
+
stub_request(:get, full_url(@wwwhisper.auth_query(path))).
|
225
|
+
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())).
|
228
|
+
with { |request| request.headers['Site-Url'] == nil}.
|
229
|
+
to_return(:status => 200, :body => 'Login required', :headers => {})
|
230
|
+
|
231
|
+
get path
|
232
|
+
assert !last_response.ok?
|
233
|
+
assert_equal 401, last_response.status
|
234
|
+
assert_equal 'Login required', last_response.body
|
235
|
+
assert_requested :get, full_url(@wwwhisper.auth_query(path))
|
236
|
+
assert_requested :get, full_assets_url(@wwwhisper.auth_login_path())
|
237
|
+
end
|
238
|
+
|
239
|
+
def test_site_url_with_non_default_port
|
240
|
+
path = '/foo/bar'
|
241
|
+
port = 11235
|
242
|
+
# Site-Url header should be sent to wwwhisper backend but not to
|
243
|
+
# assets server.
|
244
|
+
stub_request(:get, full_url(@wwwhisper.auth_query(path))).
|
245
|
+
with(:headers => {'Site-Url' => "#{SITE_PROTO}://#{SITE_HOST}:#{port}"}).
|
246
|
+
to_return(:status => 400, :body => '', :headers => {})
|
247
|
+
|
248
|
+
get(path, {}, {'HTTP_X_FORWARDED_PORT' => port.to_s})
|
249
|
+
assert !last_response.ok?
|
250
|
+
assert_equal 400, last_response.status
|
251
|
+
assert_requested :get, full_url(@wwwhisper.auth_query(path))
|
252
|
+
end
|
253
|
+
|
254
|
+
def test_aliases
|
255
|
+
requested_path = '/wwwhisper/auth/login'
|
256
|
+
expected_path = requested_path + '.html'
|
257
|
+
stub_request(:get, full_assets_url(expected_path)).
|
258
|
+
to_return(:status => 200, :body => 'Login', :headers => {})
|
259
|
+
|
260
|
+
get requested_path
|
261
|
+
assert last_response.ok?
|
262
|
+
assert_equal 'Login', last_response.body
|
263
|
+
end
|
264
|
+
|
265
|
+
def test_redirects
|
266
|
+
path = '/wwwhisper/admin/index.html'
|
267
|
+
stub_request(:get, full_url(@wwwhisper.auth_query(path))).
|
268
|
+
to_return(:status => 200, :body => '', :headers => {})
|
269
|
+
stub_request(:get, full_assets_url(path)).
|
270
|
+
to_return(:status => 303, :body => 'Admin page moved',
|
271
|
+
:headers => {'Location' => 'https://new.location/foo/bar'})
|
272
|
+
|
273
|
+
get path
|
274
|
+
assert !last_response.ok?
|
275
|
+
assert_equal 303, last_response.status
|
276
|
+
assert_equal 'Admin page moved', last_response.body
|
277
|
+
assert_equal("#{SITE_PROTO}://#{SITE_HOST}:#{SITE_PORT}/foo/bar",
|
278
|
+
last_response['Location'])
|
279
|
+
assert_requested :get, full_url(@wwwhisper.auth_query(path))
|
280
|
+
assert_requested :get, full_assets_url(path)
|
281
|
+
end
|
282
|
+
|
283
|
+
end
|
metadata
ADDED
@@ -0,0 +1,145 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: rack-wwwhisper
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.0.pre
|
5
|
+
prerelease: 6
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Jan Wrobel
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2013-01-09 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: rack
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ~>
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '1.0'
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ~>
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: '1.0'
|
30
|
+
- !ruby/object:Gem::Dependency
|
31
|
+
name: addresable
|
32
|
+
requirement: !ruby/object:Gem::Requirement
|
33
|
+
none: false
|
34
|
+
requirements:
|
35
|
+
- - ~>
|
36
|
+
- !ruby/object:Gem::Version
|
37
|
+
version: '2.0'
|
38
|
+
type: :runtime
|
39
|
+
prerelease: false
|
40
|
+
version_requirements: !ruby/object:Gem::Requirement
|
41
|
+
none: false
|
42
|
+
requirements:
|
43
|
+
- - ~>
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
version: '2.0'
|
46
|
+
- !ruby/object:Gem::Dependency
|
47
|
+
name: net-http-persistent
|
48
|
+
requirement: !ruby/object:Gem::Requirement
|
49
|
+
none: false
|
50
|
+
requirements:
|
51
|
+
- - ! '>='
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: '0'
|
54
|
+
type: :runtime
|
55
|
+
prerelease: false
|
56
|
+
version_requirements: !ruby/object:Gem::Requirement
|
57
|
+
none: false
|
58
|
+
requirements:
|
59
|
+
- - ! '>='
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
- !ruby/object:Gem::Dependency
|
63
|
+
name: rack-test
|
64
|
+
requirement: !ruby/object:Gem::Requirement
|
65
|
+
none: false
|
66
|
+
requirements:
|
67
|
+
- - ! '>='
|
68
|
+
- !ruby/object:Gem::Version
|
69
|
+
version: '0'
|
70
|
+
type: :development
|
71
|
+
prerelease: false
|
72
|
+
version_requirements: !ruby/object:Gem::Requirement
|
73
|
+
none: false
|
74
|
+
requirements:
|
75
|
+
- - ! '>='
|
76
|
+
- !ruby/object:Gem::Version
|
77
|
+
version: '0'
|
78
|
+
- !ruby/object:Gem::Dependency
|
79
|
+
name: webmock
|
80
|
+
requirement: !ruby/object:Gem::Requirement
|
81
|
+
none: false
|
82
|
+
requirements:
|
83
|
+
- - ! '>='
|
84
|
+
- !ruby/object:Gem::Version
|
85
|
+
version: '0'
|
86
|
+
type: :development
|
87
|
+
prerelease: false
|
88
|
+
version_requirements: !ruby/object:Gem::Requirement
|
89
|
+
none: false
|
90
|
+
requirements:
|
91
|
+
- - ! '>='
|
92
|
+
- !ruby/object:Gem::Version
|
93
|
+
version: '0'
|
94
|
+
- !ruby/object:Gem::Dependency
|
95
|
+
name: rake
|
96
|
+
requirement: !ruby/object:Gem::Requirement
|
97
|
+
none: false
|
98
|
+
requirements:
|
99
|
+
- - ! '>='
|
100
|
+
- !ruby/object:Gem::Version
|
101
|
+
version: '0'
|
102
|
+
type: :development
|
103
|
+
prerelease: false
|
104
|
+
version_requirements: !ruby/object:Gem::Requirement
|
105
|
+
none: false
|
106
|
+
requirements:
|
107
|
+
- - ! '>='
|
108
|
+
- !ruby/object:Gem::Version
|
109
|
+
version: '0'
|
110
|
+
description: Middleware that uses wwwhisper service to authorize requests.
|
111
|
+
email: wrr@mixedbit.org
|
112
|
+
executables: []
|
113
|
+
extensions: []
|
114
|
+
extra_rdoc_files: []
|
115
|
+
files:
|
116
|
+
- lib/rack/wwwhisper.rb
|
117
|
+
- test/test_wwwhisper.rb
|
118
|
+
- Rakefile
|
119
|
+
homepage: https://github.com/wrr/rack-wwwhisper
|
120
|
+
licenses:
|
121
|
+
- BSD
|
122
|
+
post_install_message:
|
123
|
+
rdoc_options: []
|
124
|
+
require_paths:
|
125
|
+
- lib
|
126
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
127
|
+
none: false
|
128
|
+
requirements:
|
129
|
+
- - ! '>='
|
130
|
+
- !ruby/object:Gem::Version
|
131
|
+
version: '0'
|
132
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
133
|
+
none: false
|
134
|
+
requirements:
|
135
|
+
- - ! '>'
|
136
|
+
- !ruby/object:Gem::Version
|
137
|
+
version: 1.3.1
|
138
|
+
requirements: []
|
139
|
+
rubyforge_project:
|
140
|
+
rubygems_version: 1.8.24
|
141
|
+
signing_key:
|
142
|
+
specification_version: 3
|
143
|
+
summary: Persona based authorization layer for Rack applications.
|
144
|
+
test_files:
|
145
|
+
- test/test_wwwhisper.rb
|