rack 1.4.0 → 1.4.1
Sign up to get free protection for your applications and to get access to all the features.
Potentially problematic release.
This version of rack might be problematic. Click here for more details.
- data/README.rdoc +17 -1
- data/lib/rack.rb +1 -1
- data/lib/rack/body_proxy.rb +5 -2
- data/lib/rack/cascade.rb +1 -1
- data/lib/rack/file.rb +2 -2
- data/lib/rack/multipart/parser.rb +6 -13
- data/lib/rack/request.rb +2 -2
- data/lib/rack/response.rb +17 -16
- data/lib/rack/session/abstract/id.rb +3 -3
- data/lib/rack/session/cookie.rb +11 -7
- data/lib/rack/showstatus.rb +2 -2
- data/lib/rack/static.rb +1 -1
- data/lib/rack/utils.rb +48 -28
- data/rack.gemspec +1 -1
- data/test/multipart/filename_with_unescaped_percentages +6 -0
- data/test/multipart/filename_with_unescaped_percentages2 +6 -0
- data/test/multipart/filename_with_unescaped_percentages3 +6 -0
- data/test/spec_body_proxy.rb +17 -0
- data/test/spec_cascade.rb +6 -3
- data/test/spec_file.rb +16 -5
- data/test/spec_multipart.rb +47 -2
- data/test/spec_request.rb +13 -0
- data/test/spec_response.rb +11 -3
- data/test/spec_session_cookie.rb +25 -2
- data/test/spec_static.rb +5 -0
- data/test/spec_utils.rb +16 -7
- metadata +7 -4
data/README.rdoc
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
= Rack, a modular Ruby webserver interface
|
1
|
+
= Rack, a modular Ruby webserver interface {<img src="https://secure.travis-ci.org/rack/rack.png" alt="Build Status" />}[http://travis-ci.org/rack/rack] {<img src="https://gemnasium.com/rack/rack.png" alt="Dependency Status" />}[https://gemnasium.com/rack/rack]
|
2
2
|
|
3
3
|
Rack provides a minimal, modular and adaptable interface for developing
|
4
4
|
web applications in Ruby. By wrapping HTTP requests and responses in
|
@@ -392,6 +392,22 @@ run on port 11211) and memcache-client installed.
|
|
392
392
|
* Support added for HTTP_X_FORWARDED_SCHEME
|
393
393
|
* Numerous bug fixes, including many fixes for new and alternate rubies
|
394
394
|
|
395
|
+
* January 22nd, 2012: Twenty fifth public release 1.4.1
|
396
|
+
* Alter the keyspace limit calculations to reduce issues with nested params
|
397
|
+
* Add a workaround for multipart parsing where files contian unescaped "%"
|
398
|
+
* Added Rack::Response::Helpers#method_not_allowed? (code 405)
|
399
|
+
* Rack::File now returns 404's for illegal directory traversals
|
400
|
+
* Rack::File now returns 405's for illegal methods (non HEAD/GET)
|
401
|
+
* Rack::Cascade now catches 405 by default, as well as 404
|
402
|
+
* Cookies missing '--' no longer cause an exception to be raised
|
403
|
+
* Various style changes and documentation spelling errors
|
404
|
+
* Rack::BodyProxy always ensures to execute it's block
|
405
|
+
* Additional test coverage around cookies and secrets
|
406
|
+
* Rack::Session::Cookie can now be supplied either secret or old_secret
|
407
|
+
* Tests are no longer dependent on set order
|
408
|
+
* Rack::Static no longer defaults to serving index files
|
409
|
+
* Rack.release was fixed
|
410
|
+
|
395
411
|
== Contact
|
396
412
|
|
397
413
|
Please post bugs, suggestions and patches to
|
data/lib/rack.rb
CHANGED
data/lib/rack/body_proxy.rb
CHANGED
data/lib/rack/cascade.rb
CHANGED
data/lib/rack/file.rb
CHANGED
@@ -34,7 +34,7 @@ module Rack
|
|
34
34
|
|
35
35
|
def _call(env)
|
36
36
|
unless ALLOWED_VERBS.include? env["REQUEST_METHOD"]
|
37
|
-
return fail(
|
37
|
+
return fail(405, "Method Not Allowed")
|
38
38
|
end
|
39
39
|
|
40
40
|
@path_info = Utils.unescape(env["PATH_INFO"])
|
@@ -45,7 +45,7 @@ module Rack
|
|
45
45
|
when '', '.'
|
46
46
|
depth
|
47
47
|
when '..'
|
48
|
-
return fail(
|
48
|
+
return fail(404, "Not Found") if depth - 1 < 0
|
49
49
|
depth - 1
|
50
50
|
else
|
51
51
|
depth + 1
|
@@ -14,9 +14,6 @@ module Rack
|
|
14
14
|
|
15
15
|
fast_forward_to_first_boundary
|
16
16
|
|
17
|
-
max_key_space = Utils.key_space_limit
|
18
|
-
bytes = 0
|
19
|
-
|
20
17
|
loop do
|
21
18
|
head, filename, content_type, name, body =
|
22
19
|
get_current_head_and_filename_and_content_type_and_name_and_body
|
@@ -31,13 +28,6 @@ module Rack
|
|
31
28
|
|
32
29
|
filename, data = get_data(filename, body, content_type, name, head)
|
33
30
|
|
34
|
-
if name
|
35
|
-
bytes += name.size
|
36
|
-
if bytes > max_key_space
|
37
|
-
raise RangeError, "exceeded available parameter key space"
|
38
|
-
end
|
39
|
-
end
|
40
|
-
|
41
31
|
Utils.normalize_params(@params, name, data) unless data.nil?
|
42
32
|
|
43
33
|
# break if we're at the end of a buffer, but not if it is the end of a field
|
@@ -46,7 +36,7 @@ module Rack
|
|
46
36
|
|
47
37
|
@io.rewind
|
48
38
|
|
49
|
-
@params
|
39
|
+
@params.to_params_hash
|
50
40
|
end
|
51
41
|
|
52
42
|
private
|
@@ -56,7 +46,7 @@ module Rack
|
|
56
46
|
@boundary = "--#{$1}"
|
57
47
|
|
58
48
|
@buf = ""
|
59
|
-
@params =
|
49
|
+
@params = Utils::KeySpaceConstrainedParams.new
|
60
50
|
|
61
51
|
@content_length = @env['CONTENT_LENGTH'].to_i
|
62
52
|
@io = @env['rack.input']
|
@@ -135,8 +125,11 @@ module Rack
|
|
135
125
|
filename = $1
|
136
126
|
end
|
137
127
|
|
128
|
+
if filename && filename.scan(/%.?.?/).all? { |s| s =~ /%[0-9a-fA-F]{2}/ }
|
129
|
+
filename = Utils.unescape(filename)
|
130
|
+
end
|
138
131
|
if filename && filename !~ /\\[^\\"]/
|
139
|
-
filename =
|
132
|
+
filename = filename.gsub(/\\(.)/, '\1')
|
140
133
|
end
|
141
134
|
filename
|
142
135
|
end
|
data/lib/rack/request.rb
CHANGED
@@ -177,7 +177,7 @@ module Rack
|
|
177
177
|
PARSEABLE_DATA_MEDIA_TYPES.include?(media_type)
|
178
178
|
end
|
179
179
|
|
180
|
-
# Returns the data
|
180
|
+
# Returns the data received in the query string.
|
181
181
|
def GET
|
182
182
|
if @env["rack.request.query_string"] == query_string
|
183
183
|
@env["rack.request.query_hash"]
|
@@ -187,7 +187,7 @@ module Rack
|
|
187
187
|
end
|
188
188
|
end
|
189
189
|
|
190
|
-
# Returns the data
|
190
|
+
# Returns the data received in the request body.
|
191
191
|
#
|
192
192
|
# This method support both application/x-www-form-urlencoded and
|
193
193
|
# multipart/form-data.
|
data/lib/rack/response.rb
CHANGED
@@ -12,7 +12,7 @@ module Rack
|
|
12
12
|
# You can use Response#write to iteratively generate your response,
|
13
13
|
# but note that this is buffered by Rack::Response until you call
|
14
14
|
# +finish+. +finish+ however can take a block inside which calls to
|
15
|
-
# +write+ are
|
15
|
+
# +write+ are synchronous with the Rack response.
|
16
16
|
#
|
17
17
|
# Your application's +call+ should end returning Response#finish.
|
18
18
|
|
@@ -112,21 +112,22 @@ module Rack
|
|
112
112
|
alias headers header
|
113
113
|
|
114
114
|
module Helpers
|
115
|
-
def invalid?;
|
116
|
-
|
117
|
-
def informational?;
|
118
|
-
def successful?;
|
119
|
-
def redirection?;
|
120
|
-
def client_error?;
|
121
|
-
def server_error?;
|
122
|
-
|
123
|
-
def ok?;
|
124
|
-
def bad_request?;
|
125
|
-
def forbidden?;
|
126
|
-
def not_found?;
|
127
|
-
def
|
128
|
-
|
129
|
-
|
115
|
+
def invalid?; status < 100 || status >= 600; end
|
116
|
+
|
117
|
+
def informational?; status >= 100 && status < 200; end
|
118
|
+
def successful?; status >= 200 && status < 300; end
|
119
|
+
def redirection?; status >= 300 && status < 400; end
|
120
|
+
def client_error?; status >= 400 && status < 500; end
|
121
|
+
def server_error?; status >= 500 && status < 600; end
|
122
|
+
|
123
|
+
def ok?; status == 200; end
|
124
|
+
def bad_request?; status == 400; end
|
125
|
+
def forbidden?; status == 403; end
|
126
|
+
def not_found?; status == 404; end
|
127
|
+
def method_not_allowed?; status == 405; end
|
128
|
+
def unprocessable?; status == 422; end
|
129
|
+
|
130
|
+
def redirect?; [301, 302, 303, 307].include? status; end
|
130
131
|
|
131
132
|
# Headers
|
132
133
|
attr_reader :headers, :original_headers
|
@@ -36,7 +36,7 @@ module Rack
|
|
36
36
|
private
|
37
37
|
|
38
38
|
def session_id_not_loaded?
|
39
|
-
!key?(:id)
|
39
|
+
!(@session_id_loaded || key?(:id))
|
40
40
|
end
|
41
41
|
|
42
42
|
def load_session_id!
|
@@ -183,7 +183,7 @@ module Rack
|
|
183
183
|
:renew => false,
|
184
184
|
:sidbits => 128,
|
185
185
|
:cookie_only => true,
|
186
|
-
:secure_random =>
|
186
|
+
:secure_random => (::SecureRandom rescue false)
|
187
187
|
}
|
188
188
|
|
189
189
|
attr_reader :key, :default_options
|
@@ -191,7 +191,7 @@ module Rack
|
|
191
191
|
def initialize(app, options={})
|
192
192
|
@app = app
|
193
193
|
@default_options = self.class::DEFAULT_OPTIONS.merge(options)
|
194
|
-
@key =
|
194
|
+
@key = @default_options.delete(:key)
|
195
195
|
@cookie_only = @default_options.delete(:cookie_only)
|
196
196
|
initialize_sid
|
197
197
|
end
|
data/lib/rack/session/cookie.rb
CHANGED
@@ -81,8 +81,7 @@ module Rack
|
|
81
81
|
attr_reader :coder
|
82
82
|
|
83
83
|
def initialize(app, options={})
|
84
|
-
@
|
85
|
-
@old_secret = options[:old_secret]
|
84
|
+
@secrets = options.values_at(:secret, :old_secret).compact
|
86
85
|
@coder = options[:coder] ||= Base64::Marshal.new
|
87
86
|
super(app, options.merge!(:cookie_only => true))
|
88
87
|
end
|
@@ -104,11 +103,16 @@ module Rack
|
|
104
103
|
request = Rack::Request.new(env)
|
105
104
|
session_data = request.cookies[@key]
|
106
105
|
|
107
|
-
if
|
106
|
+
if @secrets.size > 0 && session_data
|
108
107
|
session_data, digest = session_data.split("--")
|
109
|
-
|
110
|
-
|
108
|
+
|
109
|
+
if session_data && digest
|
110
|
+
ok = @secrets.any? do |secret|
|
111
|
+
secret && digest == generate_hmac(session_data, secret)
|
112
|
+
end
|
111
113
|
end
|
114
|
+
|
115
|
+
session_data = nil unless ok
|
112
116
|
end
|
113
117
|
|
114
118
|
coder.decode(session_data) || {}
|
@@ -131,8 +135,8 @@ module Rack
|
|
131
135
|
session = session.merge("session_id" => session_id)
|
132
136
|
session_data = coder.encode(session)
|
133
137
|
|
134
|
-
if @
|
135
|
-
session_data = "#{session_data}--#{generate_hmac(session_data, @
|
138
|
+
if @secrets.first
|
139
|
+
session_data = "#{session_data}--#{generate_hmac(session_data, @secrets.first)}"
|
136
140
|
end
|
137
141
|
|
138
142
|
if session_data.size > (4096 - @key.size)
|
data/lib/rack/showstatus.rb
CHANGED
@@ -3,8 +3,8 @@ require 'rack/request'
|
|
3
3
|
require 'rack/utils'
|
4
4
|
|
5
5
|
module Rack
|
6
|
-
# Rack::ShowStatus catches all empty responses
|
7
|
-
#
|
6
|
+
# Rack::ShowStatus catches all empty responses and replaces them
|
7
|
+
# with a site explaining the error.
|
8
8
|
#
|
9
9
|
# Additional details can be put into <tt>rack.showstatus.detail</tt>
|
10
10
|
# and will be shown as HTML. If such details exist, the error page
|
data/lib/rack/static.rb
CHANGED
@@ -38,7 +38,7 @@ module Rack
|
|
38
38
|
def initialize(app, options={})
|
39
39
|
@app = app
|
40
40
|
@urls = options[:urls] || ["/favicon.ico"]
|
41
|
-
@index = options[:index]
|
41
|
+
@index = options[:index]
|
42
42
|
root = options[:root] || Dir.pwd
|
43
43
|
cache_control = options[:cache_control]
|
44
44
|
@file_server = Rack::File.new(root, cache_control)
|
data/lib/rack/utils.rb
CHANGED
@@ -61,21 +61,11 @@ module Rack
|
|
61
61
|
# cookies by changing the characters used in the second
|
62
62
|
# parameter (which defaults to '&;').
|
63
63
|
def parse_query(qs, d = nil)
|
64
|
-
params =
|
65
|
-
|
66
|
-
max_key_space = Utils.key_space_limit
|
67
|
-
bytes = 0
|
64
|
+
params = KeySpaceConstrainedParams.new
|
68
65
|
|
69
66
|
(qs || '').split(d ? /[#{d}] */n : DEFAULT_SEP).each do |p|
|
70
67
|
k, v = p.split('=', 2).map { |x| unescape(x) }
|
71
68
|
|
72
|
-
if k
|
73
|
-
bytes += k.size
|
74
|
-
if bytes > max_key_space
|
75
|
-
raise RangeError, "exceeded available parameter key space"
|
76
|
-
end
|
77
|
-
end
|
78
|
-
|
79
69
|
if cur = params[k]
|
80
70
|
if cur.class == Array
|
81
71
|
params[k] << v
|
@@ -87,30 +77,20 @@ module Rack
|
|
87
77
|
end
|
88
78
|
end
|
89
79
|
|
90
|
-
return params
|
80
|
+
return params.to_params_hash
|
91
81
|
end
|
92
82
|
module_function :parse_query
|
93
83
|
|
94
84
|
def parse_nested_query(qs, d = nil)
|
95
|
-
params =
|
96
|
-
|
97
|
-
max_key_space = Utils.key_space_limit
|
98
|
-
bytes = 0
|
85
|
+
params = KeySpaceConstrainedParams.new
|
99
86
|
|
100
87
|
(qs || '').split(d ? /[#{d}] */n : DEFAULT_SEP).each do |p|
|
101
88
|
k, v = p.split('=', 2).map { |s| unescape(s) }
|
102
89
|
|
103
|
-
if k
|
104
|
-
bytes += k.size
|
105
|
-
if bytes > max_key_space
|
106
|
-
raise RangeError, "exceeded available parameter key space"
|
107
|
-
end
|
108
|
-
end
|
109
|
-
|
110
90
|
normalize_params(params, k, v)
|
111
91
|
end
|
112
92
|
|
113
|
-
return params
|
93
|
+
return params.to_params_hash
|
114
94
|
end
|
115
95
|
module_function :parse_nested_query
|
116
96
|
|
@@ -131,14 +111,14 @@ module Rack
|
|
131
111
|
child_key = $1
|
132
112
|
params[k] ||= []
|
133
113
|
raise TypeError, "expected Array (got #{params[k].class.name}) for param `#{k}'" unless params[k].is_a?(Array)
|
134
|
-
if params[k].last
|
114
|
+
if params_hash_type?(params[k].last) && !params[k].last.key?(child_key)
|
135
115
|
normalize_params(params[k].last, child_key, v)
|
136
116
|
else
|
137
|
-
params[k] << normalize_params(
|
117
|
+
params[k] << normalize_params(params.class.new, child_key, v)
|
138
118
|
end
|
139
119
|
else
|
140
|
-
params[k] ||=
|
141
|
-
raise TypeError, "expected Hash (got #{params[k].class.name}) for param `#{k}'" unless params[k]
|
120
|
+
params[k] ||= params.class.new
|
121
|
+
raise TypeError, "expected Hash (got #{params[k].class.name}) for param `#{k}'" unless params_hash_type?(params[k])
|
142
122
|
params[k] = normalize_params(params[k], after, v)
|
143
123
|
end
|
144
124
|
|
@@ -146,6 +126,11 @@ module Rack
|
|
146
126
|
end
|
147
127
|
module_function :normalize_params
|
148
128
|
|
129
|
+
def params_hash_type?(obj)
|
130
|
+
obj.kind_of?(KeySpaceConstrainedParams) || obj.kind_of?(Hash)
|
131
|
+
end
|
132
|
+
module_function :params_hash_type?
|
133
|
+
|
149
134
|
def build_query(params)
|
150
135
|
params.map { |k, v|
|
151
136
|
if v.class == Array
|
@@ -445,6 +430,41 @@ module Rack
|
|
445
430
|
end
|
446
431
|
end
|
447
432
|
|
433
|
+
class KeySpaceConstrainedParams
|
434
|
+
def initialize(limit = Utils.key_space_limit)
|
435
|
+
@limit = limit
|
436
|
+
@size = 0
|
437
|
+
@params = {}
|
438
|
+
end
|
439
|
+
|
440
|
+
def [](key)
|
441
|
+
@params[key]
|
442
|
+
end
|
443
|
+
|
444
|
+
def []=(key, value)
|
445
|
+
@size += key.size unless @params.key?(key)
|
446
|
+
raise RangeError, 'exceeded available parameter key space' if @size > @limit
|
447
|
+
@params[key] = value
|
448
|
+
end
|
449
|
+
|
450
|
+
def key?(key)
|
451
|
+
@params.key?(key)
|
452
|
+
end
|
453
|
+
|
454
|
+
def to_params_hash
|
455
|
+
hash = @params
|
456
|
+
hash.keys.each do |key|
|
457
|
+
value = hash[key]
|
458
|
+
if value.kind_of?(self.class)
|
459
|
+
hash[key] = value.to_params_hash
|
460
|
+
elsif value.kind_of?(Array)
|
461
|
+
value.map! {|x| x.kind_of?(self.class) ? x.to_params_hash : x}
|
462
|
+
end
|
463
|
+
end
|
464
|
+
hash
|
465
|
+
end
|
466
|
+
end
|
467
|
+
|
448
468
|
# Every standard HTTP code mapped to the appropriate message.
|
449
469
|
# Generated with:
|
450
470
|
# curl -s http://www.iana.org/assignments/http-status-codes | \
|
data/rack.gemspec
CHANGED
data/test/spec_body_proxy.rb
CHANGED
@@ -1,4 +1,5 @@
|
|
1
1
|
require 'rack/body_proxy'
|
2
|
+
require 'stringio'
|
2
3
|
|
3
4
|
describe Rack::BodyProxy do
|
4
5
|
should 'call each on the wrapped body' do
|
@@ -32,6 +33,22 @@ describe Rack::BodyProxy do
|
|
32
33
|
called.should.equal true
|
33
34
|
end
|
34
35
|
|
36
|
+
should 'call the passed block on close even if there is an exception' do
|
37
|
+
object = Object.new
|
38
|
+
def object.close() raise "No!" end
|
39
|
+
called = false
|
40
|
+
|
41
|
+
begin
|
42
|
+
proxy = Rack::BodyProxy.new(object) { called = true }
|
43
|
+
called.should.equal false
|
44
|
+
proxy.close
|
45
|
+
rescue RuntimeError => e
|
46
|
+
end
|
47
|
+
|
48
|
+
raise "Expected exception to have been raised" unless e
|
49
|
+
called.should.equal true
|
50
|
+
end
|
51
|
+
|
35
52
|
should 'not close more than one time' do
|
36
53
|
count = 0
|
37
54
|
proxy = Rack::BodyProxy.new([]) { count += 1; raise "Block invoked more than 1 time!" if count > 1 }
|
data/test/spec_cascade.rb
CHANGED
@@ -17,12 +17,15 @@ describe Rack::Cascade do
|
|
17
17
|
app3 = Rack::URLMap.new("/foo" => lambda { |env|
|
18
18
|
[200, { "Content-Type" => "text/plain"}, [""]]})
|
19
19
|
|
20
|
-
should "dispatch onward on 404 by default" do
|
20
|
+
should "dispatch onward on 404 and 405 by default" do
|
21
21
|
cascade = cascade([app1, app2, app3])
|
22
22
|
Rack::MockRequest.new(cascade).get("/cgi/test").should.be.ok
|
23
23
|
Rack::MockRequest.new(cascade).get("/foo").should.be.ok
|
24
24
|
Rack::MockRequest.new(cascade).get("/toobad").should.be.not_found
|
25
|
-
Rack::MockRequest.new(cascade).get("/cgi/../..").should.be.
|
25
|
+
Rack::MockRequest.new(cascade).get("/cgi/../..").should.be.client_error
|
26
|
+
|
27
|
+
# Put is not allowed by Rack::File so it'll 405.
|
28
|
+
Rack::MockRequest.new(cascade).put("/foo").should.be.ok
|
26
29
|
end
|
27
30
|
|
28
31
|
should "dispatch onward on whatever is passed" do
|
@@ -42,7 +45,7 @@ describe Rack::Cascade do
|
|
42
45
|
Rack::MockRequest.new(cascade).get('/cgi/../bla').should.be.not_found
|
43
46
|
cascade << app1
|
44
47
|
Rack::MockRequest.new(cascade).get('/cgi/test').should.be.ok
|
45
|
-
Rack::MockRequest.new(cascade).get('/cgi/../..').should.be.
|
48
|
+
Rack::MockRequest.new(cascade).get('/cgi/../..').should.be.client_error
|
46
49
|
Rack::MockRequest.new(cascade).get('/foo').should.be.not_found
|
47
50
|
cascade << app3
|
48
51
|
Rack::MockRequest.new(cascade).get('/foo').should.be.ok
|
data/test/spec_file.rb
CHANGED
@@ -64,13 +64,15 @@ describe Rack::File do
|
|
64
64
|
req = Rack::MockRequest.new(Rack::Lint.new(Rack::File.new(DOCROOT)))
|
65
65
|
|
66
66
|
res = req.get("/../README")
|
67
|
-
res.should.be.
|
67
|
+
res.should.be.client_error
|
68
68
|
|
69
69
|
res = req.get("../test")
|
70
|
-
res.should.be.
|
70
|
+
res.should.be.client_error
|
71
71
|
|
72
72
|
res = req.get("..")
|
73
|
-
res.should.be.
|
73
|
+
res.should.be.client_error
|
74
|
+
|
75
|
+
res.should.be.not_found
|
74
76
|
end
|
75
77
|
|
76
78
|
should "allow files with .. in their name" do
|
@@ -89,7 +91,8 @@ describe Rack::File do
|
|
89
91
|
res = Rack::MockRequest.new(Rack::Lint.new(Rack::File.new(DOCROOT))).
|
90
92
|
get("/%2E%2E/README")
|
91
93
|
|
92
|
-
res.should.be.
|
94
|
+
res.should.be.client_error?
|
95
|
+
res.should.be.not_found
|
93
96
|
end
|
94
97
|
|
95
98
|
should "allow safe directory traversal with encoded periods" do
|
@@ -159,7 +162,8 @@ describe Rack::File do
|
|
159
162
|
forbidden.each do |method|
|
160
163
|
|
161
164
|
res = req.send(method, "/cgi/test")
|
162
|
-
res.should.be.
|
165
|
+
res.should.be.client_error
|
166
|
+
res.should.be.method_not_allowed
|
163
167
|
end
|
164
168
|
|
165
169
|
allowed = %w[get head]
|
@@ -169,4 +173,11 @@ describe Rack::File do
|
|
169
173
|
end
|
170
174
|
end
|
171
175
|
|
176
|
+
should "set Content-Length correctly for HEAD requests" do
|
177
|
+
req = Rack::MockRequest.new(Rack::Lint.new(Rack::File.new(DOCROOT)))
|
178
|
+
res = req.head "/cgi/test"
|
179
|
+
res.should.be.successful
|
180
|
+
res['Content-Length'].should.equal "193"
|
181
|
+
end
|
182
|
+
|
172
183
|
end
|
data/test/spec_multipart.rb
CHANGED
@@ -2,11 +2,11 @@ require 'rack/utils'
|
|
2
2
|
require 'rack/mock'
|
3
3
|
|
4
4
|
describe Rack::Multipart do
|
5
|
-
def multipart_fixture(name)
|
5
|
+
def multipart_fixture(name, boundary = "AaB03x")
|
6
6
|
file = multipart_file(name)
|
7
7
|
data = File.open(file, 'rb') { |io| io.read }
|
8
8
|
|
9
|
-
type = "multipart/form-data; boundary
|
9
|
+
type = "multipart/form-data; boundary=#{boundary}"
|
10
10
|
length = data.respond_to?(:bytesize) ? data.bytesize : data.size
|
11
11
|
|
12
12
|
{ "CONTENT_TYPE" => type,
|
@@ -211,6 +211,51 @@ describe Rack::Multipart do
|
|
211
211
|
params["files"][:tempfile].read.should.equal "contents"
|
212
212
|
end
|
213
213
|
|
214
|
+
should "parse filename with unescaped percentage characters" do
|
215
|
+
env = Rack::MockRequest.env_for("/", multipart_fixture(:filename_with_unescaped_percentages, "----WebKitFormBoundary2NHc7OhsgU68l3Al"))
|
216
|
+
params = Rack::Multipart.parse_multipart(env)
|
217
|
+
files = params["document"]["attachment"]
|
218
|
+
files[:type].should.equal "image/jpeg"
|
219
|
+
files[:filename].should.equal "100% of a photo.jpeg"
|
220
|
+
files[:head].should.equal <<-MULTIPART
|
221
|
+
Content-Disposition: form-data; name="document[attachment]"; filename="100% of a photo.jpeg"\r
|
222
|
+
Content-Type: image/jpeg\r
|
223
|
+
MULTIPART
|
224
|
+
|
225
|
+
files[:name].should.equal "document[attachment]"
|
226
|
+
files[:tempfile].read.should.equal "contents"
|
227
|
+
end
|
228
|
+
|
229
|
+
should "parse filename with unescaped percentage characters that look like partial hex escapes" do
|
230
|
+
env = Rack::MockRequest.env_for("/", multipart_fixture(:filename_with_unescaped_percentages2, "----WebKitFormBoundary2NHc7OhsgU68l3Al"))
|
231
|
+
params = Rack::Multipart.parse_multipart(env)
|
232
|
+
files = params["document"]["attachment"]
|
233
|
+
files[:type].should.equal "image/jpeg"
|
234
|
+
files[:filename].should.equal "100%a"
|
235
|
+
files[:head].should.equal <<-MULTIPART
|
236
|
+
Content-Disposition: form-data; name="document[attachment]"; filename="100%a"\r
|
237
|
+
Content-Type: image/jpeg\r
|
238
|
+
MULTIPART
|
239
|
+
|
240
|
+
files[:name].should.equal "document[attachment]"
|
241
|
+
files[:tempfile].read.should.equal "contents"
|
242
|
+
end
|
243
|
+
|
244
|
+
should "parse filename with unescaped percentage characters that look like partial hex escapes" do
|
245
|
+
env = Rack::MockRequest.env_for("/", multipart_fixture(:filename_with_unescaped_percentages3, "----WebKitFormBoundary2NHc7OhsgU68l3Al"))
|
246
|
+
params = Rack::Multipart.parse_multipart(env)
|
247
|
+
files = params["document"]["attachment"]
|
248
|
+
files[:type].should.equal "image/jpeg"
|
249
|
+
files[:filename].should.equal "100%"
|
250
|
+
files[:head].should.equal <<-MULTIPART
|
251
|
+
Content-Disposition: form-data; name="document[attachment]"; filename="100%"\r
|
252
|
+
Content-Type: image/jpeg\r
|
253
|
+
MULTIPART
|
254
|
+
|
255
|
+
files[:name].should.equal "document[attachment]"
|
256
|
+
files[:tempfile].read.should.equal "contents"
|
257
|
+
end
|
258
|
+
|
214
259
|
it "rewinds input after parsing upload" do
|
215
260
|
options = multipart_fixture(:text)
|
216
261
|
input = options[:input]
|
data/test/spec_request.rb
CHANGED
@@ -137,6 +137,19 @@ describe Rack::Request do
|
|
137
137
|
end
|
138
138
|
end
|
139
139
|
|
140
|
+
should "limit the key size per nested params hash" do
|
141
|
+
nested_query = Rack::MockRequest.env_for("/?foo[bar][baz][qux]=1")
|
142
|
+
plain_query = Rack::MockRequest.env_for("/?foo_bar__baz__qux_=1")
|
143
|
+
|
144
|
+
old, Rack::Utils.key_space_limit = Rack::Utils.key_space_limit, 3
|
145
|
+
begin
|
146
|
+
lambda { Rack::Request.new(nested_query).GET }.should.not.raise(RangeError)
|
147
|
+
lambda { Rack::Request.new(plain_query).GET }.should.raise(RangeError)
|
148
|
+
ensure
|
149
|
+
Rack::Utils.key_space_limit = old
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
140
153
|
should "not unify GET and POST when calling params" do
|
141
154
|
mr = Rack::MockRequest.env_for("/?foo=quux",
|
142
155
|
"REQUEST_METHOD" => 'POST',
|
data/test/spec_response.rb
CHANGED
@@ -1,4 +1,3 @@
|
|
1
|
-
require 'set'
|
2
1
|
require 'rack/response'
|
3
2
|
require 'stringio'
|
4
3
|
|
@@ -125,7 +124,6 @@ describe Rack::Response do
|
|
125
124
|
response = Rack::Response.new
|
126
125
|
response.redirect "/foo"
|
127
126
|
status, header, body = response.finish
|
128
|
-
|
129
127
|
status.should.equal 302
|
130
128
|
header["Location"].should.equal "/foo"
|
131
129
|
|
@@ -147,7 +145,12 @@ describe Rack::Response do
|
|
147
145
|
str = ""; body.each { |part| str << part }
|
148
146
|
str.should.equal "foobar"
|
149
147
|
|
150
|
-
|
148
|
+
object_with_each = Object.new
|
149
|
+
def object_with_each.each
|
150
|
+
yield "foo"
|
151
|
+
yield "bar"
|
152
|
+
end
|
153
|
+
r = Rack::Response.new(object_with_each)
|
151
154
|
r.write "foo"
|
152
155
|
status, header, body = r.finish
|
153
156
|
str = ""; body.each { |part| str << part }
|
@@ -218,6 +221,11 @@ describe Rack::Response do
|
|
218
221
|
res.should.be.client_error
|
219
222
|
res.should.be.not_found
|
220
223
|
|
224
|
+
res.status = 405
|
225
|
+
res.should.not.be.successful
|
226
|
+
res.should.be.client_error
|
227
|
+
res.should.be.method_not_allowed
|
228
|
+
|
221
229
|
res.status = 422
|
222
230
|
res.should.not.be.successful
|
223
231
|
res.should.be.client_error
|
data/test/spec_session_cookie.rb
CHANGED
@@ -123,6 +123,10 @@ describe Rack::Session::Cookie do
|
|
123
123
|
res = Rack::MockRequest.new(Rack::Session::Cookie.new(incrementor)).
|
124
124
|
get("/", "HTTP_COOKIE" => "rack.session=blarghfasel")
|
125
125
|
res.body.should.equal '{"counter"=>1}'
|
126
|
+
|
127
|
+
app = Rack::Session::Cookie.new(incrementor, :secret => 'test')
|
128
|
+
res = Rack::MockRequest.new(app).get("/", "HTTP_COOKIE" => "rack.session=")
|
129
|
+
res.body.should.equal '{"counter"=>1}'
|
126
130
|
end
|
127
131
|
|
128
132
|
bigcookie = lambda do |env|
|
@@ -137,7 +141,7 @@ describe Rack::Session::Cookie do
|
|
137
141
|
}.should.raise(Rack::MockRequest::FatalWarning)
|
138
142
|
end
|
139
143
|
|
140
|
-
it "loads from a cookie
|
144
|
+
it "loads from a cookie with integrity hash" do
|
141
145
|
res = Rack::MockRequest.new(Rack::Session::Cookie.new(incrementor, :secret => 'test')).get("/")
|
142
146
|
cookie = res["Set-Cookie"]
|
143
147
|
res = Rack::MockRequest.new(Rack::Session::Cookie.new(incrementor, :secret => 'test')).
|
@@ -147,6 +151,9 @@ describe Rack::Session::Cookie do
|
|
147
151
|
res = Rack::MockRequest.new(Rack::Session::Cookie.new(incrementor, :secret => 'test')).
|
148
152
|
get("/", "HTTP_COOKIE" => cookie)
|
149
153
|
res.body.should.equal '{"counter"=>3}'
|
154
|
+
res = Rack::MockRequest.new(Rack::Session::Cookie.new(incrementor, :secret => 'other')).
|
155
|
+
get("/", "HTTP_COOKIE" => cookie)
|
156
|
+
res.body.should.equal '{"counter"=>1}'
|
150
157
|
end
|
151
158
|
|
152
159
|
it "loads from a cookie wih accept-only integrity hash for graceful key rotation" do
|
@@ -165,16 +172,31 @@ describe Rack::Session::Cookie do
|
|
165
172
|
app = Rack::Session::Cookie.new(incrementor, :secret => 'test')
|
166
173
|
response1 = Rack::MockRequest.new(app).get("/")
|
167
174
|
response1.body.should.equal '{"counter"=>1}'
|
175
|
+
response1 = Rack::MockRequest.new(app).get("/", "HTTP_COOKIE" => response1["Set-Cookie"])
|
176
|
+
response1.body.should.equal '{"counter"=>2}'
|
168
177
|
|
169
178
|
_, digest = response1["Set-Cookie"].split("--")
|
170
179
|
tampered_with_cookie = "hackerman-was-here" + "--" + digest
|
171
180
|
response2 = Rack::MockRequest.new(app).get("/", "HTTP_COOKIE" =>
|
172
181
|
tampered_with_cookie)
|
173
182
|
|
174
|
-
#
|
183
|
+
# Tampered cookie was ignored. Counter is back to 1.
|
175
184
|
response2.body.should.equal '{"counter"=>1}'
|
176
185
|
end
|
177
186
|
|
187
|
+
it "supports either of secret or old_secret" do
|
188
|
+
app = Rack::Session::Cookie.new(incrementor, :secret => 'test')
|
189
|
+
res = Rack::MockRequest.new(app).get("/")
|
190
|
+
res.body.should.equal '{"counter"=>1}'
|
191
|
+
res = Rack::MockRequest.new(app).get("/", "HTTP_COOKIE" => res["Set-Cookie"])
|
192
|
+
res.body.should.equal '{"counter"=>2}'
|
193
|
+
app = Rack::Session::Cookie.new(incrementor, :old_secret => 'test')
|
194
|
+
res = Rack::MockRequest.new(app).get("/")
|
195
|
+
res.body.should.equal '{"counter"=>1}'
|
196
|
+
res = Rack::MockRequest.new(app).get("/", "HTTP_COOKIE" => res["Set-Cookie"])
|
197
|
+
res.body.should.equal '{"counter"=>2}'
|
198
|
+
end
|
199
|
+
|
178
200
|
describe "1.9 bugs relating to inspecting yet-to-be-loaded from cookie data: Rack::Session::Abstract::SessionHash" do
|
179
201
|
|
180
202
|
it "can handle Rack::Lint middleware" do
|
@@ -225,6 +247,7 @@ describe Rack::Session::Cookie do
|
|
225
247
|
|
226
248
|
res = Rack::MockRequest.new(app).get("/", "HTTPS" => "on")
|
227
249
|
res["Set-Cookie"].should.not.be.nil
|
250
|
+
res["Set-Cookie"].should.match(/secure/)
|
228
251
|
end
|
229
252
|
|
230
253
|
it "does not return a cookie if cookie was not read/written" do
|
data/test/spec_static.rb
CHANGED
@@ -40,6 +40,11 @@ describe Rack::Static do
|
|
40
40
|
res.should.be.ok
|
41
41
|
res.body.should =~ /index!/
|
42
42
|
end
|
43
|
+
|
44
|
+
it "doesn't call index file if :index option was omitted" do
|
45
|
+
res = @request.get("/")
|
46
|
+
res.body.should == "Hello World"
|
47
|
+
end
|
43
48
|
|
44
49
|
it "serves hidden files" do
|
45
50
|
res = @hash_request.get("/cgi/sekret")
|
data/test/spec_utils.rb
CHANGED
@@ -3,6 +3,15 @@ require 'rack/utils'
|
|
3
3
|
require 'rack/mock'
|
4
4
|
|
5
5
|
describe Rack::Utils do
|
6
|
+
|
7
|
+
# A helper method which checks
|
8
|
+
# if certain query parameters
|
9
|
+
# are equal.
|
10
|
+
def equal_query_to(query)
|
11
|
+
parts = query.split('&')
|
12
|
+
lambda{|other| (parts & other.split('&')) == parts }
|
13
|
+
end
|
14
|
+
|
6
15
|
def kcodeu
|
7
16
|
one8 = RUBY_VERSION.to_f < 1.9
|
8
17
|
default_kcode, $KCODE = $KCODE, 'U' if one8
|
@@ -179,7 +188,7 @@ describe Rack::Utils do
|
|
179
188
|
|
180
189
|
lambda { Rack::Utils.parse_nested_query("x[y]=1&x[]=1") }.
|
181
190
|
should.raise(TypeError).
|
182
|
-
message.should.
|
191
|
+
message.should.match /expected Array \(got [^)]*\) for param `x'/
|
183
192
|
|
184
193
|
lambda { Rack::Utils.parse_nested_query("x[y]=1&x[y][][w]=2") }.
|
185
194
|
should.raise(TypeError).
|
@@ -187,13 +196,13 @@ describe Rack::Utils do
|
|
187
196
|
end
|
188
197
|
|
189
198
|
should "build query strings correctly" do
|
190
|
-
Rack::Utils.build_query("foo" => "bar").should.
|
199
|
+
Rack::Utils.build_query("foo" => "bar").should.be equal_query_to("foo=bar")
|
191
200
|
Rack::Utils.build_query("foo" => ["bar", "quux"]).
|
192
|
-
should.
|
201
|
+
should.be equal_query_to("foo=bar&foo=quux")
|
193
202
|
Rack::Utils.build_query("foo" => "1", "bar" => "2").
|
194
|
-
should.
|
203
|
+
should.be equal_query_to("foo=1&bar=2")
|
195
204
|
Rack::Utils.build_query("my weird field" => "q1!2\"'w$5&7/z8)?").
|
196
|
-
should.
|
205
|
+
should.be equal_query_to("my+weird+field=q1%212%22%27w%245%267%2Fz8%29%3F")
|
197
206
|
end
|
198
207
|
|
199
208
|
should "build nested query strings correctly" do
|
@@ -202,9 +211,9 @@ describe Rack::Utils do
|
|
202
211
|
Rack::Utils.build_nested_query("foo" => "bar").should.equal "foo=bar"
|
203
212
|
|
204
213
|
Rack::Utils.build_nested_query("foo" => "1", "bar" => "2").
|
205
|
-
should.
|
214
|
+
should.be equal_query_to("foo=1&bar=2")
|
206
215
|
Rack::Utils.build_nested_query("my weird field" => "q1!2\"'w$5&7/z8)?").
|
207
|
-
should.
|
216
|
+
should.be equal_query_to("my+weird+field=q1%212%22%27w%245%267%2Fz8%29%3F")
|
208
217
|
|
209
218
|
Rack::Utils.build_nested_query("foo" => [nil]).
|
210
219
|
should.equal "foo[]"
|
metadata
CHANGED
@@ -1,13 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: rack
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
hash:
|
4
|
+
hash: 5
|
5
5
|
prerelease:
|
6
6
|
segments:
|
7
7
|
- 1
|
8
8
|
- 4
|
9
|
-
-
|
10
|
-
version: 1.4.
|
9
|
+
- 1
|
10
|
+
version: 1.4.1
|
11
11
|
platform: ruby
|
12
12
|
authors:
|
13
13
|
- Christian Neukirchen
|
@@ -15,7 +15,7 @@ autorequire:
|
|
15
15
|
bindir: bin
|
16
16
|
cert_chain: []
|
17
17
|
|
18
|
-
date:
|
18
|
+
date: 2012-01-23 00:00:00 Z
|
19
19
|
dependencies:
|
20
20
|
- !ruby/object:Gem::Dependency
|
21
21
|
name: bacon
|
@@ -213,6 +213,9 @@ files:
|
|
213
213
|
- test/multipart/filename_with_escaped_quotes
|
214
214
|
- test/multipart/filename_with_escaped_quotes_and_modification_param
|
215
215
|
- test/multipart/filename_with_percent_escaped_quotes
|
216
|
+
- test/multipart/filename_with_unescaped_percentages
|
217
|
+
- test/multipart/filename_with_unescaped_percentages2
|
218
|
+
- test/multipart/filename_with_unescaped_percentages3
|
216
219
|
- test/multipart/filename_with_unescaped_quotes
|
217
220
|
- test/multipart/ie
|
218
221
|
- test/multipart/mixed_files
|