rack 1.5.5 → 1.6.0.beta

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of rack might be problematic. Click here for more details.

Files changed (79) hide show
  1. checksums.yaml +4 -4
  2. data/KNOWN-ISSUES +14 -0
  3. data/README.rdoc +10 -6
  4. data/Rakefile +3 -4
  5. data/SPEC +59 -23
  6. data/lib/rack.rb +2 -1
  7. data/lib/rack/auth/abstract/request.rb +1 -1
  8. data/lib/rack/auth/basic.rb +1 -1
  9. data/lib/rack/auth/digest/md5.rb +1 -1
  10. data/lib/rack/backports/uri/common_18.rb +1 -1
  11. data/lib/rack/builder.rb +19 -4
  12. data/lib/rack/cascade.rb +2 -2
  13. data/lib/rack/chunked.rb +12 -1
  14. data/lib/rack/commonlogger.rb +13 -5
  15. data/lib/rack/conditionalget.rb +14 -2
  16. data/lib/rack/content_length.rb +5 -1
  17. data/lib/rack/deflater.rb +52 -13
  18. data/lib/rack/directory.rb +8 -2
  19. data/lib/rack/etag.rb +14 -6
  20. data/lib/rack/file.rb +10 -14
  21. data/lib/rack/handler.rb +2 -0
  22. data/lib/rack/handler/fastcgi.rb +4 -1
  23. data/lib/rack/handler/mongrel.rb +8 -2
  24. data/lib/rack/handler/scgi.rb +4 -1
  25. data/lib/rack/handler/thin.rb +8 -2
  26. data/lib/rack/handler/webrick.rb +46 -6
  27. data/lib/rack/head.rb +7 -2
  28. data/lib/rack/lint.rb +73 -25
  29. data/lib/rack/lobster.rb +8 -3
  30. data/lib/rack/methodoverride.rb +14 -3
  31. data/lib/rack/mime.rb +1 -15
  32. data/lib/rack/mock.rb +15 -7
  33. data/lib/rack/multipart.rb +2 -2
  34. data/lib/rack/multipart/parser.rb +107 -53
  35. data/lib/rack/multipart/uploaded_file.rb +2 -2
  36. data/lib/rack/nulllogger.rb +21 -2
  37. data/lib/rack/request.rb +38 -24
  38. data/lib/rack/response.rb +5 -0
  39. data/lib/rack/sendfile.rb +10 -5
  40. data/lib/rack/server.rb +45 -17
  41. data/lib/rack/session/abstract/id.rb +7 -6
  42. data/lib/rack/session/cookie.rb +17 -7
  43. data/lib/rack/session/memcache.rb +4 -4
  44. data/lib/rack/session/pool.rb +3 -6
  45. data/lib/rack/showexceptions.rb +20 -11
  46. data/lib/rack/showstatus.rb +1 -1
  47. data/lib/rack/static.rb +27 -30
  48. data/lib/rack/tempfile_reaper.rb +22 -0
  49. data/lib/rack/urlmap.rb +17 -3
  50. data/lib/rack/utils.rb +78 -47
  51. data/lib/rack/utils/okjson.rb +90 -91
  52. data/rack.gemspec +3 -3
  53. data/test/multipart/filename_and_no_name +6 -0
  54. data/test/multipart/invalid_character +6 -0
  55. data/test/spec_builder.rb +13 -4
  56. data/test/spec_chunked.rb +16 -0
  57. data/test/spec_commonlogger.rb +36 -0
  58. data/test/spec_content_length.rb +3 -1
  59. data/test/spec_deflater.rb +283 -148
  60. data/test/spec_etag.rb +11 -2
  61. data/test/spec_file.rb +11 -3
  62. data/test/spec_head.rb +2 -0
  63. data/test/spec_lobster.rb +1 -1
  64. data/test/spec_mock.rb +8 -0
  65. data/test/spec_multipart.rb +111 -49
  66. data/test/spec_request.rb +109 -25
  67. data/test/spec_response.rb +30 -0
  68. data/test/spec_server.rb +20 -5
  69. data/test/spec_session_cookie.rb +45 -2
  70. data/test/spec_session_memcache.rb +1 -1
  71. data/test/spec_showexceptions.rb +29 -36
  72. data/test/spec_showstatus.rb +19 -0
  73. data/test/spec_tempfile_reaper.rb +63 -0
  74. data/test/spec_urlmap.rb +23 -0
  75. data/test/spec_utils.rb +60 -10
  76. data/test/spec_webrick.rb +41 -0
  77. metadata +12 -9
  78. data/test/cgi/lighttpd.errors +0 -1
  79. data/test/multipart/three_files_three_fields +0 -31
@@ -21,13 +21,13 @@ describe Rack::ETag do
21
21
  should "set ETag if none is set if status is 200" do
22
22
  app = lambda { |env| [200, {'Content-Type' => 'text/plain'}, ["Hello, World!"]] }
23
23
  response = etag(app).call(request)
24
- response[1]['ETag'].should.equal "\"65a8e27d8879283831b664bd8b7f0ad4\""
24
+ response[1]['ETag'].should.equal "W/\"65a8e27d8879283831b664bd8b7f0ad4\""
25
25
  end
26
26
 
27
27
  should "set ETag if none is set if status is 201" do
28
28
  app = lambda { |env| [201, {'Content-Type' => 'text/plain'}, ["Hello, World!"]] }
29
29
  response = etag(app).call(request)
30
- response[1]['ETag'].should.equal "\"65a8e27d8879283831b664bd8b7f0ad4\""
30
+ response[1]['ETag'].should.equal "W/\"65a8e27d8879283831b664bd8b7f0ad4\""
31
31
  end
32
32
 
33
33
  should "set Cache-Control to 'max-age=0, private, must-revalidate' (default) if none is set" do
@@ -95,4 +95,13 @@ describe Rack::ETag do
95
95
  response = etag(app).call(request)
96
96
  response[1]['ETag'].should.be.nil
97
97
  end
98
+
99
+ should "close the original body" do
100
+ body = StringIO.new
101
+ app = lambda { |env| [200, {}, body] }
102
+ response = etag(app).call(request)
103
+ body.should.not.be.closed
104
+ response[2].close
105
+ body.should.be.closed
106
+ end
98
107
  end
@@ -164,24 +164,32 @@ describe Rack::File do
164
164
  heads['Access-Control-Allow-Origin'].should.equal nil
165
165
  end
166
166
 
167
- should "only support GET and HEAD requests" do
167
+ should "only support GET, HEAD, and OPTIONS requests" do
168
168
  req = Rack::MockRequest.new(file(DOCROOT))
169
169
 
170
170
  forbidden = %w[post put patch delete]
171
171
  forbidden.each do |method|
172
-
173
172
  res = req.send(method, "/cgi/test")
174
173
  res.should.be.client_error
175
174
  res.should.be.method_not_allowed
175
+ res.headers['Allow'].split(/, */).sort.should == %w(GET HEAD OPTIONS)
176
176
  end
177
177
 
178
- allowed = %w[get head]
178
+ allowed = %w[get head options]
179
179
  allowed.each do |method|
180
180
  res = req.send(method, "/cgi/test")
181
181
  res.should.be.successful
182
182
  end
183
183
  end
184
184
 
185
+ should "set Allow correctly for OPTIONS requests" do
186
+ req = Rack::MockRequest.new(file(DOCROOT))
187
+ res = req.options('/cgi/test')
188
+ res.should.be.successful
189
+ res.headers['Allow'].should.not.equal nil
190
+ res.headers['Allow'].split(/, */).sort.should == %w(GET HEAD OPTIONS)
191
+ end
192
+
185
193
  should "set Content-Length correctly for HEAD requests" do
186
194
  req = Rack::MockRequest.new(Rack::Lint.new(Rack::File.new(DOCROOT)))
187
195
  res = req.head "/cgi/test"
@@ -38,6 +38,8 @@ describe Rack::Head do
38
38
  resp[0].should.equal(200)
39
39
  resp[1].should.equal({"Content-type" => "test/plain", "Content-length" => "3"})
40
40
  resp[2].to_enum.to_a.should.equal([])
41
+ body.should.not.be.closed
42
+ resp[2].close
41
43
  body.should.be.closed
42
44
  end
43
45
  end
@@ -47,7 +47,7 @@ describe Rack::Lobster do
47
47
  should "be flippable" do
48
48
  res = lobster.get("/?flip=left")
49
49
  res.should.be.ok
50
- res.body.should.include "(,,,(,,(,("
50
+ res.body.should.include "),,,),,),)"
51
51
  end
52
52
 
53
53
  should "provide crashing for testing purposes" do
@@ -30,6 +30,14 @@ describe Rack::MockRequest do
30
30
  env.should.include "rack.version"
31
31
  end
32
32
 
33
+ should "return an environment with a path" do
34
+ env = Rack::MockRequest.env_for("http://www.example.com/parse?location[]=1&location[]=2&age_group[]=2")
35
+ env["QUERY_STRING"].should.equal "location[]=1&location[]=2&age_group[]=2"
36
+ env["PATH_INFO"].should.equal "/parse"
37
+ env.should.be.kind_of Hash
38
+ env.should.include "rack.version"
39
+ end
40
+
33
41
  should "provide sensible defaults" do
34
42
  res = Rack::MockRequest.new(app).request
35
43
 
@@ -30,6 +30,44 @@ describe Rack::Multipart do
30
30
  params["text"].should.equal "contents"
31
31
  end
32
32
 
33
+ if "<3".respond_to?(:force_encoding)
34
+ should "set US_ASCII encoding based on charset" do
35
+ env = Rack::MockRequest.env_for("/", multipart_fixture(:content_type_and_no_filename))
36
+ params = Rack::Multipart.parse_multipart(env)
37
+ params["text"].encoding.should.equal Encoding::US_ASCII
38
+
39
+ # I'm not 100% sure if making the param name encoding match the
40
+ # Content-Type charset is the right thing to do. We should revisit this.
41
+ params.keys.each do |key|
42
+ key.encoding.should.equal Encoding::US_ASCII
43
+ end
44
+ end
45
+
46
+ should "set BINARY encoding on things without content type" do
47
+ env = Rack::MockRequest.env_for("/", multipart_fixture(:none))
48
+ params = Rack::Multipart.parse_multipart(env)
49
+ params["submit-name"].encoding.should.equal Encoding::UTF_8
50
+ end
51
+
52
+ should "set UTF8 encoding on names of things without content type" do
53
+ env = Rack::MockRequest.env_for("/", multipart_fixture(:none))
54
+ params = Rack::Multipart.parse_multipart(env)
55
+ params.keys.each do |key|
56
+ key.encoding.should.equal Encoding::UTF_8
57
+ end
58
+ end
59
+
60
+ should "default text to UTF8" do
61
+ env = Rack::MockRequest.env_for("/", multipart_fixture(:text))
62
+ params = Rack::Multipart.parse_multipart(env)
63
+ params['submit-name'].encoding.should.equal Encoding::UTF_8
64
+ params['submit-name-with-content'].encoding.should.equal Encoding::UTF_8
65
+ params.keys.each do |key|
66
+ key.encoding.should.equal Encoding::UTF_8
67
+ end
68
+ end
69
+ end
70
+
33
71
  should "raise RangeError if the key space is exhausted" do
34
72
  env = Rack::MockRequest.env_for("/", multipart_fixture(:content_type_and_no_filename))
35
73
 
@@ -115,6 +153,18 @@ describe Rack::Multipart do
115
153
  params["files"][:tempfile].read.should.equal "contents"
116
154
  end
117
155
 
156
+ should "parse multipart upload with text file with no name field" do
157
+ env = Rack::MockRequest.env_for("/", multipart_fixture(:filename_and_no_name))
158
+ params = Rack::Multipart.parse_multipart(env)
159
+ params["file1.txt"][:type].should.equal "text/plain"
160
+ params["file1.txt"][:filename].should.equal "file1.txt"
161
+ params["file1.txt"][:head].should.equal "Content-Disposition: form-data; " +
162
+ "filename=\"file1.txt\"\r\n" +
163
+ "Content-Type: text/plain\r\n"
164
+ params["file1.txt"][:name].should.equal "file1.txt"
165
+ params["file1.txt"][:tempfile].read.should.equal "contents"
166
+ end
167
+
118
168
  should "parse multipart upload with nested parameters" do
119
169
  env = Rack::MockRequest.env_for("/", multipart_fixture(:nested))
120
170
  params = Rack::Multipart.parse_multipart(env)
@@ -166,6 +216,20 @@ describe Rack::Multipart do
166
216
  params["files"][:tempfile].read.should.equal "contents"
167
217
  end
168
218
 
219
+ should "parse multipart upload with filename with invalid characters" do
220
+ env = Rack::MockRequest.env_for("/", multipart_fixture(:invalid_character))
221
+ params = Rack::Multipart.parse_multipart(env)
222
+ params["files"][:type].should.equal "text/plain"
223
+ params["files"][:filename].should.match(/invalid/)
224
+ head = "Content-Disposition: form-data; " +
225
+ "name=\"files\"; filename=\"invalid\xC3.txt\"\r\n" +
226
+ "Content-Type: text/plain\r\n"
227
+ head = head.force_encoding("ASCII-8BIT") if head.respond_to?(:force_encoding)
228
+ params["files"][:head].should.equal head
229
+ params["files"][:name].should.equal "files"
230
+ params["files"][:tempfile].read.should.equal "contents"
231
+ end
232
+
169
233
  should "not include file params if no file was selected" do
170
234
  env = Rack::MockRequest.env_for("/", multipart_fixture(:none))
171
235
  params = Rack::Multipart.parse_multipart(env)
@@ -182,6 +246,14 @@ describe Rack::Multipart do
182
246
  params["files"].size.should.equal 252
183
247
  end
184
248
 
249
+ should "parse multipart/mixed" do
250
+ env = Rack::MockRequest.env_for("/", multipart_fixture(:mixed_files))
251
+ params = Rack::Utils::Multipart.parse_multipart(env)
252
+ params["foo"].should.equal "bar"
253
+ params["files"].should.be.instance_of String
254
+ params["files"].size.should.equal 252
255
+ end
256
+
185
257
  should "parse IE multipart upload and clean up filename" do
186
258
  env = Rack::MockRequest.env_for("/", multipart_fixture(:ie))
187
259
  params = Rack::Multipart.parse_multipart(env)
@@ -364,56 +436,22 @@ Content-Type: image/jpeg\r
364
436
  end
365
437
 
366
438
  it "builds complete params with the chunk size of 16384 slicing exactly on boundary" do
367
- begin
368
- previous_limit = Rack::Utils.multipart_part_limit
369
- Rack::Utils.multipart_part_limit = 256
370
-
371
- data = File.open(multipart_file("fail_16384_nofile"), 'rb') { |f| f.read }.gsub(/\n/, "\r\n")
372
- options = {
373
- "CONTENT_TYPE" => "multipart/form-data; boundary=----WebKitFormBoundaryWsY0GnpbI5U7ztzo",
374
- "CONTENT_LENGTH" => data.length.to_s,
375
- :input => StringIO.new(data)
376
- }
377
- env = Rack::MockRequest.env_for("/", options)
378
- params = Rack::Multipart.parse_multipart(env)
379
-
380
- params.should.not.equal nil
381
- params.keys.should.include "AAAAAAAAAAAAAAAAAAA"
382
- params["AAAAAAAAAAAAAAAAAAA"].keys.should.include "PLAPLAPLA_MEMMEMMEMM_ATTRATTRER"
383
- params["AAAAAAAAAAAAAAAAAAA"]["PLAPLAPLA_MEMMEMMEMM_ATTRATTRER"].keys.should.include "new"
384
- params["AAAAAAAAAAAAAAAAAAA"]["PLAPLAPLA_MEMMEMMEMM_ATTRATTRER"]["new"].keys.should.include "-2"
385
- params["AAAAAAAAAAAAAAAAAAA"]["PLAPLAPLA_MEMMEMMEMM_ATTRATTRER"]["new"]["-2"].keys.should.include "ba_unit_id"
386
- params["AAAAAAAAAAAAAAAAAAA"]["PLAPLAPLA_MEMMEMMEMM_ATTRATTRER"]["new"]["-2"]["ba_unit_id"].should.equal "1017"
387
- ensure
388
- Rack::Utils.multipart_part_limit = previous_limit
389
- end
390
- end
391
-
392
- should "not reach a multi-part limit" do
393
- begin
394
- previous_limit = Rack::Utils.multipart_part_limit
395
- Rack::Utils.multipart_part_limit = 4
396
-
397
- env = Rack::MockRequest.env_for '/', multipart_fixture(:three_files_three_fields)
398
- params = Rack::Multipart.parse_multipart(env)
399
- params['reply'].should.equal 'yes'
400
- params['to'].should.equal 'people'
401
- params['from'].should.equal 'others'
402
- ensure
403
- Rack::Utils.multipart_part_limit = previous_limit
404
- end
405
- end
406
-
407
- should "reach a multipart limit" do
408
- begin
409
- previous_limit = Rack::Utils.multipart_part_limit
410
- Rack::Utils.multipart_part_limit = 3
439
+ data = File.open(multipart_file("fail_16384_nofile"), 'rb') { |f| f.read }.gsub(/\n/, "\r\n")
440
+ options = {
441
+ "CONTENT_TYPE" => "multipart/form-data; boundary=----WebKitFormBoundaryWsY0GnpbI5U7ztzo",
442
+ "CONTENT_LENGTH" => data.length.to_s,
443
+ :input => StringIO.new(data)
444
+ }
445
+ env = Rack::MockRequest.env_for("/", options)
446
+ params = Rack::Multipart.parse_multipart(env)
411
447
 
412
- env = Rack::MockRequest.env_for '/', multipart_fixture(:three_files_three_fields)
413
- lambda { Rack::Multipart.parse_multipart(env) }.should.raise(Rack::Multipart::MultipartLimitError)
414
- ensure
415
- Rack::Utils.multipart_part_limit = previous_limit
416
- end
448
+ params.should.not.equal nil
449
+ params.keys.should.include "AAAAAAAAAAAAAAAAAAA"
450
+ params["AAAAAAAAAAAAAAAAAAA"].keys.should.include "PLAPLAPLA_MEMMEMMEMM_ATTRATTRER"
451
+ params["AAAAAAAAAAAAAAAAAAA"]["PLAPLAPLA_MEMMEMMEMM_ATTRATTRER"].keys.should.include "new"
452
+ params["AAAAAAAAAAAAAAAAAAA"]["PLAPLAPLA_MEMMEMMEMM_ATTRATTRER"]["new"].keys.should.include "-2"
453
+ params["AAAAAAAAAAAAAAAAAAA"]["PLAPLAPLA_MEMMEMMEMM_ATTRATTRER"]["new"]["-2"].keys.should.include "ba_unit_id"
454
+ params["AAAAAAAAAAAAAAAAAAA"]["PLAPLAPLA_MEMMEMMEMM_ATTRATTRER"]["new"]["-2"]["ba_unit_id"].should.equal "1017"
417
455
  end
418
456
 
419
457
  should "return nil if no UploadedFiles were used" do
@@ -476,4 +514,28 @@ contents\r
476
514
  params["file"][:filename].should.equal('long' * 100)
477
515
  end
478
516
 
517
+ should "support mixed case metadata" do
518
+ file = multipart_file(:text)
519
+ data = File.open(file, 'rb') { |io| io.read }
520
+
521
+ type = "Multipart/Form-Data; Boundary=AaB03x"
522
+ length = data.respond_to?(:bytesize) ? data.bytesize : data.size
523
+
524
+ e = { "CONTENT_TYPE" => type,
525
+ "CONTENT_LENGTH" => length.to_s,
526
+ :input => StringIO.new(data) }
527
+
528
+ env = Rack::MockRequest.env_for("/", e)
529
+ params = Rack::Multipart.parse_multipart(env)
530
+ params["submit-name"].should.equal "Larry"
531
+ params["submit-name-with-content"].should.equal "Berry"
532
+ params["files"][:type].should.equal "text/plain"
533
+ params["files"][:filename].should.equal "file1.txt"
534
+ params["files"][:head].should.equal "Content-Disposition: form-data; " +
535
+ "name=\"files\"; filename=\"file1.txt\"\r\n" +
536
+ "Content-Type: text/plain\r\n"
537
+ params["files"][:name].should.equal "files"
538
+ params["files"][:tempfile].read.should.equal "contents"
539
+ end
540
+
479
541
  end
@@ -2,7 +2,6 @@ require 'stringio'
2
2
  require 'cgi'
3
3
  require 'rack/request'
4
4
  require 'rack/mock'
5
- require 'securerandom'
6
5
 
7
6
  describe Rack::Request do
8
7
  should "wrap the rack variables" do
@@ -97,7 +96,10 @@ describe Rack::Request do
97
96
 
98
97
  req = Rack::Request.new \
99
98
  Rack::MockRequest.env_for("/", "HTTP_HOST" => "localhost", "HTTP_X_FORWARDED_PROTO" => "https", "SERVER_PORT" => "80")
99
+ req.port.should.equal 443
100
100
 
101
+ req = Rack::Request.new \
102
+ Rack::MockRequest.env_for("/", "HTTP_HOST" => "localhost", "HTTP_X_FORWARDED_PROTO" => "https,https", "SERVER_PORT" => "80")
101
103
  req.port.should.equal 443
102
104
  end
103
105
 
@@ -131,6 +133,14 @@ describe Rack::Request do
131
133
  req.params.should.equal "foo" => "bar", "quux" => "bla"
132
134
  end
133
135
 
136
+ should "not truncate query strings containing semi-colons #543" do
137
+ req = Rack::Request.new(Rack::MockRequest.env_for("/?foo=bar&quux=b;la"))
138
+ req.query_string.should.equal "foo=bar&quux=b;la"
139
+ req.GET.should.equal "foo" => "bar", "quux" => "b;la"
140
+ req.POST.should.be.empty
141
+ req.params.should.equal "foo" => "bar", "quux" => "b;la"
142
+ end
143
+
134
144
  should "limit the keys from the GET query string" do
135
145
  env = Rack::MockRequest.env_for("/?foo=bar")
136
146
 
@@ -144,7 +154,7 @@ describe Rack::Request do
144
154
  end
145
155
 
146
156
  should "limit the key size per nested params hash" do
147
- nested_query = Rack::MockRequest.env_for("/?foo[bar][baz][qux]=1")
157
+ nested_query = Rack::MockRequest.env_for("/?foo%5Bbar%5D%5Bbaz%5D%5Bqux%5D=1")
148
158
  plain_query = Rack::MockRequest.env_for("/?foo_bar__baz__qux_=1")
149
159
 
150
160
  old, Rack::Utils.key_space_limit = Rack::Utils.key_space_limit, 3
@@ -170,6 +180,18 @@ describe Rack::Request do
170
180
  req.params.should.equal req.GET.merge(req.POST)
171
181
  end
172
182
 
183
+ should "raise if input params has invalid %-encoding" do
184
+ mr = Rack::MockRequest.env_for("/?foo=quux",
185
+ "REQUEST_METHOD" => 'POST',
186
+ :input => "a%=1"
187
+ )
188
+ req = Rack::Request.new mr
189
+
190
+ lambda { req.POST }.
191
+ should.raise(Rack::Utils::InvalidParameterError).
192
+ message.should.equal "invalid %-encoding (a%)"
193
+ end
194
+
173
195
  should "raise if rack.input is missing" do
174
196
  req = Rack::Request.new({})
175
197
  lambda { req.POST }.should.raise(RuntimeError)
@@ -604,7 +626,7 @@ describe Rack::Request do
604
626
  should "handle multiple media type parameters" do
605
627
  req = Rack::Request.new \
606
628
  Rack::MockRequest.env_for("/",
607
- "CONTENT_TYPE" => 'text/plain; foo=BAR,baz=bizzle dizzle;BLING=bam')
629
+ "CONTENT_TYPE" => 'text/plain; foo=BAR,baz=bizzle dizzle;BLING=bam;blong="boo";zump="zoo\"o";weird=lol"')
608
630
  req.should.not.be.form_data
609
631
  req.media_type_params.should.include 'foo'
610
632
  req.media_type_params['foo'].should.equal 'BAR'
@@ -613,6 +635,9 @@ describe Rack::Request do
613
635
  req.media_type_params.should.not.include 'BLING'
614
636
  req.media_type_params.should.include 'bling'
615
637
  req.media_type_params['bling'].should.equal 'bam'
638
+ req.media_type_params['blong'].should.equal 'boo'
639
+ req.media_type_params['zump'].should.equal 'zoo\"o'
640
+ req.media_type_params['weird'].should.equal 'lol"'
616
641
  end
617
642
 
618
643
  should "parse with junk before boundry" do
@@ -719,22 +744,6 @@ EOF
719
744
  f[:tempfile].size.should.equal 76
720
745
  end
721
746
 
722
- should "MultipartLimitError when request has too many multipart parts if limit set" do
723
- begin
724
- data = 10000.times.map { "--AaB03x\r\nContent-Type: text/plain\r\nContent-Disposition: attachment; name=#{SecureRandom.hex(10)}; filename=#{SecureRandom.hex(10)}\r\n\r\ncontents\r\n" }.join("\r\n")
725
- data += "--AaB03x--\r"
726
-
727
- options = {
728
- "CONTENT_TYPE" => "multipart/form-data; boundary=AaB03x",
729
- "CONTENT_LENGTH" => data.length.to_s,
730
- :input => StringIO.new(data)
731
- }
732
-
733
- request = Rack::Request.new Rack::MockRequest.env_for("/", options)
734
- lambda { request.POST }.should.raise(Rack::Multipart::MultipartLimitError)
735
- end
736
- end
737
-
738
747
  should "parse big multipart form data" do
739
748
  input = <<EOF
740
749
  --AaB03x\r
@@ -757,6 +766,31 @@ EOF
757
766
  req.POST["mean"][:tempfile].read.should.equal "--AaB03xha"
758
767
  end
759
768
 
769
+ should "record tempfiles from multipart form data in env[rack.tempfiles]" do
770
+ input = <<EOF
771
+ --AaB03x\r
772
+ content-disposition: form-data; name="fileupload"; filename="foo.jpg"\r
773
+ Content-Type: image/jpeg\r
774
+ Content-Transfer-Encoding: base64\r
775
+ \r
776
+ /9j/4AAQSkZJRgABAQAAAQABAAD//gA+Q1JFQVRPUjogZ2QtanBlZyB2MS4wICh1c2luZyBJSkcg\r
777
+ --AaB03x\r
778
+ content-disposition: form-data; name="fileupload"; filename="bar.jpg"\r
779
+ Content-Type: image/jpeg\r
780
+ Content-Transfer-Encoding: base64\r
781
+ \r
782
+ /9j/4AAQSkZJRgABAQAAAQABAAD//gA+Q1JFQVRPUjogZ2QtanBlZyB2MS4wICh1c2luZyBJSkcg\r
783
+ --AaB03x--\r
784
+ EOF
785
+ env = Rack::MockRequest.env_for("/",
786
+ "CONTENT_TYPE" => "multipart/form-data, boundary=AaB03x",
787
+ "CONTENT_LENGTH" => input.size,
788
+ :input => input)
789
+ req = Rack::Request.new(env)
790
+ req.params
791
+ env['rack.tempfiles'].size.should.equal(2)
792
+ end
793
+
760
794
  should "detect invalid multipart form data" do
761
795
  input = <<EOF
762
796
  --AaB03x\r
@@ -796,6 +830,20 @@ EOF
796
830
  lambda { req.POST }.should.raise(EOFError)
797
831
  end
798
832
 
833
+ should "consistently raise EOFError on bad multipart form data" do
834
+ input = <<EOF
835
+ --AaB03x\r
836
+ content-disposition: form-data; name="huge"; filename="huge"\r
837
+ EOF
838
+ req = Rack::Request.new Rack::MockRequest.env_for("/",
839
+ "CONTENT_TYPE" => "multipart/form-data, boundary=AaB03x",
840
+ "CONTENT_LENGTH" => input.size,
841
+ :input => input)
842
+
843
+ lambda { req.POST }.should.raise(EOFError)
844
+ lambda { req.POST }.should.raise(EOFError)
845
+ end
846
+
799
847
  should "correctly parse the part name from Content-Id header" do
800
848
  input = <<EOF
801
849
  --AaB03x\r
@@ -941,6 +989,23 @@ EOF
941
989
  parser.call("gzip ; deflate").should.equal([["gzip", 1.0]])
942
990
  end
943
991
 
992
+ should "parse Accept-Language correctly" do
993
+ parser = lambda do |x|
994
+ Rack::Request.new(Rack::MockRequest.env_for("", "HTTP_ACCEPT_LANGUAGE" => x)).accept_language
995
+ end
996
+
997
+ parser.call(nil).should.equal([])
998
+
999
+ parser.call("fr, en").should.equal([["fr", 1.0], ["en", 1.0]])
1000
+ parser.call("").should.equal([])
1001
+ parser.call("*").should.equal([["*", 1.0]])
1002
+ parser.call("fr;q=0.5, en;q=1.0").should.equal([["fr", 0.5], ["en", 1.0]])
1003
+ parser.call("fr;q=1.0, en; q=0.5, *;q=0").should.equal([["fr", 1.0], ["en", 0.5], ["*", 0] ])
1004
+
1005
+ parser.call("fr ; q=0.9").should.equal([["fr", 0.9]])
1006
+ parser.call("fr").should.equal([["fr", 1.0]])
1007
+ end
1008
+
944
1009
  ip_app = lambda { |env|
945
1010
  request = Rack::Request.new(env)
946
1011
  response = Rack::Response.new
@@ -1023,12 +1088,6 @@ EOF
1023
1088
  'HTTP_CLIENT_IP' => '1.1.1.1'
1024
1089
  res.body.should.equal '1.1.1.1'
1025
1090
 
1026
- # Spoofing attempt
1027
- res = mock.get '/',
1028
- 'HTTP_X_FORWARDED_FOR' => '1.1.1.1',
1029
- 'HTTP_CLIENT_IP' => '2.2.2.2'
1030
- res.body.should.equal '1.1.1.1'
1031
-
1032
1091
  res = mock.get '/', 'HTTP_X_FORWARDED_FOR' => '8.8.8.8, 9.9.9.9'
1033
1092
  res.body.should.equal '9.9.9.9'
1034
1093
 
@@ -1047,6 +1106,24 @@ EOF
1047
1106
  res.body.should.equal '3.4.5.6'
1048
1107
  end
1049
1108
 
1109
+ should "not allow IP spoofing via Client-IP and X-Forwarded-For headers" do
1110
+ mock = Rack::MockRequest.new(Rack::Lint.new(ip_app))
1111
+
1112
+ # IP Spoofing attempt:
1113
+ # Client sends X-Forwarded-For: 6.6.6.6
1114
+ # Client-IP: 6.6.6.6
1115
+ # Load balancer adds X-Forwarded-For: 2.2.2.3, 192.168.0.7
1116
+ # App receives: X-Forwarded-For: 6.6.6.6
1117
+ # X-Forwarded-For: 2.2.2.3, 192.168.0.7
1118
+ # Client-IP: 6.6.6.6
1119
+ # Rack env: HTTP_X_FORWARDED_FOR: '6.6.6.6, 2.2.2.3, 192.168.0.7'
1120
+ # HTTP_CLIENT_IP: '6.6.6.6'
1121
+ res = mock.get '/',
1122
+ 'HTTP_X_FORWARDED_FOR' => '6.6.6.6, 2.2.2.3, 192.168.0.7',
1123
+ 'HTTP_CLIENT_IP' => '6.6.6.6'
1124
+ res.body.should.equal '2.2.2.3'
1125
+ end
1126
+
1050
1127
  should "regard local addresses as proxies" do
1051
1128
  req = Rack::Request.new(Rack::MockRequest.env_for("/"))
1052
1129
  req.trusted_proxy?('127.0.0.1').should.equal 0
@@ -1101,6 +1178,13 @@ EOF
1101
1178
  req2.params.should.equal "foo" => "bar"
1102
1179
  end
1103
1180
 
1181
+ should "raise TypeError every time if request parameters are broken" do
1182
+ broken_query = Rack::MockRequest.env_for("/?foo%5B%5D=0&foo%5Bbar%5D=1")
1183
+ req = Rack::Request.new(broken_query)
1184
+ lambda{req.GET}.should.raise(TypeError)
1185
+ lambda{req.params}.should.raise(TypeError)
1186
+ end
1187
+
1104
1188
  (0x20...0x7E).collect { |a|
1105
1189
  b = a.chr
1106
1190
  c = CGI.escape(b)