rack 2.0.9.3 → 3.0.0

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 (201) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +808 -0
  3. data/CONTRIBUTING.md +142 -0
  4. data/{COPYING → MIT-LICENSE} +4 -2
  5. data/README.md +293 -0
  6. data/SPEC.rdoc +340 -0
  7. data/lib/rack/auth/abstract/handler.rb +6 -2
  8. data/lib/rack/auth/abstract/request.rb +4 -2
  9. data/lib/rack/auth/basic.rb +7 -4
  10. data/lib/rack/auth/digest/md5.rb +1 -129
  11. data/lib/rack/auth/digest/nonce.rb +1 -51
  12. data/lib/rack/auth/digest/params.rb +1 -52
  13. data/lib/rack/auth/digest/request.rb +1 -41
  14. data/lib/rack/auth/digest.rb +256 -0
  15. data/lib/rack/body_proxy.rb +18 -15
  16. data/lib/rack/builder.rb +151 -40
  17. data/lib/rack/cascade.rb +30 -12
  18. data/lib/rack/chunked.rb +74 -23
  19. data/lib/rack/common_logger.rb +49 -36
  20. data/lib/rack/conditional_get.rb +33 -26
  21. data/lib/rack/config.rb +2 -0
  22. data/lib/rack/constants.rb +63 -0
  23. data/lib/rack/content_length.rb +13 -16
  24. data/lib/rack/content_type.rb +12 -8
  25. data/lib/rack/deflater.rb +84 -45
  26. data/lib/rack/directory.rb +90 -64
  27. data/lib/rack/etag.rb +17 -23
  28. data/lib/rack/events.rb +23 -20
  29. data/lib/rack/file.rb +5 -172
  30. data/lib/rack/files.rb +216 -0
  31. data/lib/rack/head.rb +10 -9
  32. data/lib/rack/headers.rb +154 -0
  33. data/lib/rack/lint.rb +786 -645
  34. data/lib/rack/lock.rb +4 -6
  35. data/lib/rack/logger.rb +4 -0
  36. data/lib/rack/media_type.rb +10 -5
  37. data/lib/rack/method_override.rb +8 -2
  38. data/lib/rack/mime.rb +17 -1
  39. data/lib/rack/mock.rb +2 -195
  40. data/lib/rack/mock_request.rb +166 -0
  41. data/lib/rack/mock_response.rb +126 -0
  42. data/lib/rack/multipart/generator.rb +21 -15
  43. data/lib/rack/multipart/parser.rb +161 -118
  44. data/lib/rack/multipart/uploaded_file.rb +19 -7
  45. data/lib/rack/multipart.rb +23 -41
  46. data/lib/rack/null_logger.rb +11 -0
  47. data/lib/rack/query_parser.rb +126 -65
  48. data/lib/rack/recursive.rb +9 -5
  49. data/lib/rack/reloader.rb +6 -4
  50. data/lib/rack/request.rb +331 -74
  51. data/lib/rack/response.rb +223 -70
  52. data/lib/rack/rewindable_input.rb +28 -8
  53. data/lib/rack/runtime.rb +11 -8
  54. data/lib/rack/sendfile.rb +42 -33
  55. data/lib/rack/show_exceptions.rb +35 -18
  56. data/lib/rack/show_status.rb +25 -15
  57. data/lib/rack/static.rb +30 -18
  58. data/lib/rack/tempfile_reaper.rb +16 -5
  59. data/lib/rack/urlmap.rb +14 -6
  60. data/lib/rack/utils.rb +268 -260
  61. data/lib/rack/version.rb +34 -0
  62. data/lib/rack.rb +15 -92
  63. metadata +44 -207
  64. data/HISTORY.md +0 -520
  65. data/README.rdoc +0 -316
  66. data/Rakefile +0 -116
  67. data/SPEC +0 -263
  68. data/bin/rackup +0 -4
  69. data/contrib/rack.png +0 -0
  70. data/contrib/rack.svg +0 -150
  71. data/contrib/rack_logo.svg +0 -164
  72. data/contrib/rdoc.css +0 -412
  73. data/example/lobster.ru +0 -4
  74. data/example/protectedlobster.rb +0 -14
  75. data/example/protectedlobster.ru +0 -8
  76. data/lib/rack/handler/cgi.rb +0 -60
  77. data/lib/rack/handler/fastcgi.rb +0 -100
  78. data/lib/rack/handler/lsws.rb +0 -61
  79. data/lib/rack/handler/scgi.rb +0 -70
  80. data/lib/rack/handler/thin.rb +0 -36
  81. data/lib/rack/handler/webrick.rb +0 -120
  82. data/lib/rack/handler.rb +0 -99
  83. data/lib/rack/lobster.rb +0 -70
  84. data/lib/rack/server.rb +0 -395
  85. data/lib/rack/session/abstract/id.rb +0 -510
  86. data/lib/rack/session/cookie.rb +0 -204
  87. data/lib/rack/session/memcache.rb +0 -99
  88. data/lib/rack/session/pool.rb +0 -83
  89. data/rack.gemspec +0 -34
  90. data/test/builder/an_underscore_app.rb +0 -5
  91. data/test/builder/anything.rb +0 -5
  92. data/test/builder/comment.ru +0 -4
  93. data/test/builder/end.ru +0 -5
  94. data/test/builder/line.ru +0 -1
  95. data/test/builder/options.ru +0 -2
  96. data/test/cgi/assets/folder/test.js +0 -1
  97. data/test/cgi/assets/fonts/font.eot +0 -1
  98. data/test/cgi/assets/images/image.png +0 -1
  99. data/test/cgi/assets/index.html +0 -1
  100. data/test/cgi/assets/javascripts/app.js +0 -1
  101. data/test/cgi/assets/stylesheets/app.css +0 -1
  102. data/test/cgi/lighttpd.conf +0 -26
  103. data/test/cgi/rackup_stub.rb +0 -6
  104. data/test/cgi/sample_rackup.ru +0 -5
  105. data/test/cgi/test +0 -9
  106. data/test/cgi/test+directory/test+file +0 -1
  107. data/test/cgi/test.fcgi +0 -9
  108. data/test/cgi/test.gz +0 -0
  109. data/test/cgi/test.ru +0 -5
  110. data/test/gemloader.rb +0 -10
  111. data/test/helper.rb +0 -34
  112. data/test/multipart/bad_robots +0 -259
  113. data/test/multipart/binary +0 -0
  114. data/test/multipart/content_type_and_no_filename +0 -6
  115. data/test/multipart/empty +0 -10
  116. data/test/multipart/fail_16384_nofile +0 -814
  117. data/test/multipart/file1.txt +0 -1
  118. data/test/multipart/filename_and_modification_param +0 -7
  119. data/test/multipart/filename_and_no_name +0 -6
  120. data/test/multipart/filename_with_encoded_words +0 -7
  121. data/test/multipart/filename_with_escaped_quotes +0 -6
  122. data/test/multipart/filename_with_escaped_quotes_and_modification_param +0 -7
  123. data/test/multipart/filename_with_null_byte +0 -7
  124. data/test/multipart/filename_with_percent_escaped_quotes +0 -6
  125. data/test/multipart/filename_with_single_quote +0 -7
  126. data/test/multipart/filename_with_unescaped_percentages +0 -6
  127. data/test/multipart/filename_with_unescaped_percentages2 +0 -6
  128. data/test/multipart/filename_with_unescaped_percentages3 +0 -6
  129. data/test/multipart/filename_with_unescaped_quotes +0 -6
  130. data/test/multipart/ie +0 -6
  131. data/test/multipart/invalid_character +0 -6
  132. data/test/multipart/mixed_files +0 -21
  133. data/test/multipart/nested +0 -10
  134. data/test/multipart/none +0 -9
  135. data/test/multipart/quoted +0 -15
  136. data/test/multipart/rack-logo.png +0 -0
  137. data/test/multipart/semicolon +0 -6
  138. data/test/multipart/text +0 -15
  139. data/test/multipart/three_files_three_fields +0 -31
  140. data/test/multipart/unity3d_wwwform +0 -11
  141. data/test/multipart/webkit +0 -32
  142. data/test/rackup/config.ru +0 -31
  143. data/test/registering_handler/rack/handler/registering_myself.rb +0 -8
  144. data/test/spec_auth_basic.rb +0 -89
  145. data/test/spec_auth_digest.rb +0 -260
  146. data/test/spec_body_proxy.rb +0 -85
  147. data/test/spec_builder.rb +0 -233
  148. data/test/spec_cascade.rb +0 -63
  149. data/test/spec_cgi.rb +0 -84
  150. data/test/spec_chunked.rb +0 -103
  151. data/test/spec_common_logger.rb +0 -107
  152. data/test/spec_conditional_get.rb +0 -103
  153. data/test/spec_config.rb +0 -23
  154. data/test/spec_content_length.rb +0 -86
  155. data/test/spec_content_type.rb +0 -46
  156. data/test/spec_deflater.rb +0 -375
  157. data/test/spec_directory.rb +0 -148
  158. data/test/spec_etag.rb +0 -108
  159. data/test/spec_events.rb +0 -133
  160. data/test/spec_fastcgi.rb +0 -85
  161. data/test/spec_file.rb +0 -264
  162. data/test/spec_handler.rb +0 -57
  163. data/test/spec_head.rb +0 -46
  164. data/test/spec_lint.rb +0 -520
  165. data/test/spec_lobster.rb +0 -59
  166. data/test/spec_lock.rb +0 -204
  167. data/test/spec_logger.rb +0 -24
  168. data/test/spec_media_type.rb +0 -42
  169. data/test/spec_method_override.rb +0 -110
  170. data/test/spec_mime.rb +0 -51
  171. data/test/spec_mock.rb +0 -359
  172. data/test/spec_multipart.rb +0 -721
  173. data/test/spec_null_logger.rb +0 -21
  174. data/test/spec_recursive.rb +0 -75
  175. data/test/spec_request.rb +0 -1423
  176. data/test/spec_response.rb +0 -528
  177. data/test/spec_rewindable_input.rb +0 -128
  178. data/test/spec_runtime.rb +0 -50
  179. data/test/spec_sendfile.rb +0 -125
  180. data/test/spec_server.rb +0 -193
  181. data/test/spec_session_abstract_id.rb +0 -31
  182. data/test/spec_session_abstract_session_hash.rb +0 -45
  183. data/test/spec_session_cookie.rb +0 -442
  184. data/test/spec_session_memcache.rb +0 -357
  185. data/test/spec_session_persisted_secure_secure_session_hash.rb +0 -73
  186. data/test/spec_session_pool.rb +0 -247
  187. data/test/spec_show_exceptions.rb +0 -93
  188. data/test/spec_show_status.rb +0 -104
  189. data/test/spec_static.rb +0 -184
  190. data/test/spec_tempfile_reaper.rb +0 -64
  191. data/test/spec_thin.rb +0 -96
  192. data/test/spec_urlmap.rb +0 -237
  193. data/test/spec_utils.rb +0 -742
  194. data/test/spec_version.rb +0 -11
  195. data/test/spec_webrick.rb +0 -206
  196. data/test/static/another/index.html +0 -1
  197. data/test/static/foo.html +0 -1
  198. data/test/static/index.html +0 -1
  199. data/test/testrequest.rb +0 -78
  200. data/test/unregistered_handler/rack/handler/unregistered.rb +0 -7
  201. data/test/unregistered_handler/rack/handler/unregistered_long_one.rb +0 -7
@@ -1,3 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'uploaded_file'
4
+
1
5
  module Rack
2
6
  module Multipart
3
7
  class Generator
@@ -15,9 +19,13 @@ module Rack
15
19
 
16
20
  flattened_params.map do |name, file|
17
21
  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)
22
+ if file.path
23
+ ::File.open(file.path, 'rb') do |f|
24
+ f.set_encoding(Encoding::BINARY)
25
+ content_for_tempfile(f, file, name)
26
+ end
27
+ else
28
+ content_for_tempfile(file, file, name)
21
29
  end
22
30
  else
23
31
  content_for_other(file, name)
@@ -27,21 +35,18 @@ module Rack
27
35
 
28
36
  private
29
37
  def multipart?
30
- multipart = false
31
-
32
38
  query = lambda { |value|
33
39
  case value
34
40
  when Array
35
- value.each(&query)
41
+ value.any?(&query)
36
42
  when Hash
37
- value.values.each(&query)
43
+ value.values.any?(&query)
38
44
  when Rack::Multipart::UploadedFile
39
- multipart = true
45
+ true
40
46
  end
41
47
  }
42
- @params.values.each(&query)
43
48
 
44
- multipart
49
+ @params.values.any?(&query)
45
50
  end
46
51
 
47
52
  def flattened_params
@@ -70,12 +75,13 @@ module Rack
70
75
  end
71
76
 
72
77
  def content_for_tempfile(io, file, name)
78
+ length = ::File.stat(file.path).size if file.path
79
+ filename = "; filename=\"#{Utils.escape_path(file.original_filename)}\""
73
80
  <<-EOF
74
81
  --#{MULTIPART_BOUNDARY}\r
75
- Content-Disposition: form-data; name="#{name}"; filename="#{Utils.escape(file.original_filename)}"\r
76
- Content-Type: #{file.content_type}\r
77
- Content-Length: #{::File.stat(file.path).size}\r
78
- \r
82
+ content-disposition: form-data; name="#{name}"#{filename}\r
83
+ content-type: #{file.content_type}\r
84
+ #{"content-length: #{length}\r\n" if length}\r
79
85
  #{io.read}\r
80
86
  EOF
81
87
  end
@@ -83,7 +89,7 @@ EOF
83
89
  def content_for_other(file, name)
84
90
  <<-EOF
85
91
  --#{MULTIPART_BOUNDARY}\r
86
- Content-Disposition: form-data; name="#{name}"\r
92
+ content-disposition: form-data; name="#{name}"\r
87
93
  \r
88
94
  #{file}\r
89
95
  EOF
@@ -1,15 +1,51 @@
1
- require 'rack/utils'
1
+ # frozen_string_literal: true
2
+
3
+ require 'strscan'
4
+
5
+ require_relative '../utils'
2
6
 
3
7
  module Rack
4
8
  module Multipart
5
9
  class MultipartPartLimitError < Errno::EMFILE; end
6
- class MultipartTotalPartLimitError < StandardError; end
10
+
11
+ # Use specific error class when parsing multipart request
12
+ # that ends early.
13
+ class EmptyContentError < ::EOFError; end
14
+
15
+ # Base class for multipart exceptions that do not subclass from
16
+ # other exception classes for backwards compatibility.
17
+ class Error < StandardError; end
18
+
19
+ EOL = "\r\n"
20
+ MULTIPART = %r|\Amultipart/.*boundary=\"?([^\";,]+)\"?|ni
21
+ TOKEN = /[^\s()<>,;:\\"\/\[\]?=]+/
22
+ CONDISP = /Content-Disposition:\s*#{TOKEN}\s*/i
23
+ VALUE = /"(?:\\"|[^"])*"|#{TOKEN}/
24
+ BROKEN = /^#{CONDISP}.*;\s*filename=(#{VALUE})/i
25
+ MULTIPART_CONTENT_TYPE = /Content-Type: (.*)#{EOL}/ni
26
+ MULTIPART_CONTENT_DISPOSITION = /Content-Disposition:.*;\s*name=(#{VALUE})/ni
27
+ MULTIPART_CONTENT_ID = /Content-ID:\s*([^#{EOL}]*)/ni
28
+ # Updated definitions from RFC 2231
29
+ ATTRIBUTE_CHAR = %r{[^ \t\v\n\r)(><@,;:\\"/\[\]?='*%]}
30
+ ATTRIBUTE = /#{ATTRIBUTE_CHAR}+/
31
+ SECTION = /\*[0-9]+/
32
+ REGULAR_PARAMETER_NAME = /#{ATTRIBUTE}#{SECTION}?/
33
+ REGULAR_PARAMETER = /(#{REGULAR_PARAMETER_NAME})=(#{VALUE})/
34
+ EXTENDED_OTHER_NAME = /#{ATTRIBUTE}\*[1-9][0-9]*\*/
35
+ EXTENDED_OTHER_VALUE = /%[0-9a-fA-F]{2}|#{ATTRIBUTE_CHAR}/
36
+ EXTENDED_OTHER_PARAMETER = /(#{EXTENDED_OTHER_NAME})=(#{EXTENDED_OTHER_VALUE}*)/
37
+ EXTENDED_INITIAL_NAME = /#{ATTRIBUTE}(?:\*0)?\*/
38
+ EXTENDED_INITIAL_VALUE = /[a-zA-Z0-9\-]*'[a-zA-Z0-9\-]*'#{EXTENDED_OTHER_VALUE}*/
39
+ EXTENDED_INITIAL_PARAMETER = /(#{EXTENDED_INITIAL_NAME})=(#{EXTENDED_INITIAL_VALUE})/
40
+ EXTENDED_PARAMETER = /#{EXTENDED_INITIAL_PARAMETER}|#{EXTENDED_OTHER_PARAMETER}/
41
+ DISPPARM = /;\s*(?:#{REGULAR_PARAMETER}|#{EXTENDED_PARAMETER})\s*/
42
+ RFC2183 = /^#{CONDISP}(#{DISPPARM})+$/i
7
43
 
8
44
  class Parser
9
- BUFSIZE = 16384
45
+ BUFSIZE = 1_048_576
10
46
  TEXT_PLAIN = "text/plain"
11
47
  TEMPFILE_FACTORY = lambda { |filename, content_type|
12
- Tempfile.new(["RackMultipart", ::File.extname(filename.gsub("\0".freeze, '%00'.freeze))])
48
+ Tempfile.new(["RackMultipart", ::File.extname(filename.gsub("\0", '%00'))])
13
49
  }
14
50
 
15
51
  class BoundedIO # :nodoc:
@@ -19,30 +55,26 @@ module Rack
19
55
  @cursor = 0
20
56
  end
21
57
 
22
- def read(size)
58
+ def read(size, outbuf = nil)
23
59
  return if @cursor >= @content_length
24
60
 
25
61
  left = @content_length - @cursor
26
62
 
27
63
  str = if left < size
28
- @io.read left
64
+ @io.read left, outbuf
29
65
  else
30
- @io.read size
66
+ @io.read size, outbuf
31
67
  end
32
68
 
33
69
  if str
34
70
  @cursor += str.bytesize
35
71
  else
36
- # Raise an error for mismatching Content-Length and actual contents
72
+ # Raise an error for mismatching content-length and actual contents
37
73
  raise EOFError, "bad content body"
38
74
  end
39
75
 
40
76
  str
41
77
  end
42
-
43
- def rewind
44
- @io.rewind
45
- end
46
78
  end
47
79
 
48
80
  MultipartInfo = Struct.new :params, :tmp_files
@@ -61,17 +93,17 @@ module Rack
61
93
  boundary = parse_boundary content_type
62
94
  return EMPTY unless boundary
63
95
 
96
+ if boundary.length > 70
97
+ # RFC 1521 Section 7.2.1 imposes a 70 character maximum for the boundary.
98
+ # Most clients use no more than 55 characters.
99
+ raise Error, "multipart boundary size too large (#{boundary.length} characters)"
100
+ end
101
+
64
102
  io = BoundedIO.new(io, content_length) if content_length
65
103
 
66
104
  parser = new(boundary, tmpfile, bufsize, qp)
67
- parser.on_read io.read(bufsize)
68
-
69
- loop do
70
- break if parser.state == :DONE
71
- parser.on_read io.read(bufsize)
72
- end
105
+ parser.parse(io)
73
106
 
74
- io.rewind
75
107
  parser.result
76
108
  end
77
109
 
@@ -91,14 +123,8 @@ module Rack
91
123
  # those which give the lone filename.
92
124
  fn = filename.split(/[\/\\]/).last
93
125
 
94
- data = {:filename => fn, :type => content_type,
95
- :name => name, :tempfile => body, :head => head}
96
- elsif !filename && content_type && body.is_a?(IO)
97
- body.rewind
98
-
99
- # Generic multipart cases, not coming from a form
100
- data = {:type => content_type,
101
- :name => name, :tempfile => body, :head => head}
126
+ data = { filename: fn, type: content_type,
127
+ name: name, tempfile: body, head: head }
102
128
  end
103
129
 
104
130
  yield data
@@ -117,7 +143,7 @@ module Rack
117
143
 
118
144
  include Enumerable
119
145
 
120
- def initialize tempfile
146
+ def initialize(tempfile)
121
147
  @tempfile = tempfile
122
148
  @mime_parts = []
123
149
  @open_files = 0
@@ -127,7 +153,7 @@ module Rack
127
153
  @mime_parts.each { |part| yield part }
128
154
  end
129
155
 
130
- def on_mime_head mime_index, head, filename, content_type, name
156
+ def on_mime_head(mime_index, head, filename, content_type, name)
131
157
  if filename
132
158
  body = @tempfile.call(filename, content_type)
133
159
  body.binmode if body.respond_to?(:binmode)
@@ -140,121 +166,131 @@ module Rack
140
166
 
141
167
  @mime_parts[mime_index] = klass.new(body, head, filename, content_type, name)
142
168
 
143
- check_part_limits
169
+ check_open_files
144
170
  end
145
171
 
146
- def on_mime_body mime_index, content
172
+ def on_mime_body(mime_index, content)
147
173
  @mime_parts[mime_index].body << content
148
174
  end
149
175
 
150
- def on_mime_finish mime_index
176
+ def on_mime_finish(mime_index)
151
177
  end
152
178
 
153
179
  private
154
180
 
155
- def check_part_limits
156
- file_limit = Utils.multipart_file_limit
157
- part_limit = Utils.multipart_total_part_limit
158
-
159
- if file_limit && file_limit > 0
160
- if @open_files >= file_limit
181
+ def check_open_files
182
+ if Utils.multipart_part_limit > 0
183
+ if @open_files >= Utils.multipart_part_limit
161
184
  @mime_parts.each(&:close)
162
185
  raise MultipartPartLimitError, 'Maximum file multiparts in content reached'
163
186
  end
164
187
  end
165
-
166
- if part_limit && part_limit > 0
167
- if @mime_parts.size >= part_limit
168
- @mime_parts.each(&:close)
169
- raise MultipartTotalPartLimitError, 'Maximum total multiparts in content reached'
170
- end
171
- end
172
188
  end
173
189
  end
174
190
 
175
191
  attr_reader :state
176
192
 
177
193
  def initialize(boundary, tempfile, bufsize, query_parser)
178
- @buf = String.new
179
-
180
194
  @query_parser = query_parser
181
195
  @params = query_parser.make_params
182
- @boundary = "--#{boundary}"
183
196
  @bufsize = bufsize
184
197
 
185
- @rx = /(?:#{EOL})?#{Regexp.quote(@boundary)}(#{EOL}|--)/n
186
- @rx_max_size = EOL.size + @boundary.bytesize + [EOL.size, '--'.size].max
187
- @full_boundary = @boundary
188
- @end_boundary = @boundary + '--'
189
198
  @state = :FAST_FORWARD
190
199
  @mime_index = 0
191
200
  @collector = Collector.new tempfile
201
+
202
+ @sbuf = StringScanner.new("".dup)
203
+ @body_regex = /(?:#{EOL}|\A)--#{Regexp.quote(boundary)}(?:#{EOL}|--)/m
204
+ @rx_max_size = boundary.bytesize + 6 # (\r\n-- at start, either \r\n or -- at finish)
205
+ @head_regex = /(.*?#{EOL})#{EOL}/m
192
206
  end
193
207
 
194
- def on_read content
195
- handle_empty_content!(content)
196
- @buf << content
197
- run_parser
208
+ def parse(io)
209
+ outbuf = String.new
210
+ read_data(io, outbuf)
211
+
212
+ loop do
213
+ status =
214
+ case @state
215
+ when :FAST_FORWARD
216
+ handle_fast_forward
217
+ when :CONSUME_TOKEN
218
+ handle_consume_token
219
+ when :MIME_HEAD
220
+ handle_mime_head
221
+ when :MIME_BODY
222
+ handle_mime_body
223
+ else # when :DONE
224
+ return
225
+ end
226
+
227
+ read_data(io, outbuf) if status == :want_read
228
+ end
198
229
  end
199
230
 
200
231
  def result
201
232
  @collector.each do |part|
202
233
  part.get_data do |data|
203
234
  tag_multipart_encoding(part.filename, part.content_type, part.name, data)
204
- @query_parser.normalize_params(@params, part.name, data, @query_parser.param_depth_limit)
235
+ @query_parser.normalize_params(@params, part.name, data)
205
236
  end
206
237
  end
207
-
208
238
  MultipartInfo.new @params.to_params_hash, @collector.find_all(&:file?).map(&:body)
209
239
  end
210
240
 
211
241
  private
212
242
 
213
- def run_parser
214
- loop do
215
- case @state
216
- when :FAST_FORWARD
217
- break if handle_fast_forward == :want_read
218
- when :CONSUME_TOKEN
219
- break if handle_consume_token == :want_read
220
- when :MIME_HEAD
221
- break if handle_mime_head == :want_read
222
- when :MIME_BODY
223
- break if handle_mime_body == :want_read
224
- when :DONE
225
- break
226
- end
227
- end
243
+ def dequote(str) # From WEBrick::HTTPUtils
244
+ ret = (/\A"(.*)"\Z/ =~ str) ? $1 : str.dup
245
+ ret.gsub!(/\\(.)/, "\\1")
246
+ ret
228
247
  end
229
248
 
249
+ def read_data(io, outbuf)
250
+ content = io.read(@bufsize, outbuf)
251
+ handle_empty_content!(content)
252
+ @sbuf.concat(content)
253
+ end
254
+
255
+ # This handles the initial parser state. We read until we find the starting
256
+ # boundary, then we can transition to the next state. If we find the ending
257
+ # boundary, this is an invalid multipart upload, but keep scanning for opening
258
+ # boundary in that case. If no boundary found, we need to keep reading data
259
+ # and retry. It's highly unlikely the initial read will not consume the
260
+ # boundary. The client would have to deliberately craft a response
261
+ # with the opening boundary beyond the buffer size for that to happen.
230
262
  def handle_fast_forward
231
- if consume_boundary
232
- @state = :MIME_HEAD
233
- else
234
- raise EOFError, "bad content body" if @buf.bytesize >= @bufsize
235
- :want_read
263
+ while true
264
+ case consume_boundary
265
+ when :BOUNDARY
266
+ # found opening boundary, transition to next state
267
+ @state = :MIME_HEAD
268
+ return
269
+ when :END_BOUNDARY
270
+ # invalid multipart upload, but retry for opening boundary
271
+ else
272
+ # no boundary found, keep reading data
273
+ return :want_read
274
+ end
236
275
  end
237
276
  end
238
277
 
239
278
  def handle_consume_token
240
279
  tok = consume_boundary
241
280
  # break if we're at the end of a buffer, but not if it is the end of a field
242
- if tok == :END_BOUNDARY || (@buf.empty? && tok != :BOUNDARY)
243
- @state = :DONE
281
+ @state = if tok == :END_BOUNDARY || (@sbuf.eos? && tok != :BOUNDARY)
282
+ :DONE
244
283
  else
245
- @state = :MIME_HEAD
284
+ :MIME_HEAD
246
285
  end
247
286
  end
248
287
 
249
288
  def handle_mime_head
250
- if @buf.index(EOL + EOL)
251
- i = @buf.index(EOL+EOL)
252
- head = @buf.slice!(0, i+2) # First \r\n
253
- @buf.slice!(0, 2) # Second \r\n
254
-
289
+ if @sbuf.scan_until(@head_regex)
290
+ head = @sbuf[1]
255
291
  content_type = head[MULTIPART_CONTENT_TYPE, 1]
256
292
  if name = head[MULTIPART_CONTENT_DISPOSITION, 1]
257
- name = Rack::Auth::Digest::Params::dequote(name)
293
+ name = dequote(name)
258
294
  else
259
295
  name = head[MULTIPART_CONTENT_ID, 1]
260
296
  end
@@ -262,7 +298,7 @@ module Rack
262
298
  filename = get_filename(head)
263
299
 
264
300
  if name.nil? || name.empty?
265
- name = filename || "#{content_type || TEXT_PLAIN}[]"
301
+ name = filename || "#{content_type || TEXT_PLAIN}[]".dup
266
302
  end
267
303
 
268
304
  @collector.on_mime_head @mime_index, head, filename, content_type, name
@@ -273,33 +309,34 @@ module Rack
273
309
  end
274
310
 
275
311
  def handle_mime_body
276
- if i = @buf.index(rx)
277
- # Save the rest.
278
- @collector.on_mime_body @mime_index, @buf.slice!(0, i)
279
- @buf.slice!(0, 2) # Remove \r\n after the content
312
+ if (body_with_boundary = @sbuf.check_until(@body_regex)) # check but do not advance the pointer yet
313
+ body = body_with_boundary.sub(/#{@body_regex}\z/m, '') # remove the boundary from the string
314
+ @collector.on_mime_body @mime_index, body
315
+ @sbuf.pos += body.length + 2 # skip \r\n after the content
280
316
  @state = :CONSUME_TOKEN
281
317
  @mime_index += 1
282
318
  else
283
- # Save the read body part.
284
- if @rx_max_size < @buf.size
285
- @collector.on_mime_body @mime_index, @buf.slice!(0, @buf.size - @rx_max_size)
319
+ # Save what we have so far
320
+ if @rx_max_size < @sbuf.rest_size
321
+ delta = @sbuf.rest_size - @rx_max_size
322
+ @collector.on_mime_body @mime_index, @sbuf.peek(delta)
323
+ @sbuf.pos += delta
324
+ @sbuf.string = @sbuf.rest
286
325
  end
287
326
  :want_read
288
327
  end
289
328
  end
290
329
 
291
- def full_boundary; @full_boundary; end
292
-
293
- def rx; @rx; end
294
-
330
+ # Scan until the we find the start or end of the boundary.
331
+ # If we find it, return the appropriate symbol for the start or
332
+ # end of the boundary. If we don't find the start or end of the
333
+ # boundary, clear the buffer and return nil.
295
334
  def consume_boundary
296
- while @buf.gsub!(/\A([^\n]*(?:\n|\Z))/, '')
297
- read_buffer = $1
298
- case read_buffer.strip
299
- when full_boundary then return :BOUNDARY
300
- when @end_boundary then return :END_BOUNDARY
301
- end
302
- return if @buf.empty?
335
+ if read_buffer = @sbuf.scan_until(@body_regex)
336
+ read_buffer.end_with?(EOL) ? :BOUNDARY : :END_BOUNDARY
337
+ else
338
+ @sbuf.terminate
339
+ nil
303
340
  end
304
341
  end
305
342
 
@@ -309,10 +346,10 @@ module Rack
309
346
  when RFC2183
310
347
  params = Hash[*head.scan(DISPPARM).flat_map(&:compact)]
311
348
 
312
- if filename = params['filename']
313
- filename = $1 if filename =~ /^"(.*)"$/
314
- elsif filename = params['filename*']
349
+ if filename = params['filename*']
315
350
  encoding, _, filename = filename.split("'", 3)
351
+ elsif filename = params['filename']
352
+ filename = $1 if filename =~ /^"(.*)"$/
316
353
  end
317
354
  when BROKEN
318
355
  filename = $1
@@ -321,8 +358,8 @@ module Rack
321
358
 
322
359
  return unless filename
323
360
 
324
- if filename.scan(/%.?.?/).all? { |s| s =~ /%[0-9a-fA-F]{2}/ }
325
- filename = Utils.unescape(filename)
361
+ if filename.scan(/%.?.?/).all? { |s| /%[0-9a-fA-F]{2}/.match?(s) }
362
+ filename = Utils.unescape_path(filename)
326
363
  end
327
364
 
328
365
  filename.scrub!
@@ -338,7 +375,8 @@ module Rack
338
375
  filename
339
376
  end
340
377
 
341
- CHARSET = "charset"
378
+ CHARSET = "charset"
379
+ deprecate_constant :CHARSET
342
380
 
343
381
  def tag_multipart_encoding(filename, content_type, name, body)
344
382
  name = name.to_s
@@ -353,13 +391,19 @@ module Rack
353
391
  type_subtype = list.first
354
392
  type_subtype.strip!
355
393
  if TEXT_PLAIN == type_subtype
356
- rest = list.drop 1
394
+ rest = list.drop 1
357
395
  rest.each do |param|
358
- k,v = param.split('=', 2)
396
+ k, v = param.split('=', 2)
359
397
  k.strip!
360
398
  v.strip!
361
- v = v[1..-2] if v[0] == '"' && v[-1] == '"'
362
- encoding = Encoding.find v if k == CHARSET
399
+ v = v[1..-2] if v.start_with?('"') && v.end_with?('"')
400
+ if k == "charset"
401
+ encoding = begin
402
+ Encoding.find v
403
+ rescue ArgumentError
404
+ Encoding::BINARY
405
+ end
406
+ end
363
407
  end
364
408
  end
365
409
  end
@@ -368,10 +412,9 @@ module Rack
368
412
  body.force_encoding(encoding)
369
413
  end
370
414
 
371
-
372
415
  def handle_empty_content!(content)
373
416
  if content.nil? || content.empty?
374
- raise EOFError
417
+ raise EmptyContentError
375
418
  end
376
419
  end
377
420
  end
@@ -1,23 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'tempfile'
4
+ require 'fileutils'
5
+
1
6
  module Rack
2
7
  module Multipart
3
8
  class UploadedFile
9
+
4
10
  # The filename, *not* including the path, of the "uploaded" file
5
11
  attr_reader :original_filename
6
12
 
7
13
  # The content type of the "uploaded" file
8
14
  attr_accessor :content_type
9
15
 
10
- def initialize(path, content_type = "text/plain", binary = false)
11
- raise "#{path} file does not exist" unless ::File.exist?(path)
16
+ def initialize(filepath = nil, ct = "text/plain", bin = false,
17
+ path: filepath, content_type: ct, binary: bin, filename: nil, io: nil)
18
+ if io
19
+ @tempfile = io
20
+ @original_filename = filename
21
+ else
22
+ raise "#{path} file does not exist" unless ::File.exist?(path)
23
+ @original_filename = filename || ::File.basename(path)
24
+ @tempfile = Tempfile.new([@original_filename, ::File.extname(path)], encoding: Encoding::BINARY)
25
+ @tempfile.binmode if binary
26
+ FileUtils.copy_file(path, @tempfile.path)
27
+ end
12
28
  @content_type = content_type
13
- @original_filename = ::File.basename(path)
14
- @tempfile = Tempfile.new([@original_filename, ::File.extname(path)], encoding: Encoding::BINARY)
15
- @tempfile.binmode if binary
16
- FileUtils.copy_file(path, @tempfile.path)
17
29
  end
18
30
 
19
31
  def path
20
- @tempfile.path
32
+ @tempfile.path if @tempfile.respond_to?(:path)
21
33
  end
22
34
  alias_method :local_path, :path
23
35
 
@@ -1,62 +1,44 @@
1
- require 'rack/multipart/parser'
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'constants'
4
+ require_relative 'utils'
5
+
6
+ require_relative 'multipart/parser'
7
+ require_relative 'multipart/generator'
2
8
 
3
9
  module Rack
4
10
  # A multipart form data parser, adapted from IOWA.
5
11
  #
6
12
  # Usually, Rack::Request#POST takes care of calling this.
7
13
  module Multipart
8
- autoload :UploadedFile, 'rack/multipart/uploaded_file'
9
- autoload :Generator, 'rack/multipart/generator'
10
-
11
- EOL = "\r\n"
12
14
  MULTIPART_BOUNDARY = "AaB03x"
13
- MULTIPART = %r|\Amultipart/.*boundary=\"?([^\";,]+)\"?|ni
14
- TOKEN = /[^\s()<>,;:\\"\/\[\]?=]+/
15
- CONDISP = /Content-Disposition:\s*#{TOKEN}\s*/i
16
- VALUE = /"(?:\\"|[^"])*"|#{TOKEN}/
17
- BROKEN = /^#{CONDISP}.*;\s*filename=(#{VALUE})/i
18
- MULTIPART_CONTENT_TYPE = /Content-Type: (.*)#{EOL}/ni
19
- MULTIPART_CONTENT_DISPOSITION = /Content-Disposition:[^:]*;\s+name=(#{VALUE})/ni
20
- MULTIPART_CONTENT_ID = /Content-ID:\s*([^#{EOL}]*)/ni
21
- # Updated definitions from RFC 2231
22
- ATTRIBUTE_CHAR = %r{[^ \x00-\x1f\x7f)(><@,;:\\"/\[\]?='*%]}
23
- ATTRIBUTE = /#{ATTRIBUTE_CHAR}+/
24
- SECTION = /\*[0-9]+/
25
- REGULAR_PARAMETER_NAME = /#{ATTRIBUTE}#{SECTION}?/
26
- REGULAR_PARAMETER = /(#{REGULAR_PARAMETER_NAME})=(#{VALUE})/
27
- EXTENDED_OTHER_NAME = /#{ATTRIBUTE}\*[1-9][0-9]*\*/
28
- EXTENDED_OTHER_VALUE = /%[0-9a-fA-F]{2}|#{ATTRIBUTE_CHAR}/
29
- EXTENDED_OTHER_PARAMETER = /(#{EXTENDED_OTHER_NAME})=(#{EXTENDED_OTHER_VALUE}*)/
30
- EXTENDED_INITIAL_NAME = /#{ATTRIBUTE}(?:\*0)?\*/
31
- EXTENDED_INITIAL_VALUE = /[a-zA-Z0-9\-]*'[a-zA-Z0-9\-]*'#{EXTENDED_OTHER_VALUE}*/
32
- EXTENDED_INITIAL_PARAMETER = /(#{EXTENDED_INITIAL_NAME})=(#{EXTENDED_INITIAL_VALUE})/
33
- EXTENDED_PARAMETER = /#{EXTENDED_INITIAL_PARAMETER}|#{EXTENDED_OTHER_PARAMETER}/
34
- DISPPARM = /;\s*(?:#{REGULAR_PARAMETER}|#{EXTENDED_PARAMETER})\s*/
35
- RFC2183 = /^#{CONDISP}(#{DISPPARM})+$/i
36
15
 
37
16
  class << self
38
17
  def parse_multipart(env, params = Rack::Utils.default_query_parser)
39
- extract_multipart Rack::Request.new(env), params
40
- end
18
+ io = env[RACK_INPUT]
19
+
20
+ if content_length = env['CONTENT_LENGTH']
21
+ content_length = content_length.to_i
22
+ end
41
23
 
42
- def extract_multipart(req, params = Rack::Utils.default_query_parser)
43
- io = req.get_header(RACK_INPUT)
44
- io.rewind
45
- content_length = req.content_length
46
- content_length = content_length.to_i if content_length
24
+ content_type = env['CONTENT_TYPE']
47
25
 
48
- tempfile = req.get_header(RACK_MULTIPART_TEMPFILE_FACTORY) || Parser::TEMPFILE_FACTORY
49
- bufsize = req.get_header(RACK_MULTIPART_BUFFER_SIZE) || Parser::BUFSIZE
26
+ tempfile = env[RACK_MULTIPART_TEMPFILE_FACTORY] || Parser::TEMPFILE_FACTORY
27
+ bufsize = env[RACK_MULTIPART_BUFFER_SIZE] || Parser::BUFSIZE
50
28
 
51
- info = Parser.parse io, content_length, req.get_header('CONTENT_TYPE'), tempfile, bufsize, params
52
- req.set_header(RACK_TEMPFILES, info.tmp_files)
53
- info.params
29
+ info = Parser.parse(io, content_length, content_type, tempfile, bufsize, params)
30
+ env[RACK_TEMPFILES] = info.tmp_files
31
+
32
+ return info.params
33
+ end
34
+
35
+ def extract_multipart(request, params = Rack::Utils.default_query_parser)
36
+ parse_multipart(request.env)
54
37
  end
55
38
 
56
39
  def build_multipart(params, first = true)
57
40
  Generator.new(params, first).dump
58
41
  end
59
42
  end
60
-
61
43
  end
62
44
  end