rack 2.0.8 → 2.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 rack might be problematic. Click here for more details.

Files changed (190) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +690 -0
  3. data/CONTRIBUTING.md +136 -0
  4. data/{COPYING → MIT-LICENSE} +4 -2
  5. data/README.rdoc +152 -148
  6. data/Rakefile +37 -23
  7. data/{SPEC → SPEC.rdoc} +29 -5
  8. data/bin/rackup +1 -0
  9. data/example/lobster.ru +2 -0
  10. data/example/protectedlobster.rb +3 -1
  11. data/example/protectedlobster.ru +2 -0
  12. data/lib/rack.rb +67 -73
  13. data/lib/rack/auth/abstract/handler.rb +3 -1
  14. data/lib/rack/auth/abstract/request.rb +1 -1
  15. data/lib/rack/auth/basic.rb +7 -4
  16. data/lib/rack/auth/digest/md5.rb +13 -11
  17. data/lib/rack/auth/digest/nonce.rb +6 -3
  18. data/lib/rack/auth/digest/params.rb +4 -2
  19. data/lib/rack/auth/digest/request.rb +5 -3
  20. data/lib/rack/body_proxy.rb +15 -14
  21. data/lib/rack/builder.rb +116 -23
  22. data/lib/rack/cascade.rb +28 -12
  23. data/lib/rack/chunked.rb +68 -20
  24. data/lib/rack/common_logger.rb +33 -25
  25. data/lib/rack/conditional_get.rb +20 -16
  26. data/lib/rack/config.rb +2 -0
  27. data/lib/rack/content_length.rb +8 -7
  28. data/lib/rack/content_type.rb +5 -4
  29. data/lib/rack/core_ext/regexp.rb +14 -0
  30. data/lib/rack/deflater.rb +59 -34
  31. data/lib/rack/directory.rb +84 -64
  32. data/lib/rack/etag.rb +5 -4
  33. data/lib/rack/events.rb +19 -20
  34. data/lib/rack/file.rb +4 -173
  35. data/lib/rack/files.rb +218 -0
  36. data/lib/rack/handler.rb +7 -2
  37. data/lib/rack/handler/cgi.rb +2 -3
  38. data/lib/rack/handler/fastcgi.rb +4 -4
  39. data/lib/rack/handler/lsws.rb +3 -3
  40. data/lib/rack/handler/scgi.rb +9 -8
  41. data/lib/rack/handler/thin.rb +3 -3
  42. data/lib/rack/handler/webrick.rb +15 -6
  43. data/lib/rack/head.rb +1 -1
  44. data/lib/rack/lint.rb +71 -25
  45. data/lib/rack/lobster.rb +10 -10
  46. data/lib/rack/lock.rb +2 -1
  47. data/lib/rack/logger.rb +2 -0
  48. data/lib/rack/media_type.rb +10 -5
  49. data/lib/rack/method_override.rb +4 -2
  50. data/lib/rack/mime.rb +9 -1
  51. data/lib/rack/mock.rb +97 -20
  52. data/lib/rack/multipart.rb +6 -4
  53. data/lib/rack/multipart/generator.rb +17 -13
  54. data/lib/rack/multipart/parser.rb +54 -56
  55. data/lib/rack/multipart/uploaded_file.rb +15 -7
  56. data/lib/rack/null_logger.rb +2 -0
  57. data/lib/rack/query_parser.rb +53 -28
  58. data/lib/rack/recursive.rb +7 -5
  59. data/lib/rack/reloader.rb +8 -4
  60. data/lib/rack/request.rb +220 -61
  61. data/lib/rack/response.rb +127 -44
  62. data/lib/rack/rewindable_input.rb +4 -3
  63. data/lib/rack/runtime.rb +6 -4
  64. data/lib/rack/sendfile.rb +13 -9
  65. data/lib/rack/server.rb +95 -24
  66. data/lib/rack/session/abstract/id.rb +36 -23
  67. data/lib/rack/session/cookie.rb +11 -12
  68. data/lib/rack/session/memcache.rb +4 -93
  69. data/lib/rack/session/pool.rb +5 -3
  70. data/lib/rack/show_exceptions.rb +21 -17
  71. data/lib/rack/show_status.rb +9 -9
  72. data/lib/rack/static.rb +23 -11
  73. data/lib/rack/tempfile_reaper.rb +1 -1
  74. data/lib/rack/urlmap.rb +12 -6
  75. data/lib/rack/utils.rb +98 -109
  76. data/lib/rack/version.rb +29 -0
  77. data/rack.gemspec +40 -28
  78. metadata +36 -177
  79. data/HISTORY.md +0 -505
  80. data/test/builder/an_underscore_app.rb +0 -5
  81. data/test/builder/anything.rb +0 -5
  82. data/test/builder/comment.ru +0 -4
  83. data/test/builder/end.ru +0 -5
  84. data/test/builder/line.ru +0 -1
  85. data/test/builder/options.ru +0 -2
  86. data/test/cgi/assets/folder/test.js +0 -1
  87. data/test/cgi/assets/fonts/font.eot +0 -1
  88. data/test/cgi/assets/images/image.png +0 -1
  89. data/test/cgi/assets/index.html +0 -1
  90. data/test/cgi/assets/javascripts/app.js +0 -1
  91. data/test/cgi/assets/stylesheets/app.css +0 -1
  92. data/test/cgi/lighttpd.conf +0 -26
  93. data/test/cgi/rackup_stub.rb +0 -6
  94. data/test/cgi/sample_rackup.ru +0 -5
  95. data/test/cgi/test +0 -9
  96. data/test/cgi/test+directory/test+file +0 -1
  97. data/test/cgi/test.fcgi +0 -9
  98. data/test/cgi/test.gz +0 -0
  99. data/test/cgi/test.ru +0 -5
  100. data/test/gemloader.rb +0 -10
  101. data/test/helper.rb +0 -34
  102. data/test/multipart/bad_robots +0 -259
  103. data/test/multipart/binary +0 -0
  104. data/test/multipart/content_type_and_no_filename +0 -6
  105. data/test/multipart/empty +0 -10
  106. data/test/multipart/fail_16384_nofile +0 -814
  107. data/test/multipart/file1.txt +0 -1
  108. data/test/multipart/filename_and_modification_param +0 -7
  109. data/test/multipart/filename_and_no_name +0 -6
  110. data/test/multipart/filename_with_encoded_words +0 -7
  111. data/test/multipart/filename_with_escaped_quotes +0 -6
  112. data/test/multipart/filename_with_escaped_quotes_and_modification_param +0 -7
  113. data/test/multipart/filename_with_null_byte +0 -7
  114. data/test/multipart/filename_with_percent_escaped_quotes +0 -6
  115. data/test/multipart/filename_with_single_quote +0 -7
  116. data/test/multipart/filename_with_unescaped_percentages +0 -6
  117. data/test/multipart/filename_with_unescaped_percentages2 +0 -6
  118. data/test/multipart/filename_with_unescaped_percentages3 +0 -6
  119. data/test/multipart/filename_with_unescaped_quotes +0 -6
  120. data/test/multipart/ie +0 -6
  121. data/test/multipart/invalid_character +0 -6
  122. data/test/multipart/mixed_files +0 -21
  123. data/test/multipart/nested +0 -10
  124. data/test/multipart/none +0 -9
  125. data/test/multipart/quoted +0 -15
  126. data/test/multipart/rack-logo.png +0 -0
  127. data/test/multipart/semicolon +0 -6
  128. data/test/multipart/text +0 -15
  129. data/test/multipart/three_files_three_fields +0 -31
  130. data/test/multipart/unity3d_wwwform +0 -11
  131. data/test/multipart/webkit +0 -32
  132. data/test/rackup/config.ru +0 -31
  133. data/test/registering_handler/rack/handler/registering_myself.rb +0 -8
  134. data/test/spec_auth_basic.rb +0 -89
  135. data/test/spec_auth_digest.rb +0 -260
  136. data/test/spec_body_proxy.rb +0 -85
  137. data/test/spec_builder.rb +0 -233
  138. data/test/spec_cascade.rb +0 -63
  139. data/test/spec_cgi.rb +0 -84
  140. data/test/spec_chunked.rb +0 -103
  141. data/test/spec_common_logger.rb +0 -95
  142. data/test/spec_conditional_get.rb +0 -103
  143. data/test/spec_config.rb +0 -23
  144. data/test/spec_content_length.rb +0 -86
  145. data/test/spec_content_type.rb +0 -46
  146. data/test/spec_deflater.rb +0 -375
  147. data/test/spec_directory.rb +0 -148
  148. data/test/spec_etag.rb +0 -108
  149. data/test/spec_events.rb +0 -133
  150. data/test/spec_fastcgi.rb +0 -85
  151. data/test/spec_file.rb +0 -264
  152. data/test/spec_handler.rb +0 -57
  153. data/test/spec_head.rb +0 -46
  154. data/test/spec_lint.rb +0 -515
  155. data/test/spec_lobster.rb +0 -59
  156. data/test/spec_lock.rb +0 -204
  157. data/test/spec_logger.rb +0 -24
  158. data/test/spec_media_type.rb +0 -42
  159. data/test/spec_method_override.rb +0 -110
  160. data/test/spec_mime.rb +0 -51
  161. data/test/spec_mock.rb +0 -359
  162. data/test/spec_multipart.rb +0 -722
  163. data/test/spec_null_logger.rb +0 -21
  164. data/test/spec_recursive.rb +0 -75
  165. data/test/spec_request.rb +0 -1407
  166. data/test/spec_response.rb +0 -510
  167. data/test/spec_rewindable_input.rb +0 -128
  168. data/test/spec_runtime.rb +0 -50
  169. data/test/spec_sendfile.rb +0 -125
  170. data/test/spec_server.rb +0 -193
  171. data/test/spec_session_abstract_id.rb +0 -31
  172. data/test/spec_session_abstract_session_hash.rb +0 -45
  173. data/test/spec_session_cookie.rb +0 -442
  174. data/test/spec_session_memcache.rb +0 -357
  175. data/test/spec_session_pool.rb +0 -247
  176. data/test/spec_show_exceptions.rb +0 -93
  177. data/test/spec_show_status.rb +0 -104
  178. data/test/spec_static.rb +0 -184
  179. data/test/spec_tempfile_reaper.rb +0 -64
  180. data/test/spec_thin.rb +0 -96
  181. data/test/spec_urlmap.rb +0 -237
  182. data/test/spec_utils.rb +0 -742
  183. data/test/spec_version.rb +0 -11
  184. data/test/spec_webrick.rb +0 -206
  185. data/test/static/another/index.html +0 -1
  186. data/test/static/foo.html +0 -1
  187. data/test/static/index.html +0 -1
  188. data/test/testrequest.rb +0 -78
  189. data/test/unregistered_handler/rack/handler/unregistered.rb +0 -7
  190. data/test/unregistered_handler/rack/handler/unregistered_long_one.rb +0 -7
@@ -1,9 +1,11 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Rack
2
4
  class MethodOverride
3
5
  HTTP_METHODS = %w[GET HEAD PUT POST DELETE OPTIONS PATCH LINK UNLINK]
4
6
 
5
- METHOD_OVERRIDE_PARAM_KEY = "_method".freeze
6
- HTTP_METHOD_OVERRIDE_HEADER = "HTTP_X_HTTP_METHOD_OVERRIDE".freeze
7
+ METHOD_OVERRIDE_PARAM_KEY = "_method"
8
+ HTTP_METHOD_OVERRIDE_HEADER = "HTTP_X_HTTP_METHOD_OVERRIDE"
7
9
  ALLOWED_METHODS = %w[POST]
8
10
 
9
11
  def initialize(app)
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Rack
2
4
  module Mime
3
5
  # Returns String with mime type if found, otherwise use +fallback+.
@@ -13,7 +15,7 @@ module Rack
13
15
  # This is a shortcut for:
14
16
  # Rack::Mime::MIME_TYPES.fetch('.foo', 'application/octet-stream')
15
17
 
16
- def mime_type(ext, fallback='application/octet-stream')
18
+ def mime_type(ext, fallback = 'application/octet-stream')
17
19
  MIME_TYPES.fetch(ext.to_s.downcase, fallback)
18
20
  end
19
21
  module_function :mime_type
@@ -306,6 +308,7 @@ module Rack
306
308
  ".lvp" => "audio/vnd.lucent.voice",
307
309
  ".lwp" => "application/vnd.lotus-wordpro",
308
310
  ".m3u" => "audio/x-mpegurl",
311
+ ".m3u8" => "application/x-mpegurl",
309
312
  ".m4a" => "audio/mp4a-latm",
310
313
  ".m4v" => "video/mp4",
311
314
  ".ma" => "application/mathematica",
@@ -343,6 +346,7 @@ module Rack
343
346
  ".mp4s" => "application/mp4",
344
347
  ".mp4v" => "video/mp4",
345
348
  ".mpc" => "application/vnd.mophun.certificate",
349
+ ".mpd" => "application/dash+xml",
346
350
  ".mpeg" => "video/mpeg",
347
351
  ".mpg" => "video/mpeg",
348
352
  ".mpga" => "audio/mpeg",
@@ -542,6 +546,7 @@ module Rack
542
546
  ".spp" => "application/scvp-vp-response",
543
547
  ".spq" => "application/scvp-vp-request",
544
548
  ".src" => "application/x-wais-source",
549
+ ".srt" => "text/srt",
545
550
  ".srx" => "application/sparql-results+xml",
546
551
  ".sse" => "application/vnd.kodak-descriptor",
547
552
  ".ssf" => "application/vnd.epson.ssf",
@@ -576,6 +581,7 @@ module Rack
576
581
  ".tr" => "text/troff",
577
582
  ".tra" => "application/vnd.trueapp",
578
583
  ".trm" => "application/x-msterminal",
584
+ ".ts" => "video/mp2t",
579
585
  ".tsv" => "text/tab-separated-values",
580
586
  ".ttf" => "application/octet-stream",
581
587
  ".twd" => "application/vnd.simtech-mindmapper",
@@ -600,9 +606,11 @@ module Rack
600
606
  ".vrml" => "model/vrml",
601
607
  ".vsd" => "application/vnd.visio",
602
608
  ".vsf" => "application/vnd.vsf",
609
+ ".vtt" => "text/vtt",
603
610
  ".vtu" => "model/vnd.vtu",
604
611
  ".vxml" => "application/voicexml+xml",
605
612
  ".war" => "application/java-archive",
613
+ ".wasm" => "application/wasm",
606
614
  ".wav" => "audio/x-wav",
607
615
  ".wax" => "audio/x-ms-wax",
608
616
  ".wbmp" => "image/vnd.wap.wbmp",
@@ -1,9 +1,9 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'uri'
2
4
  require 'stringio'
3
- require 'rack'
4
- require 'rack/lint'
5
- require 'rack/utils'
6
- require 'rack/response'
5
+ require_relative '../rack'
6
+ require 'cgi/cookie'
7
7
 
8
8
  module Rack
9
9
  # Rack::MockRequest helps testing your Rack application without
@@ -53,16 +53,26 @@ module Rack
53
53
  @app = app
54
54
  end
55
55
 
56
- def get(uri, opts={}) request(GET, uri, opts) end
57
- def post(uri, opts={}) request(POST, uri, opts) end
58
- def put(uri, opts={}) request(PUT, uri, opts) end
59
- def patch(uri, opts={}) request(PATCH, uri, opts) end
60
- def delete(uri, opts={}) request(DELETE, uri, opts) end
61
- def head(uri, opts={}) request(HEAD, uri, opts) end
62
- def options(uri, opts={}) request(OPTIONS, uri, opts) end
63
-
64
- def request(method=GET, uri="", opts={})
65
- env = self.class.env_for(uri, opts.merge(:method => method))
56
+ # Make a GET request and return a MockResponse. See #request.
57
+ def get(uri, opts = {}) request(GET, uri, opts) end
58
+ # Make a POST request and return a MockResponse. See #request.
59
+ def post(uri, opts = {}) request(POST, uri, opts) end
60
+ # Make a PUT request and return a MockResponse. See #request.
61
+ def put(uri, opts = {}) request(PUT, uri, opts) end
62
+ # Make a PATCH request and return a MockResponse. See #request.
63
+ def patch(uri, opts = {}) request(PATCH, uri, opts) end
64
+ # Make a DELETE request and return a MockResponse. See #request.
65
+ def delete(uri, opts = {}) request(DELETE, uri, opts) end
66
+ # Make a HEAD request and return a MockResponse. See #request.
67
+ def head(uri, opts = {}) request(HEAD, uri, opts) end
68
+ # Make an OPTIONS request and return a MockResponse. See #request.
69
+ def options(uri, opts = {}) request(OPTIONS, uri, opts) end
70
+
71
+ # Make a request using the given request method for the given
72
+ # uri to the rack application and return a MockResponse.
73
+ # Options given are passed to MockRequest.env_for.
74
+ def request(method = GET, uri = "", opts = {})
75
+ env = self.class.env_for(uri, opts.merge(method: method))
66
76
 
67
77
  if opts[:lint]
68
78
  app = Rack::Lint.new(@app)
@@ -71,7 +81,7 @@ module Rack
71
81
  end
72
82
 
73
83
  errors = env[RACK_ERRORS]
74
- status, headers, body = app.call(env)
84
+ status, headers, body = app.call(env)
75
85
  MockResponse.new(status, headers, body, errors)
76
86
  ensure
77
87
  body.close if body.respond_to?(:close)
@@ -85,7 +95,14 @@ module Rack
85
95
  end
86
96
 
87
97
  # Return the Rack environment used for a request to +uri+.
88
- def self.env_for(uri="", opts={})
98
+ # All options that are strings are added to the returned environment.
99
+ # Options:
100
+ # :fatal :: Whether to raise an exception if request outputs to rack.errors
101
+ # :input :: The rack.input to set
102
+ # :method :: The HTTP request method to use
103
+ # :params :: The params to use
104
+ # :script_name :: The SCRIPT_NAME to set
105
+ def self.env_for(uri = "", opts = {})
89
106
  uri = parse_uri_rfc2396(uri)
90
107
  uri.path = "/#{uri.path}" unless uri.path[0] == ?/
91
108
 
@@ -139,7 +156,7 @@ module Rack
139
156
  rack_input.set_encoding(Encoding::BINARY)
140
157
  env[RACK_INPUT] = rack_input
141
158
 
142
- env["CONTENT_LENGTH"] ||= env[RACK_INPUT].length.to_s
159
+ env["CONTENT_LENGTH"] ||= env[RACK_INPUT].size.to_s if env[RACK_INPUT].respond_to?(:size)
143
160
 
144
161
  opts.each { |field, value|
145
162
  env[field] = value if String === field
@@ -154,17 +171,24 @@ module Rack
154
171
  # MockRequest.
155
172
 
156
173
  class MockResponse < Rack::Response
174
+ class << self
175
+ alias [] new
176
+ end
177
+
157
178
  # Headers
158
- attr_reader :original_headers
179
+ attr_reader :original_headers, :cookies
159
180
 
160
181
  # Errors
161
182
  attr_accessor :errors
162
183
 
163
- def initialize(status, headers, body, errors=StringIO.new(""))
184
+ def initialize(status, headers, body, errors = StringIO.new(""))
164
185
  @original_headers = headers
165
186
  @errors = errors.string if errors.respond_to?(:string)
187
+ @cookies = parse_cookies_from_header
166
188
 
167
189
  super(body, status, headers)
190
+
191
+ buffered_body!
168
192
  end
169
193
 
170
194
  def =~(other)
@@ -186,11 +210,64 @@ module Rack
186
210
  # ...
187
211
  # res.body.should == "foo!"
188
212
  # end
189
- super.join
213
+ buffer = String.new
214
+
215
+ super.each do |chunk|
216
+ buffer << chunk
217
+ end
218
+
219
+ return buffer
190
220
  end
191
221
 
192
222
  def empty?
193
223
  [201, 204, 304].include? status
194
224
  end
225
+
226
+ def cookie(name)
227
+ cookies.fetch(name, nil)
228
+ end
229
+
230
+ private
231
+
232
+ def parse_cookies_from_header
233
+ cookies = Hash.new
234
+ if original_headers.has_key? 'Set-Cookie'
235
+ set_cookie_header = original_headers.fetch('Set-Cookie')
236
+ set_cookie_header.split("\n").each do |cookie|
237
+ cookie_name, cookie_filling = cookie.split('=', 2)
238
+ cookie_attributes = identify_cookie_attributes cookie_filling
239
+ parsed_cookie = CGI::Cookie.new(
240
+ 'name' => cookie_name.strip,
241
+ 'value' => cookie_attributes.fetch('value'),
242
+ 'path' => cookie_attributes.fetch('path', nil),
243
+ 'domain' => cookie_attributes.fetch('domain', nil),
244
+ 'expires' => cookie_attributes.fetch('expires', nil),
245
+ 'secure' => cookie_attributes.fetch('secure', false)
246
+ )
247
+ cookies.store(cookie_name, parsed_cookie)
248
+ end
249
+ end
250
+ cookies
251
+ end
252
+
253
+ def identify_cookie_attributes(cookie_filling)
254
+ cookie_bits = cookie_filling.split(';')
255
+ cookie_attributes = Hash.new
256
+ cookie_attributes.store('value', cookie_bits[0].strip)
257
+ cookie_bits.each do |bit|
258
+ if bit.include? '='
259
+ cookie_attribute, attribute_value = bit.split('=')
260
+ cookie_attributes.store(cookie_attribute.strip, attribute_value.strip)
261
+ if cookie_attribute.include? 'max-age'
262
+ cookie_attributes.store('expires', Time.now + attribute_value.strip.to_i)
263
+ end
264
+ end
265
+ if bit.include? 'secure'
266
+ cookie_attributes.store('secure', true)
267
+ end
268
+ end
269
+ cookie_attributes
270
+ end
271
+
195
272
  end
196
273
  end
@@ -1,4 +1,6 @@
1
- require 'rack/multipart/parser'
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'multipart/parser'
2
4
 
3
5
  module Rack
4
6
  # A multipart form data parser, adapted from IOWA.
@@ -14,10 +16,10 @@ module Rack
14
16
  TOKEN = /[^\s()<>,;:\\"\/\[\]?=]+/
15
17
  CONDISP = /Content-Disposition:\s*#{TOKEN}\s*/i
16
18
  VALUE = /"(?:\\"|[^"])*"|#{TOKEN}/
17
- BROKEN_QUOTED = /^#{CONDISP}.*;\sfilename="(.*?)"(?:\s*$|\s*;\s*#{TOKEN}=)/i
18
- BROKEN_UNQUOTED = /^#{CONDISP}.*;\sfilename=(#{TOKEN})/i
19
+ BROKEN_QUOTED = /^#{CONDISP}.*;\s*filename="(.*?)"(?:\s*$|\s*;\s*#{TOKEN}=)/i
20
+ BROKEN_UNQUOTED = /^#{CONDISP}.*;\s*filename=(#{TOKEN})/i
19
21
  MULTIPART_CONTENT_TYPE = /Content-Type: (.*)#{EOL}/ni
20
- MULTIPART_CONTENT_DISPOSITION = /Content-Disposition:.*\s+name=(#{VALUE})/ni
22
+ MULTIPART_CONTENT_DISPOSITION = /Content-Disposition:.*;\s*name=(#{VALUE})/ni
21
23
  MULTIPART_CONTENT_ID = /Content-ID:\s*([^#{EOL}]*)/ni
22
24
  # Updated definitions from RFC 2231
23
25
  ATTRIBUTE_CHAR = %r{[^ \t\v\n\r)(><@,;:\\"/\[\]?='*%]}
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Rack
2
4
  module Multipart
3
5
  class Generator
@@ -15,9 +17,13 @@ module Rack
15
17
 
16
18
  flattened_params.map do |name, file|
17
19
  if file.respond_to?(:original_filename)
18
- ::File.open(file.path, 'rb') do |f|
19
- f.set_encoding(Encoding::BINARY)
20
- content_for_tempfile(f, file, name)
20
+ if file.path
21
+ ::File.open(file.path, 'rb') do |f|
22
+ f.set_encoding(Encoding::BINARY)
23
+ content_for_tempfile(f, file, name)
24
+ end
25
+ else
26
+ content_for_tempfile(file, file, name)
21
27
  end
22
28
  else
23
29
  content_for_other(file, name)
@@ -27,21 +33,18 @@ module Rack
27
33
 
28
34
  private
29
35
  def multipart?
30
- multipart = false
31
-
32
36
  query = lambda { |value|
33
37
  case value
34
38
  when Array
35
- value.each(&query)
39
+ value.any?(&query)
36
40
  when Hash
37
- value.values.each(&query)
41
+ value.values.any?(&query)
38
42
  when Rack::Multipart::UploadedFile
39
- multipart = true
43
+ true
40
44
  end
41
45
  }
42
- @params.values.each(&query)
43
46
 
44
- multipart
47
+ @params.values.any?(&query)
45
48
  end
46
49
 
47
50
  def flattened_params
@@ -70,12 +73,13 @@ module Rack
70
73
  end
71
74
 
72
75
  def content_for_tempfile(io, file, name)
76
+ length = ::File.stat(file.path).size if file.path
77
+ filename = "; filename=\"#{Utils.escape(file.original_filename)}\"" if file.original_filename
73
78
  <<-EOF
74
79
  --#{MULTIPART_BOUNDARY}\r
75
- Content-Disposition: form-data; name="#{name}"; filename="#{Utils.escape(file.original_filename)}"\r
80
+ Content-Disposition: form-data; name="#{name}"#{filename}\r
76
81
  Content-Type: #{file.content_type}\r
77
- Content-Length: #{::File.stat(file.path).size}\r
78
- \r
82
+ #{"Content-Length: #{length}\r\n" if length}\r
79
83
  #{io.read}\r
80
84
  EOF
81
85
  end
@@ -1,16 +1,22 @@
1
- require 'rack/utils'
1
+ # frozen_string_literal: true
2
+
3
+ require 'strscan'
2
4
 
3
5
  module Rack
4
6
  module Multipart
5
7
  class MultipartPartLimitError < Errno::EMFILE; end
6
8
 
7
9
  class Parser
8
- BUFSIZE = 16384
10
+ (require_relative '../core_ext/regexp'; using ::Rack::RegexpExtensions) if RUBY_VERSION < '2.4'
11
+
12
+ BUFSIZE = 1_048_576
9
13
  TEXT_PLAIN = "text/plain"
10
14
  TEMPFILE_FACTORY = lambda { |filename, content_type|
11
- Tempfile.new(["RackMultipart", ::File.extname(filename.gsub("\0".freeze, '%00'.freeze))])
15
+ Tempfile.new(["RackMultipart", ::File.extname(filename.gsub("\0", '%00'))])
12
16
  }
13
17
 
18
+ BOUNDARY_REGEX = /\A([^\n]*(?:\n|\Z))/
19
+
14
20
  class BoundedIO # :nodoc:
15
21
  def initialize(io, content_length)
16
22
  @io = io
@@ -18,15 +24,15 @@ module Rack
18
24
  @cursor = 0
19
25
  end
20
26
 
21
- def read(size)
27
+ def read(size, outbuf = nil)
22
28
  return if @cursor >= @content_length
23
29
 
24
30
  left = @content_length - @cursor
25
31
 
26
32
  str = if left < size
27
- @io.read left
33
+ @io.read left, outbuf
28
34
  else
29
- @io.read size
35
+ @io.read size, outbuf
30
36
  end
31
37
 
32
38
  if str
@@ -61,13 +67,14 @@ module Rack
61
67
  return EMPTY unless boundary
62
68
 
63
69
  io = BoundedIO.new(io, content_length) if content_length
70
+ outbuf = String.new
64
71
 
65
72
  parser = new(boundary, tmpfile, bufsize, qp)
66
- parser.on_read io.read(bufsize)
73
+ parser.on_read io.read(bufsize, outbuf)
67
74
 
68
75
  loop do
69
76
  break if parser.state == :DONE
70
- parser.on_read io.read(bufsize)
77
+ parser.on_read io.read(bufsize, outbuf)
71
78
  end
72
79
 
73
80
  io.rewind
@@ -90,14 +97,8 @@ module Rack
90
97
  # those which give the lone filename.
91
98
  fn = filename.split(/[\/\\]/).last
92
99
 
93
- data = {:filename => fn, :type => content_type,
94
- :name => name, :tempfile => body, :head => head}
95
- elsif !filename && content_type && body.is_a?(IO)
96
- body.rewind
97
-
98
- # Generic multipart cases, not coming from a form
99
- data = {:type => content_type,
100
- :name => name, :tempfile => body, :head => head}
100
+ data = { filename: fn, type: content_type,
101
+ name: name, tempfile: body, head: head }
101
102
  end
102
103
 
103
104
  yield data
@@ -116,7 +117,7 @@ module Rack
116
117
 
117
118
  include Enumerable
118
119
 
119
- def initialize tempfile
120
+ def initialize(tempfile)
120
121
  @tempfile = tempfile
121
122
  @mime_parts = []
122
123
  @open_files = 0
@@ -126,7 +127,7 @@ module Rack
126
127
  @mime_parts.each { |part| yield part }
127
128
  end
128
129
 
129
- def on_mime_head mime_index, head, filename, content_type, name
130
+ def on_mime_head(mime_index, head, filename, content_type, name)
130
131
  if filename
131
132
  body = @tempfile.call(filename, content_type)
132
133
  body.binmode if body.respond_to?(:binmode)
@@ -138,14 +139,15 @@ module Rack
138
139
  end
139
140
 
140
141
  @mime_parts[mime_index] = klass.new(body, head, filename, content_type, name)
142
+
141
143
  check_open_files
142
144
  end
143
145
 
144
- def on_mime_body mime_index, content
146
+ def on_mime_body(mime_index, content)
145
147
  @mime_parts[mime_index].body << content
146
148
  end
147
149
 
148
- def on_mime_finish mime_index
150
+ def on_mime_finish(mime_index)
149
151
  end
150
152
 
151
153
  private
@@ -163,25 +165,26 @@ module Rack
163
165
  attr_reader :state
164
166
 
165
167
  def initialize(boundary, tempfile, bufsize, query_parser)
166
- @buf = String.new
167
-
168
168
  @query_parser = query_parser
169
169
  @params = query_parser.make_params
170
170
  @boundary = "--#{boundary}"
171
171
  @bufsize = bufsize
172
172
 
173
- @rx = /(?:#{EOL})?#{Regexp.quote(@boundary)}(#{EOL}|--)/n
174
- @rx_max_size = EOL.size + @boundary.bytesize + [EOL.size, '--'.size].max
175
173
  @full_boundary = @boundary
176
174
  @end_boundary = @boundary + '--'
177
175
  @state = :FAST_FORWARD
178
176
  @mime_index = 0
179
177
  @collector = Collector.new tempfile
178
+
179
+ @sbuf = StringScanner.new("".dup)
180
+ @body_regex = /(?:#{EOL})?#{Regexp.quote(@boundary)}(?:#{EOL}|--)/m
181
+ @rx_max_size = EOL.size + @boundary.bytesize + [EOL.size, '--'.size].max
182
+ @head_regex = /(.*?#{EOL})#{EOL}/m
180
183
  end
181
184
 
182
- def on_read content
185
+ def on_read(content)
183
186
  handle_empty_content!(content)
184
- @buf << content
187
+ @sbuf.concat content
185
188
  run_parser
186
189
  end
187
190
 
@@ -192,7 +195,6 @@ module Rack
192
195
  @query_parser.normalize_params(@params, part.name, data, @query_parser.param_depth_limit)
193
196
  end
194
197
  end
195
-
196
198
  MultipartInfo.new @params.to_params_hash, @collector.find_all(&:file?).map(&:body)
197
199
  end
198
200
 
@@ -219,7 +221,7 @@ module Rack
219
221
  if consume_boundary
220
222
  @state = :MIME_HEAD
221
223
  else
222
- raise EOFError, "bad content body" if @buf.bytesize >= @bufsize
224
+ raise EOFError, "bad content body" if @sbuf.rest_size >= @bufsize
223
225
  :want_read
224
226
  end
225
227
  end
@@ -227,19 +229,16 @@ module Rack
227
229
  def handle_consume_token
228
230
  tok = consume_boundary
229
231
  # break if we're at the end of a buffer, but not if it is the end of a field
230
- if tok == :END_BOUNDARY || (@buf.empty? && tok != :BOUNDARY)
231
- @state = :DONE
232
+ @state = if tok == :END_BOUNDARY || (@sbuf.eos? && tok != :BOUNDARY)
233
+ :DONE
232
234
  else
233
- @state = :MIME_HEAD
235
+ :MIME_HEAD
234
236
  end
235
237
  end
236
238
 
237
239
  def handle_mime_head
238
- if @buf.index(EOL + EOL)
239
- i = @buf.index(EOL+EOL)
240
- head = @buf.slice!(0, i+2) # First \r\n
241
- @buf.slice!(0, 2) # Second \r\n
242
-
240
+ if @sbuf.scan_until(@head_regex)
241
+ head = @sbuf[1]
243
242
  content_type = head[MULTIPART_CONTENT_TYPE, 1]
244
243
  if name = head[MULTIPART_CONTENT_DISPOSITION, 1]
245
244
  name = Rack::Auth::Digest::Params::dequote(name)
@@ -250,7 +249,7 @@ module Rack
250
249
  filename = get_filename(head)
251
250
 
252
251
  if name.nil? || name.empty?
253
- name = filename || "#{content_type || TEXT_PLAIN}[]"
252
+ name = filename || "#{content_type || TEXT_PLAIN}[]".dup
254
253
  end
255
254
 
256
255
  @collector.on_mime_head @mime_index, head, filename, content_type, name
@@ -261,16 +260,19 @@ module Rack
261
260
  end
262
261
 
263
262
  def handle_mime_body
264
- if i = @buf.index(rx)
265
- # Save the rest.
266
- @collector.on_mime_body @mime_index, @buf.slice!(0, i)
267
- @buf.slice!(0, 2) # Remove \r\n after the content
263
+ if (body_with_boundary = @sbuf.check_until(@body_regex)) # check but do not advance the pointer yet
264
+ body = body_with_boundary.sub(/#{@body_regex}\z/m, '') # remove the boundary from the string
265
+ @collector.on_mime_body @mime_index, body
266
+ @sbuf.pos += body.length + 2 # skip \r\n after the content
268
267
  @state = :CONSUME_TOKEN
269
268
  @mime_index += 1
270
269
  else
271
- # Save the read body part.
272
- if @rx_max_size < @buf.size
273
- @collector.on_mime_body @mime_index, @buf.slice!(0, @buf.size - @rx_max_size)
270
+ # Save what we have so far
271
+ if @rx_max_size < @sbuf.rest_size
272
+ delta = @sbuf.rest_size - @rx_max_size
273
+ @collector.on_mime_body @mime_index, @sbuf.peek(delta)
274
+ @sbuf.pos += delta
275
+ @sbuf.string = @sbuf.rest
274
276
  end
275
277
  :want_read
276
278
  end
@@ -278,16 +280,13 @@ module Rack
278
280
 
279
281
  def full_boundary; @full_boundary; end
280
282
 
281
- def rx; @rx; end
282
-
283
283
  def consume_boundary
284
- while @buf.gsub!(/\A([^\n]*(?:\n|\Z))/, '')
285
- read_buffer = $1
284
+ while read_buffer = @sbuf.scan_until(BOUNDARY_REGEX)
286
285
  case read_buffer.strip
287
286
  when full_boundary then return :BOUNDARY
288
287
  when @end_boundary then return :END_BOUNDARY
289
288
  end
290
- return if @buf.empty?
289
+ return if @sbuf.eos?
291
290
  end
292
291
  end
293
292
 
@@ -308,8 +307,8 @@ module Rack
308
307
 
309
308
  return unless filename
310
309
 
311
- if filename.scan(/%.?.?/).all? { |s| s =~ /%[0-9a-fA-F]{2}/ }
312
- filename = Utils.unescape(filename)
310
+ if filename.scan(/%.?.?/).all? { |s| /%[0-9a-fA-F]{2}/.match?(s) }
311
+ filename = Utils.unescape_path(filename)
313
312
  end
314
313
 
315
314
  filename.scrub!
@@ -325,7 +324,7 @@ module Rack
325
324
  filename
326
325
  end
327
326
 
328
- CHARSET = "charset"
327
+ CHARSET = "charset"
329
328
 
330
329
  def tag_multipart_encoding(filename, content_type, name, body)
331
330
  name = name.to_s
@@ -340,12 +339,12 @@ module Rack
340
339
  type_subtype = list.first
341
340
  type_subtype.strip!
342
341
  if TEXT_PLAIN == type_subtype
343
- rest = list.drop 1
342
+ rest = list.drop 1
344
343
  rest.each do |param|
345
- k,v = param.split('=', 2)
344
+ k, v = param.split('=', 2)
346
345
  k.strip!
347
346
  v.strip!
348
- v = v[1..-2] if v[0] == '"' && v[-1] == '"'
347
+ v = v[1..-2] if v.start_with?('"') && v.end_with?('"')
349
348
  encoding = Encoding.find v if k == CHARSET
350
349
  end
351
350
  end
@@ -355,7 +354,6 @@ module Rack
355
354
  body.force_encoding(encoding)
356
355
  end
357
356
 
358
-
359
357
  def handle_empty_content!(content)
360
358
  if content.nil? || content.empty?
361
359
  raise EOFError