rack-wwwhisper 1.0.0.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/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
|