sinatra 0.2.1 → 0.2.2
Sign up to get free protection for your applications and to get access to all the features.
Potentially problematic release.
This version of sinatra might be problematic. Click here for more details.
- data/CHANGELOG +1 -0
- data/LICENSE +22 -0
- data/Manifest +78 -1
- data/lib/sinatra.rb +12 -1
- data/sinatra.gemspec +7 -14
- data/vendor/rack/AUTHORS +7 -0
- data/vendor/rack/COPYING +18 -0
- data/vendor/rack/KNOWN-ISSUES +18 -0
- data/vendor/rack/README +242 -0
- data/vendor/rack/Rakefile +174 -0
- data/vendor/rack/bin/rackup +153 -0
- data/vendor/rack/contrib/rack_logo.svg +111 -0
- data/vendor/rack/example/lobster.ru +4 -0
- data/vendor/rack/example/protectedlobster.rb +14 -0
- data/vendor/rack/example/protectedlobster.ru +8 -0
- data/vendor/rack/lib/rack.rb +92 -0
- data/vendor/rack/lib/rack/adapter/camping.rb +22 -0
- data/vendor/rack/lib/rack/auth/abstract/handler.rb +28 -0
- data/vendor/rack/lib/rack/auth/abstract/request.rb +37 -0
- data/vendor/rack/lib/rack/auth/basic.rb +58 -0
- data/vendor/rack/lib/rack/auth/digest/md5.rb +124 -0
- data/vendor/rack/lib/rack/auth/digest/nonce.rb +51 -0
- data/vendor/rack/lib/rack/auth/digest/params.rb +55 -0
- data/vendor/rack/lib/rack/auth/digest/request.rb +40 -0
- data/vendor/rack/lib/rack/auth/openid.rb +116 -0
- data/vendor/rack/lib/rack/builder.rb +56 -0
- data/vendor/rack/lib/rack/cascade.rb +36 -0
- data/vendor/rack/lib/rack/commonlogger.rb +56 -0
- data/vendor/rack/lib/rack/file.rb +112 -0
- data/vendor/rack/lib/rack/handler/cgi.rb +57 -0
- data/vendor/rack/lib/rack/handler/fastcgi.rb +83 -0
- data/vendor/rack/lib/rack/handler/lsws.rb +52 -0
- data/vendor/rack/lib/rack/handler/mongrel.rb +78 -0
- data/vendor/rack/lib/rack/handler/scgi.rb +57 -0
- data/vendor/rack/lib/rack/handler/webrick.rb +57 -0
- data/vendor/rack/lib/rack/lint.rb +394 -0
- data/vendor/rack/lib/rack/lobster.rb +65 -0
- data/vendor/rack/lib/rack/mock.rb +160 -0
- data/vendor/rack/lib/rack/recursive.rb +57 -0
- data/vendor/rack/lib/rack/reloader.rb +64 -0
- data/vendor/rack/lib/rack/request.rb +197 -0
- data/vendor/rack/lib/rack/response.rb +166 -0
- data/vendor/rack/lib/rack/session/abstract/id.rb +126 -0
- data/vendor/rack/lib/rack/session/cookie.rb +71 -0
- data/vendor/rack/lib/rack/session/memcache.rb +83 -0
- data/vendor/rack/lib/rack/session/pool.rb +67 -0
- data/vendor/rack/lib/rack/showexceptions.rb +344 -0
- data/vendor/rack/lib/rack/showstatus.rb +103 -0
- data/vendor/rack/lib/rack/static.rb +38 -0
- data/vendor/rack/lib/rack/urlmap.rb +48 -0
- data/vendor/rack/lib/rack/utils.rb +240 -0
- data/vendor/rack/test/cgi/lighttpd.conf +20 -0
- data/vendor/rack/test/cgi/test +9 -0
- data/vendor/rack/test/cgi/test.fcgi +7 -0
- data/vendor/rack/test/cgi/test.ru +7 -0
- data/vendor/rack/test/spec_rack_auth_basic.rb +69 -0
- data/vendor/rack/test/spec_rack_auth_digest.rb +169 -0
- data/vendor/rack/test/spec_rack_builder.rb +50 -0
- data/vendor/rack/test/spec_rack_camping.rb +47 -0
- data/vendor/rack/test/spec_rack_cascade.rb +50 -0
- data/vendor/rack/test/spec_rack_cgi.rb +91 -0
- data/vendor/rack/test/spec_rack_commonlogger.rb +32 -0
- data/vendor/rack/test/spec_rack_fastcgi.rb +91 -0
- data/vendor/rack/test/spec_rack_file.rb +40 -0
- data/vendor/rack/test/spec_rack_lint.rb +317 -0
- data/vendor/rack/test/spec_rack_lobster.rb +45 -0
- data/vendor/rack/test/spec_rack_mock.rb +152 -0
- data/vendor/rack/test/spec_rack_mongrel.rb +165 -0
- data/vendor/rack/test/spec_rack_recursive.rb +77 -0
- data/vendor/rack/test/spec_rack_request.rb +384 -0
- data/vendor/rack/test/spec_rack_response.rb +167 -0
- data/vendor/rack/test/spec_rack_session_cookie.rb +49 -0
- data/vendor/rack/test/spec_rack_session_memcache.rb +100 -0
- data/vendor/rack/test/spec_rack_session_pool.rb +84 -0
- data/vendor/rack/test/spec_rack_showexceptions.rb +21 -0
- data/vendor/rack/test/spec_rack_showstatus.rb +71 -0
- data/vendor/rack/test/spec_rack_static.rb +37 -0
- data/vendor/rack/test/spec_rack_urlmap.rb +175 -0
- data/vendor/rack/test/spec_rack_utils.rb +57 -0
- data/vendor/rack/test/spec_rack_webrick.rb +106 -0
- data/vendor/rack/test/testrequest.rb +43 -0
- metadata +81 -4
- data/Rakefile +0 -24
@@ -0,0 +1,48 @@
|
|
1
|
+
module Rack
|
2
|
+
# Rack::URLMap takes a hash mapping urls or paths to apps, and
|
3
|
+
# dispatches accordingly. Support for HTTP/1.1 host names exists if
|
4
|
+
# the URLs start with <tt>http://</tt> or <tt>https://</tt>.
|
5
|
+
#
|
6
|
+
# URLMap modifies the SCRIPT_NAME and PATH_INFO such that the part
|
7
|
+
# relevant for dispatch is in the SCRIPT_NAME, and the rest in the
|
8
|
+
# PATH_INFO. This should be taken care of when you need to
|
9
|
+
# reconstruct the URL in order to create links.
|
10
|
+
#
|
11
|
+
# URLMap dispatches in such a way that the longest paths are tried
|
12
|
+
# first, since they are most specific.
|
13
|
+
|
14
|
+
class URLMap
|
15
|
+
def initialize(map)
|
16
|
+
@mapping = map.map { |location, app|
|
17
|
+
if location =~ %r{\Ahttps?://(.*?)(/.*)}
|
18
|
+
host, location = $1, $2
|
19
|
+
else
|
20
|
+
host = nil
|
21
|
+
end
|
22
|
+
|
23
|
+
unless location[0] == ?/
|
24
|
+
raise ArgumentError, "paths need to start with /"
|
25
|
+
end
|
26
|
+
location = location.chomp('/')
|
27
|
+
|
28
|
+
[host, location, app]
|
29
|
+
}.sort_by { |(h, l, a)| -l.size } # Longest path first
|
30
|
+
end
|
31
|
+
|
32
|
+
def call(env)
|
33
|
+
path = env["PATH_INFO"].to_s.squeeze("/")
|
34
|
+
hHost, sName, sPort = env.values_at('HTTP_HOST','SERVER_NAME','SERVER_PORT')
|
35
|
+
@mapping.each { |host, location, app|
|
36
|
+
next unless (hHost == host || sName == host \
|
37
|
+
|| (host.nil? && (hHost == sName || hHost == sName+':'+sPort)))
|
38
|
+
next unless location == path[0, location.size]
|
39
|
+
next unless path[location.size] == nil || path[location.size] == ?/
|
40
|
+
env["SCRIPT_NAME"] += location
|
41
|
+
env["PATH_INFO"] = path[location.size..-1]
|
42
|
+
return app.call(env)
|
43
|
+
}
|
44
|
+
[404, {"Content-Type" => "text/plain"}, ["Not Found: #{path}"]]
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
@@ -0,0 +1,240 @@
|
|
1
|
+
require 'tempfile'
|
2
|
+
|
3
|
+
module Rack
|
4
|
+
# Rack::Utils contains a grab-bag of useful methods for writing web
|
5
|
+
# applications adopted from all kinds of Ruby libraries.
|
6
|
+
|
7
|
+
module Utils
|
8
|
+
# Performs URI escaping so that you can construct proper
|
9
|
+
# query strings faster. Use this rather than the cgi.rb
|
10
|
+
# version since it's faster. (Stolen from Camping).
|
11
|
+
def escape(s)
|
12
|
+
s.to_s.gsub(/([^ a-zA-Z0-9_.-]+)/n) {
|
13
|
+
'%'+$1.unpack('H2'*$1.size).join('%').upcase
|
14
|
+
}.tr(' ', '+')
|
15
|
+
end
|
16
|
+
module_function :escape
|
17
|
+
|
18
|
+
# Unescapes a URI escaped string. (Stolen from Camping).
|
19
|
+
def unescape(s)
|
20
|
+
s.tr('+', ' ').gsub(/((?:%[0-9a-fA-F]{2})+)/n){
|
21
|
+
[$1.delete('%')].pack('H*')
|
22
|
+
}
|
23
|
+
end
|
24
|
+
module_function :unescape
|
25
|
+
|
26
|
+
# Stolen from Mongrel:
|
27
|
+
# Parses a query string by breaking it up at the '&'
|
28
|
+
# and ';' characters. You can also use this to parse
|
29
|
+
# cookies by changing the characters used in the second
|
30
|
+
# parameter (which defaults to '&;').
|
31
|
+
|
32
|
+
def parse_query(qs, d = '&;')
|
33
|
+
params = {}
|
34
|
+
(qs||'').split(/[#{d}] */n).inject(params) { |h,p|
|
35
|
+
k, v=unescape(p).split('=',2)
|
36
|
+
if cur = params[k]
|
37
|
+
if cur.class == Array
|
38
|
+
params[k] << v
|
39
|
+
else
|
40
|
+
params[k] = [cur, v]
|
41
|
+
end
|
42
|
+
else
|
43
|
+
params[k] = v
|
44
|
+
end
|
45
|
+
}
|
46
|
+
|
47
|
+
return params
|
48
|
+
end
|
49
|
+
module_function :parse_query
|
50
|
+
|
51
|
+
# Escape ampersands, brackets and quotes to their HTML/XML entities.
|
52
|
+
def escape_html(string)
|
53
|
+
string.to_s.gsub("&", "&").
|
54
|
+
gsub("<", "<").
|
55
|
+
gsub(">", ">").
|
56
|
+
gsub("'", "'").
|
57
|
+
gsub('"', """)
|
58
|
+
end
|
59
|
+
module_function :escape_html
|
60
|
+
|
61
|
+
class Context < Proc
|
62
|
+
attr_reader :for, :app
|
63
|
+
def initialize app_f=nil, app_r=nil
|
64
|
+
@for, @app = app_f, app_r
|
65
|
+
end
|
66
|
+
alias_method :old_inspect, :inspect
|
67
|
+
def inspect
|
68
|
+
"#{old_inspect} ==> #{@for.inspect} ==> #{@app.inspect}"
|
69
|
+
end
|
70
|
+
def pretty_print pp
|
71
|
+
pp.text old_inspect
|
72
|
+
pp.nest 1 do
|
73
|
+
pp.breakable
|
74
|
+
pp.text '=for> '
|
75
|
+
pp.pp @for
|
76
|
+
pp.breakable
|
77
|
+
pp.text '=app> '
|
78
|
+
pp.pp @app
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
# A case-normalizing Hash, adjusting on [] and []=.
|
84
|
+
class HeaderHash < Hash
|
85
|
+
def initialize(hash={})
|
86
|
+
hash.each { |k, v| self[k] = v }
|
87
|
+
end
|
88
|
+
|
89
|
+
def to_hash
|
90
|
+
{}.replace(self)
|
91
|
+
end
|
92
|
+
|
93
|
+
def [](k)
|
94
|
+
super capitalize(k)
|
95
|
+
end
|
96
|
+
|
97
|
+
def []=(k, v)
|
98
|
+
super capitalize(k), v
|
99
|
+
end
|
100
|
+
|
101
|
+
def capitalize(k)
|
102
|
+
k.to_s.downcase.gsub(/^.|[-_\s]./) { |x| x.upcase }
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
# Every standard HTTP code mapped to the appropriate message.
|
107
|
+
# Stolen from Mongrel.
|
108
|
+
HTTP_STATUS_CODES = {
|
109
|
+
100 => 'Continue',
|
110
|
+
101 => 'Switching Protocols',
|
111
|
+
200 => 'OK',
|
112
|
+
201 => 'Created',
|
113
|
+
202 => 'Accepted',
|
114
|
+
203 => 'Non-Authoritative Information',
|
115
|
+
204 => 'No Content',
|
116
|
+
205 => 'Reset Content',
|
117
|
+
206 => 'Partial Content',
|
118
|
+
300 => 'Multiple Choices',
|
119
|
+
301 => 'Moved Permanently',
|
120
|
+
302 => 'Moved Temporarily',
|
121
|
+
303 => 'See Other',
|
122
|
+
304 => 'Not Modified',
|
123
|
+
305 => 'Use Proxy',
|
124
|
+
400 => 'Bad Request',
|
125
|
+
401 => 'Unauthorized',
|
126
|
+
402 => 'Payment Required',
|
127
|
+
403 => 'Forbidden',
|
128
|
+
404 => 'Not Found',
|
129
|
+
405 => 'Method Not Allowed',
|
130
|
+
406 => 'Not Acceptable',
|
131
|
+
407 => 'Proxy Authentication Required',
|
132
|
+
408 => 'Request Time-out',
|
133
|
+
409 => 'Conflict',
|
134
|
+
410 => 'Gone',
|
135
|
+
411 => 'Length Required',
|
136
|
+
412 => 'Precondition Failed',
|
137
|
+
413 => 'Request Entity Too Large',
|
138
|
+
414 => 'Request-URI Too Large',
|
139
|
+
415 => 'Unsupported Media Type',
|
140
|
+
500 => 'Internal Server Error',
|
141
|
+
501 => 'Not Implemented',
|
142
|
+
502 => 'Bad Gateway',
|
143
|
+
503 => 'Service Unavailable',
|
144
|
+
504 => 'Gateway Time-out',
|
145
|
+
505 => 'HTTP Version not supported'
|
146
|
+
}
|
147
|
+
|
148
|
+
# A multipart form data parser, adapted from IOWA.
|
149
|
+
#
|
150
|
+
# Usually, Rack::Request#POST takes care of calling this.
|
151
|
+
|
152
|
+
module Multipart
|
153
|
+
EOL = "\r\n"
|
154
|
+
|
155
|
+
def self.parse_multipart(env)
|
156
|
+
unless env['CONTENT_TYPE'] =~
|
157
|
+
%r|\Amultipart/form-data.*boundary=\"?([^\";,]+)\"?|n
|
158
|
+
nil
|
159
|
+
else
|
160
|
+
boundary = "--#{$1}"
|
161
|
+
|
162
|
+
params = {}
|
163
|
+
buf = ""
|
164
|
+
content_length = env['CONTENT_LENGTH'].to_i
|
165
|
+
input = env['rack.input']
|
166
|
+
|
167
|
+
boundary_size = boundary.size + EOL.size
|
168
|
+
bufsize = 16384
|
169
|
+
|
170
|
+
content_length -= boundary_size
|
171
|
+
|
172
|
+
status = input.read(boundary_size)
|
173
|
+
raise EOFError, "bad content body" unless status == boundary + EOL
|
174
|
+
|
175
|
+
rx = /(?:#{EOL})?#{Regexp.quote boundary}(#{EOL}|--)/
|
176
|
+
|
177
|
+
loop {
|
178
|
+
head = nil
|
179
|
+
body = ''
|
180
|
+
filename = content_type = name = nil
|
181
|
+
|
182
|
+
until head && buf =~ rx
|
183
|
+
if !head && i = buf.index("\r\n\r\n")
|
184
|
+
head = buf.slice!(0, i+2) # First \r\n
|
185
|
+
buf.slice!(0, 2) # Second \r\n
|
186
|
+
|
187
|
+
filename = head[/Content-Disposition:.* filename="?([^\";]*)"?/ni, 1]
|
188
|
+
content_type = head[/Content-Type: (.*)\r\n/ni, 1]
|
189
|
+
name = head[/Content-Disposition:.* name="?([^\";]*)"?/ni, 1]
|
190
|
+
|
191
|
+
body = Tempfile.new("RackMultipart") if filename
|
192
|
+
|
193
|
+
next
|
194
|
+
end
|
195
|
+
|
196
|
+
# Save the read body part.
|
197
|
+
if head && (boundary_size+4 < buf.size)
|
198
|
+
body << buf.slice!(0, buf.size - (boundary_size+4))
|
199
|
+
end
|
200
|
+
|
201
|
+
c = input.read(bufsize < content_length ? bufsize : content_length)
|
202
|
+
raise EOFError, "bad content body" if c.nil? || c.empty?
|
203
|
+
buf << c
|
204
|
+
content_length -= c.size
|
205
|
+
end
|
206
|
+
|
207
|
+
# Save the rest.
|
208
|
+
if i = buf.index(rx)
|
209
|
+
body << buf.slice!(0, i)
|
210
|
+
buf.slice!(0, boundary_size+2)
|
211
|
+
|
212
|
+
content_length = -1 if $1 == "--"
|
213
|
+
end
|
214
|
+
|
215
|
+
if filename
|
216
|
+
body.rewind
|
217
|
+
data = {:filename => filename, :type => content_type,
|
218
|
+
:name => name, :tempfile => body, :head => head}
|
219
|
+
else
|
220
|
+
data = body
|
221
|
+
end
|
222
|
+
|
223
|
+
if name
|
224
|
+
if name =~ /\[\]\z/
|
225
|
+
params[name] ||= []
|
226
|
+
params[name] << data
|
227
|
+
else
|
228
|
+
params[name] = data
|
229
|
+
end
|
230
|
+
end
|
231
|
+
|
232
|
+
break if buf.empty? || content_length == -1
|
233
|
+
}
|
234
|
+
|
235
|
+
params
|
236
|
+
end
|
237
|
+
end
|
238
|
+
end
|
239
|
+
end
|
240
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
server.modules = ("mod_fastcgi", "mod_cgi")
|
2
|
+
server.document-root = "."
|
3
|
+
server.errorlog = "lighttpd.errors"
|
4
|
+
server.port = 9203
|
5
|
+
|
6
|
+
server.event-handler = "freebsd-kqueue"
|
7
|
+
|
8
|
+
cgi.assign = ("/test" => "",
|
9
|
+
# ".ru" => ""
|
10
|
+
)
|
11
|
+
|
12
|
+
fastcgi.server = ("test.fcgi" => ("localhost" =>
|
13
|
+
("min-procs" => 1,
|
14
|
+
"socket" => "/tmp/rack-test-fcgi",
|
15
|
+
"bin-path" => "test.fcgi")),
|
16
|
+
"test.ru" => ("localhost" =>
|
17
|
+
("min-procs" => 1,
|
18
|
+
"socket" => "/tmp/rack-test-ru-fcgi",
|
19
|
+
"bin-path" => "test.ru")),
|
20
|
+
)
|
@@ -0,0 +1,69 @@
|
|
1
|
+
require 'test/spec'
|
2
|
+
|
3
|
+
require 'rack/auth/basic'
|
4
|
+
require 'rack/mock'
|
5
|
+
|
6
|
+
context 'Rack::Auth::Basic' do
|
7
|
+
|
8
|
+
def realm
|
9
|
+
'WallysWorld'
|
10
|
+
end
|
11
|
+
|
12
|
+
def unprotected_app
|
13
|
+
lambda { |env| [ 200, {'Content-Type' => 'text/plain'}, ["Hi #{env['REMOTE_USER']}"] ] }
|
14
|
+
end
|
15
|
+
|
16
|
+
def protected_app
|
17
|
+
app = Rack::Auth::Basic.new(unprotected_app) { |username, password| 'Boss' == username }
|
18
|
+
app.realm = realm
|
19
|
+
app
|
20
|
+
end
|
21
|
+
|
22
|
+
setup do
|
23
|
+
@request = Rack::MockRequest.new(protected_app)
|
24
|
+
end
|
25
|
+
|
26
|
+
def request_with_basic_auth(username, password, &block)
|
27
|
+
request 'HTTP_AUTHORIZATION' => 'Basic ' + ["#{username}:#{password}"].pack("m*"), &block
|
28
|
+
end
|
29
|
+
|
30
|
+
def request(headers = {})
|
31
|
+
yield @request.get('/', headers)
|
32
|
+
end
|
33
|
+
|
34
|
+
def assert_basic_auth_challenge(response)
|
35
|
+
response.should.be.a.client_error
|
36
|
+
response.status.should.equal 401
|
37
|
+
response.should.include 'WWW-Authenticate'
|
38
|
+
response.headers['WWW-Authenticate'].should =~ /Basic realm="/
|
39
|
+
response.body.should.be.empty
|
40
|
+
end
|
41
|
+
|
42
|
+
specify 'should challenge correctly when no credentials are specified' do
|
43
|
+
request do |response|
|
44
|
+
assert_basic_auth_challenge response
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
specify 'should rechallenge if incorrect credentials are specified' do
|
49
|
+
request_with_basic_auth 'joe', 'password' do |response|
|
50
|
+
assert_basic_auth_challenge response
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
specify 'should return application output if correct credentials are specified' do
|
55
|
+
request_with_basic_auth 'Boss', 'password' do |response|
|
56
|
+
response.status.should.equal 200
|
57
|
+
response.body.to_s.should.equal 'Hi Boss'
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
specify 'should return 400 Bad Request if different auth scheme used' do
|
62
|
+
request 'HTTP_AUTHORIZATION' => 'Digest params' do |response|
|
63
|
+
response.should.be.a.client_error
|
64
|
+
response.status.should.equal 400
|
65
|
+
response.should.not.include 'WWW-Authenticate'
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
end
|
@@ -0,0 +1,169 @@
|
|
1
|
+
require 'test/spec'
|
2
|
+
|
3
|
+
require 'rack/auth/digest/md5'
|
4
|
+
require 'rack/mock'
|
5
|
+
|
6
|
+
context 'Rack::Auth::Digest::MD5' do
|
7
|
+
|
8
|
+
def realm
|
9
|
+
'WallysWorld'
|
10
|
+
end
|
11
|
+
|
12
|
+
def unprotected_app
|
13
|
+
lambda do |env|
|
14
|
+
[ 200, {'Content-Type' => 'text/plain'}, ["Hi #{env['REMOTE_USER']}"] ]
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def protected_app
|
19
|
+
app = Rack::Auth::Digest::MD5.new(unprotected_app) do |username|
|
20
|
+
{ 'Alice' => 'correct-password' }[username]
|
21
|
+
end
|
22
|
+
app.realm = realm
|
23
|
+
app.opaque = 'this-should-be-secret'
|
24
|
+
app
|
25
|
+
end
|
26
|
+
|
27
|
+
def protected_app_with_hashed_passwords
|
28
|
+
app = Rack::Auth::Digest::MD5.new(unprotected_app) do |username|
|
29
|
+
username == 'Alice' ? Digest::MD5.hexdigest("Alice:#{realm}:correct-password") : nil
|
30
|
+
end
|
31
|
+
app.realm = realm
|
32
|
+
app.opaque = 'this-should-be-secret'
|
33
|
+
app.passwords_hashed = true
|
34
|
+
app
|
35
|
+
end
|
36
|
+
|
37
|
+
setup do
|
38
|
+
@request = Rack::MockRequest.new(protected_app)
|
39
|
+
end
|
40
|
+
|
41
|
+
def request(path, headers = {}, &block)
|
42
|
+
response = @request.get(path, headers)
|
43
|
+
block.call(response) if block
|
44
|
+
return response
|
45
|
+
end
|
46
|
+
|
47
|
+
class MockDigestRequest
|
48
|
+
def initialize(params)
|
49
|
+
@params = params
|
50
|
+
end
|
51
|
+
def method_missing(sym)
|
52
|
+
if @params.has_key? k = sym.to_s
|
53
|
+
return @params[k]
|
54
|
+
end
|
55
|
+
super
|
56
|
+
end
|
57
|
+
def method
|
58
|
+
'GET'
|
59
|
+
end
|
60
|
+
def response(password)
|
61
|
+
Rack::Auth::Digest::MD5.new(nil).send :digest, self, password
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def request_with_digest_auth(path, username, password, options = {}, &block)
|
66
|
+
response = request('/')
|
67
|
+
|
68
|
+
return response unless response.status == 401
|
69
|
+
|
70
|
+
if wait = options.delete(:wait)
|
71
|
+
sleep wait
|
72
|
+
end
|
73
|
+
|
74
|
+
challenge = response['WWW-Authenticate'].split(' ', 2).last
|
75
|
+
|
76
|
+
params = Rack::Auth::Digest::Params.parse(challenge)
|
77
|
+
|
78
|
+
params['username'] = username
|
79
|
+
params['nc'] = '00000001'
|
80
|
+
params['cnonce'] = 'nonsensenonce'
|
81
|
+
params['uri'] = path
|
82
|
+
|
83
|
+
params.update options
|
84
|
+
|
85
|
+
params['response'] = MockDigestRequest.new(params).response(password)
|
86
|
+
|
87
|
+
request(path, { 'HTTP_AUTHORIZATION' => "Digest #{params}" }, &block)
|
88
|
+
end
|
89
|
+
|
90
|
+
def assert_digest_auth_challenge(response)
|
91
|
+
response.should.be.a.client_error
|
92
|
+
response.status.should.equal 401
|
93
|
+
response.should.include 'WWW-Authenticate'
|
94
|
+
response.headers['WWW-Authenticate'].should =~ /^Digest /
|
95
|
+
response.body.should.be.empty
|
96
|
+
end
|
97
|
+
|
98
|
+
def assert_bad_request(response)
|
99
|
+
response.should.be.a.client_error
|
100
|
+
response.status.should.equal 400
|
101
|
+
response.should.not.include 'WWW-Authenticate'
|
102
|
+
end
|
103
|
+
|
104
|
+
specify 'should challenge when no credentials are specified' do
|
105
|
+
request '/' do |response|
|
106
|
+
assert_digest_auth_challenge response
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
specify 'should return application output if correct credentials given' do
|
111
|
+
request_with_digest_auth '/', 'Alice', 'correct-password' do |response|
|
112
|
+
response.status.should.equal 200
|
113
|
+
response.body.to_s.should.equal 'Hi Alice'
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
specify 'should return application output if correct credentials given (hashed passwords)' do
|
118
|
+
@request = Rack::MockRequest.new(protected_app_with_hashed_passwords)
|
119
|
+
|
120
|
+
request_with_digest_auth '/', 'Alice', 'correct-password' do |response|
|
121
|
+
response.status.should.equal 200
|
122
|
+
response.body.to_s.should.equal 'Hi Alice'
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
specify 'should rechallenge if incorrect username given' do
|
127
|
+
request_with_digest_auth '/', 'Bob', 'correct-password' do |response|
|
128
|
+
assert_digest_auth_challenge response
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
specify 'should rechallenge if incorrect password given' do
|
133
|
+
request_with_digest_auth '/', 'Alice', 'wrong-password' do |response|
|
134
|
+
assert_digest_auth_challenge response
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
specify 'should rechallenge with stale parameter if nonce is stale' do
|
139
|
+
begin
|
140
|
+
Rack::Auth::Digest::Nonce.time_limit = 1
|
141
|
+
|
142
|
+
request_with_digest_auth '/', 'Alice', 'correct-password', :wait => 2 do |response|
|
143
|
+
assert_digest_auth_challenge response
|
144
|
+
response.headers['WWW-Authenticate'].should =~ /\bstale=true\b/
|
145
|
+
end
|
146
|
+
ensure
|
147
|
+
Rack::Auth::Digest::Nonce.time_limit = nil
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
specify 'should return 400 Bad Request if incorrect qop given' do
|
152
|
+
request_with_digest_auth '/', 'Alice', 'correct-password', 'qop' => 'auth-int' do |response|
|
153
|
+
assert_bad_request response
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
specify 'should return 400 Bad Request if incorrect uri given' do
|
158
|
+
request_with_digest_auth '/', 'Alice', 'correct-password', 'uri' => '/foo' do |response|
|
159
|
+
assert_bad_request response
|
160
|
+
end
|
161
|
+
end
|
162
|
+
|
163
|
+
specify 'should return 400 Bad Request if different auth scheme used' do
|
164
|
+
request '/', 'HTTP_AUTHORIZATION' => 'Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==' do |response|
|
165
|
+
assert_bad_request response
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
169
|
+
end
|