rack-wwwhisper 1.0.0.pre

Sign up to get free protection for your applications and to get access to all the features.
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