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.
Files changed (4) hide show
  1. data/Rakefile +13 -0
  2. data/lib/rack/wwwhisper.rb +267 -0
  3. data/test/test_wwwhisper.rb +283 -0
  4. metadata +145 -0
data/Rakefile ADDED
@@ -0,0 +1,13 @@
1
+ require 'rake/testtask'
2
+
3
+ Rake::TestTask.new do |t|
4
+ t.libs << 'test'
5
+ end
6
+
7
+ desc "Run tests"
8
+ task :default => :test
9
+
10
+
11
+ task :gem do
12
+ sh "gem build rack-wwwhisper.gemspec"
13
+ end
@@ -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