rack 2.0.6 → 2.2.3

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.
Files changed (190) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +694 -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} +38 -10
  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 +57 -62
  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 +100 -22
  67. data/lib/rack/session/cookie.rb +22 -14
  68. data/lib/rack/session/memcache.rb +4 -87
  69. data/lib/rack/session/pool.rb +18 -9
  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 +102 -109
  76. data/lib/rack/version.rb +29 -0
  77. data/rack.gemspec +40 -28
  78. metadata +39 -181
  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 -1398
  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 -320
  175. data/test/spec_session_pool.rb +0 -210
  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
@@ -39,8 +45,6 @@ module Rack
39
45
  str
40
46
  end
41
47
 
42
- def eof?; @content_length == @cursor; end
43
-
44
48
  def rewind
45
49
  @io.rewind
46
50
  end
@@ -63,13 +67,14 @@ module Rack
63
67
  return EMPTY unless boundary
64
68
 
65
69
  io = BoundedIO.new(io, content_length) if content_length
70
+ outbuf = String.new
66
71
 
67
72
  parser = new(boundary, tmpfile, bufsize, qp)
68
- parser.on_read io.read(bufsize), io.eof?
73
+ parser.on_read io.read(bufsize, outbuf)
69
74
 
70
75
  loop do
71
76
  break if parser.state == :DONE
72
- parser.on_read io.read(bufsize), io.eof?
77
+ parser.on_read io.read(bufsize, outbuf)
73
78
  end
74
79
 
75
80
  io.rewind
@@ -92,14 +97,8 @@ module Rack
92
97
  # those which give the lone filename.
93
98
  fn = filename.split(/[\/\\]/).last
94
99
 
95
- data = {:filename => fn, :type => content_type,
96
- :name => name, :tempfile => body, :head => head}
97
- elsif !filename && content_type && body.is_a?(IO)
98
- body.rewind
99
-
100
- # Generic multipart cases, not coming from a form
101
- data = {:type => content_type,
102
- :name => name, :tempfile => body, :head => head}
100
+ data = { filename: fn, type: content_type,
101
+ name: name, tempfile: body, head: head }
103
102
  end
104
103
 
105
104
  yield data
@@ -118,7 +117,7 @@ module Rack
118
117
 
119
118
  include Enumerable
120
119
 
121
- def initialize tempfile
120
+ def initialize(tempfile)
122
121
  @tempfile = tempfile
123
122
  @mime_parts = []
124
123
  @open_files = 0
@@ -128,7 +127,7 @@ module Rack
128
127
  @mime_parts.each { |part| yield part }
129
128
  end
130
129
 
131
- def on_mime_head mime_index, head, filename, content_type, name
130
+ def on_mime_head(mime_index, head, filename, content_type, name)
132
131
  if filename
133
132
  body = @tempfile.call(filename, content_type)
134
133
  body.binmode if body.respond_to?(:binmode)
@@ -140,14 +139,15 @@ module Rack
140
139
  end
141
140
 
142
141
  @mime_parts[mime_index] = klass.new(body, head, filename, content_type, name)
142
+
143
143
  check_open_files
144
144
  end
145
145
 
146
- def on_mime_body mime_index, content
146
+ def on_mime_body(mime_index, content)
147
147
  @mime_parts[mime_index].body << content
148
148
  end
149
149
 
150
- def on_mime_finish mime_index
150
+ def on_mime_finish(mime_index)
151
151
  end
152
152
 
153
153
  private
@@ -165,25 +165,26 @@ module Rack
165
165
  attr_reader :state
166
166
 
167
167
  def initialize(boundary, tempfile, bufsize, query_parser)
168
- @buf = String.new
169
-
170
168
  @query_parser = query_parser
171
169
  @params = query_parser.make_params
172
170
  @boundary = "--#{boundary}"
173
171
  @bufsize = bufsize
174
172
 
175
- @rx = /(?:#{EOL})?#{Regexp.quote(@boundary)}(#{EOL}|--)/n
176
- @rx_max_size = EOL.size + @boundary.bytesize + [EOL.size, '--'.size].max
177
173
  @full_boundary = @boundary
178
174
  @end_boundary = @boundary + '--'
179
175
  @state = :FAST_FORWARD
180
176
  @mime_index = 0
181
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
182
183
  end
183
184
 
184
- def on_read content, eof
185
- handle_empty_content!(content, eof)
186
- @buf << content
185
+ def on_read(content)
186
+ handle_empty_content!(content)
187
+ @sbuf.concat content
187
188
  run_parser
188
189
  end
189
190
 
@@ -194,7 +195,6 @@ module Rack
194
195
  @query_parser.normalize_params(@params, part.name, data, @query_parser.param_depth_limit)
195
196
  end
196
197
  end
197
-
198
198
  MultipartInfo.new @params.to_params_hash, @collector.find_all(&:file?).map(&:body)
199
199
  end
200
200
 
@@ -221,7 +221,7 @@ module Rack
221
221
  if consume_boundary
222
222
  @state = :MIME_HEAD
223
223
  else
224
- raise EOFError, "bad content body" if @buf.bytesize >= @bufsize
224
+ raise EOFError, "bad content body" if @sbuf.rest_size >= @bufsize
225
225
  :want_read
226
226
  end
227
227
  end
@@ -229,19 +229,16 @@ module Rack
229
229
  def handle_consume_token
230
230
  tok = consume_boundary
231
231
  # break if we're at the end of a buffer, but not if it is the end of a field
232
- if tok == :END_BOUNDARY || (@buf.empty? && tok != :BOUNDARY)
233
- @state = :DONE
232
+ @state = if tok == :END_BOUNDARY || (@sbuf.eos? && tok != :BOUNDARY)
233
+ :DONE
234
234
  else
235
- @state = :MIME_HEAD
235
+ :MIME_HEAD
236
236
  end
237
237
  end
238
238
 
239
239
  def handle_mime_head
240
- if @buf.index(EOL + EOL)
241
- i = @buf.index(EOL+EOL)
242
- head = @buf.slice!(0, i+2) # First \r\n
243
- @buf.slice!(0, 2) # Second \r\n
244
-
240
+ if @sbuf.scan_until(@head_regex)
241
+ head = @sbuf[1]
245
242
  content_type = head[MULTIPART_CONTENT_TYPE, 1]
246
243
  if name = head[MULTIPART_CONTENT_DISPOSITION, 1]
247
244
  name = Rack::Auth::Digest::Params::dequote(name)
@@ -252,7 +249,7 @@ module Rack
252
249
  filename = get_filename(head)
253
250
 
254
251
  if name.nil? || name.empty?
255
- name = filename || "#{content_type || TEXT_PLAIN}[]"
252
+ name = filename || "#{content_type || TEXT_PLAIN}[]".dup
256
253
  end
257
254
 
258
255
  @collector.on_mime_head @mime_index, head, filename, content_type, name
@@ -263,16 +260,19 @@ module Rack
263
260
  end
264
261
 
265
262
  def handle_mime_body
266
- if i = @buf.index(rx)
267
- # Save the rest.
268
- @collector.on_mime_body @mime_index, @buf.slice!(0, i)
269
- @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
270
267
  @state = :CONSUME_TOKEN
271
268
  @mime_index += 1
272
269
  else
273
- # Save the read body part.
274
- if @rx_max_size < @buf.size
275
- @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
276
276
  end
277
277
  :want_read
278
278
  end
@@ -280,16 +280,13 @@ module Rack
280
280
 
281
281
  def full_boundary; @full_boundary; end
282
282
 
283
- def rx; @rx; end
284
-
285
283
  def consume_boundary
286
- while @buf.gsub!(/\A([^\n]*(?:\n|\Z))/, '')
287
- read_buffer = $1
284
+ while read_buffer = @sbuf.scan_until(BOUNDARY_REGEX)
288
285
  case read_buffer.strip
289
286
  when full_boundary then return :BOUNDARY
290
287
  when @end_boundary then return :END_BOUNDARY
291
288
  end
292
- return if @buf.empty?
289
+ return if @sbuf.eos?
293
290
  end
294
291
  end
295
292
 
@@ -310,8 +307,8 @@ module Rack
310
307
 
311
308
  return unless filename
312
309
 
313
- if filename.scan(/%.?.?/).all? { |s| s =~ /%[0-9a-fA-F]{2}/ }
314
- filename = Utils.unescape(filename)
310
+ if filename.scan(/%.?.?/).all? { |s| /%[0-9a-fA-F]{2}/.match?(s) }
311
+ filename = Utils.unescape_path(filename)
315
312
  end
316
313
 
317
314
  filename.scrub!
@@ -327,7 +324,7 @@ module Rack
327
324
  filename
328
325
  end
329
326
 
330
- CHARSET = "charset"
327
+ CHARSET = "charset"
331
328
 
332
329
  def tag_multipart_encoding(filename, content_type, name, body)
333
330
  name = name.to_s
@@ -342,12 +339,12 @@ module Rack
342
339
  type_subtype = list.first
343
340
  type_subtype.strip!
344
341
  if TEXT_PLAIN == type_subtype
345
- rest = list.drop 1
342
+ rest = list.drop 1
346
343
  rest.each do |param|
347
- k,v = param.split('=', 2)
344
+ k, v = param.split('=', 2)
348
345
  k.strip!
349
346
  v.strip!
350
- v = v[1..-2] if v[0] == '"' && v[-1] == '"'
347
+ v = v[1..-2] if v.start_with?('"') && v.end_with?('"')
351
348
  encoding = Encoding.find v if k == CHARSET
352
349
  end
353
350
  end
@@ -357,11 +354,9 @@ module Rack
357
354
  body.force_encoding(encoding)
358
355
  end
359
356
 
360
-
361
- def handle_empty_content!(content, eof)
357
+ def handle_empty_content!(content)
362
358
  if content.nil? || content.empty?
363
- raise EOFError if eof
364
- return true
359
+ raise EOFError
365
360
  end
366
361
  end
367
362
  end