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.

@@ -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
@@ -20,7 +20,7 @@ module Rack
20
20
 
21
21
  # Return the Rack release as a dotted string.
22
22
  def self.release
23
- "1.3"
23
+ "1.4"
24
24
  end
25
25
 
26
26
  autoload :Builder, "rack/builder"
@@ -11,8 +11,11 @@ module Rack
11
11
  def close
12
12
  return if @closed
13
13
  @closed = true
14
- @body.close if @body.respond_to? :close
15
- @block.call
14
+ begin
15
+ @body.close if @body.respond_to? :close
16
+ ensure
17
+ @block.call
18
+ end
16
19
  end
17
20
 
18
21
  def closed?
@@ -8,7 +8,7 @@ module Rack
8
8
 
9
9
  attr_reader :apps
10
10
 
11
- def initialize(apps, catch=404)
11
+ def initialize(apps, catch=[404, 405])
12
12
  @apps = []; @has_app = {}
13
13
  apps.each { |app| add app }
14
14
 
@@ -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(403, "Forbidden")
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(403, "Forbidden") if depth - 1 < 0
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 = Utils.unescape(filename).gsub(/\\(.)/, '\1')
132
+ filename = filename.gsub(/\\(.)/, '\1')
140
133
  end
141
134
  filename
142
135
  end
@@ -177,7 +177,7 @@ module Rack
177
177
  PARSEABLE_DATA_MEDIA_TYPES.include?(media_type)
178
178
  end
179
179
 
180
- # Returns the data recieved in the query string.
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 recieved in the request body.
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.
@@ -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 syncronous with the Rack response.
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?; 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 unprocessable?; status == 422; end
128
-
129
- def redirect?; [301, 302, 303, 307].include? status; end
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) && !@session_id_loaded
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 => begin ::SecureRandom rescue false end
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 = options[:key] || "rack.session"
194
+ @key = @default_options.delete(:key)
195
195
  @cookie_only = @default_options.delete(:cookie_only)
196
196
  initialize_sid
197
197
  end
@@ -81,8 +81,7 @@ module Rack
81
81
  attr_reader :coder
82
82
 
83
83
  def initialize(app, options={})
84
- @secret = options[:secret]
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 (@secret || @old_secret) && session_data
106
+ if @secrets.size > 0 && session_data
108
107
  session_data, digest = session_data.split("--")
109
- if (digest != generate_hmac(session_data, @secret)) && (digest != generate_hmac(session_data, @old_secret))
110
- session_data = nil
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 @secret
135
- session_data = "#{session_data}--#{generate_hmac(session_data, @secret)}"
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)
@@ -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 the app it wraps and
7
- # replaces them with a site explaining the error.
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
@@ -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] || "index.html"
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)
@@ -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.is_a?(Hash) && !params[k].last.key?(child_key)
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({}, child_key, v)
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].is_a?(Hash)
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 | \
@@ -1,6 +1,6 @@
1
1
  Gem::Specification.new do |s|
2
2
  s.name = "rack"
3
- s.version = "1.4.0"
3
+ s.version = "1.4.1"
4
4
  s.platform = Gem::Platform::RUBY
5
5
  s.summary = "a modular Ruby webserver interface"
6
6
 
@@ -0,0 +1,6 @@
1
+ ------WebKitFormBoundary2NHc7OhsgU68l3Al
2
+ Content-Disposition: form-data; name="document[attachment]"; filename="100% of a photo.jpeg"
3
+ Content-Type: image/jpeg
4
+
5
+ contents
6
+ ------WebKitFormBoundary2NHc7OhsgU68l3Al--
@@ -0,0 +1,6 @@
1
+ ------WebKitFormBoundary2NHc7OhsgU68l3Al
2
+ Content-Disposition: form-data; name="document[attachment]"; filename="100%a"
3
+ Content-Type: image/jpeg
4
+
5
+ contents
6
+ ------WebKitFormBoundary2NHc7OhsgU68l3Al--
@@ -0,0 +1,6 @@
1
+ ------WebKitFormBoundary2NHc7OhsgU68l3Al
2
+ Content-Disposition: form-data; name="document[attachment]"; filename="100%"
3
+ Content-Type: image/jpeg
4
+
5
+ contents
6
+ ------WebKitFormBoundary2NHc7OhsgU68l3Al--
@@ -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 }
@@ -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.forbidden
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.forbidden
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
@@ -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.forbidden
67
+ res.should.be.client_error
68
68
 
69
69
  res = req.get("../test")
70
- res.should.be.forbidden
70
+ res.should.be.client_error
71
71
 
72
72
  res = req.get("..")
73
- res.should.be.forbidden
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.forbidden
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.forbidden
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
@@ -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=AaB03x"
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]
@@ -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',
@@ -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
- r = Rack::Response.new(["foo", "bar"].to_set)
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
@@ -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 wih integrity hash" do
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
- # Tampared cookie was ignored. Counter is back to 1.
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
@@ -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")
@@ -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.equal "expected Array (got Hash) for param `x'"
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.equal "foo=bar"
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.equal "foo=bar&foo=quux"
201
+ should.be equal_query_to("foo=bar&foo=quux")
193
202
  Rack::Utils.build_query("foo" => "1", "bar" => "2").
194
- should.equal "foo=1&bar=2"
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.equal "my+weird+field=q1%212%22%27w%245%267%2Fz8%29%3F"
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.equal "foo=1&bar=2"
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.equal "my+weird+field=q1%212%22%27w%245%267%2Fz8%29%3F"
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: 7
4
+ hash: 5
5
5
  prerelease:
6
6
  segments:
7
7
  - 1
8
8
  - 4
9
- - 0
10
- version: 1.4.0
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: 2011-12-28 00:00:00 Z
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