rack 1.6.13 → 2.1.4.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 (188) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +92 -0
  3. data/{COPYING → MIT-LICENSE} +4 -2
  4. data/README.rdoc +105 -141
  5. data/Rakefile +27 -28
  6. data/SPEC +6 -7
  7. data/bin/rackup +1 -0
  8. data/contrib/rack_logo.svg +164 -111
  9. data/example/lobster.ru +2 -0
  10. data/example/protectedlobster.rb +4 -2
  11. data/example/protectedlobster.ru +3 -1
  12. data/lib/rack/auth/abstract/handler.rb +3 -1
  13. data/lib/rack/auth/abstract/request.rb +7 -1
  14. data/lib/rack/auth/basic.rb +4 -1
  15. data/lib/rack/auth/digest/md5.rb +9 -7
  16. data/lib/rack/auth/digest/nonce.rb +6 -3
  17. data/lib/rack/auth/digest/params.rb +5 -4
  18. data/lib/rack/auth/digest/request.rb +3 -1
  19. data/lib/rack/body_proxy.rb +11 -9
  20. data/lib/rack/builder.rb +42 -18
  21. data/lib/rack/cascade.rb +6 -5
  22. data/lib/rack/chunked.rb +33 -10
  23. data/lib/rack/{commonlogger.rb → common_logger.rb} +14 -10
  24. data/lib/rack/{conditionalget.rb → conditional_get.rb} +3 -1
  25. data/lib/rack/config.rb +2 -0
  26. data/lib/rack/content_length.rb +5 -3
  27. data/lib/rack/content_type.rb +3 -1
  28. data/lib/rack/core_ext/regexp.rb +14 -0
  29. data/lib/rack/deflater.rb +33 -53
  30. data/lib/rack/directory.rb +75 -60
  31. data/lib/rack/etag.rb +8 -5
  32. data/lib/rack/events.rb +156 -0
  33. data/lib/rack/file.rb +4 -149
  34. data/lib/rack/files.rb +178 -0
  35. data/lib/rack/handler/cgi.rb +18 -17
  36. data/lib/rack/handler/fastcgi.rb +17 -16
  37. data/lib/rack/handler/lsws.rb +14 -12
  38. data/lib/rack/handler/scgi.rb +22 -19
  39. data/lib/rack/handler/thin.rb +6 -1
  40. data/lib/rack/handler/webrick.rb +28 -28
  41. data/lib/rack/handler.rb +9 -26
  42. data/lib/rack/head.rb +17 -17
  43. data/lib/rack/lint.rb +55 -52
  44. data/lib/rack/lobster.rb +8 -6
  45. data/lib/rack/lock.rb +17 -10
  46. data/lib/rack/logger.rb +4 -2
  47. data/lib/rack/media_type.rb +43 -0
  48. data/lib/rack/{methodoverride.rb → method_override.rb} +10 -8
  49. data/lib/rack/mime.rb +27 -6
  50. data/lib/rack/mock.rb +101 -60
  51. data/lib/rack/multipart/generator.rb +11 -12
  52. data/lib/rack/multipart/parser.rb +292 -161
  53. data/lib/rack/multipart/uploaded_file.rb +3 -2
  54. data/lib/rack/multipart.rb +38 -8
  55. data/lib/rack/{nulllogger.rb → null_logger.rb} +3 -1
  56. data/lib/rack/query_parser.rb +218 -0
  57. data/lib/rack/recursive.rb +11 -9
  58. data/lib/rack/reloader.rb +10 -4
  59. data/lib/rack/request.rb +447 -305
  60. data/lib/rack/response.rb +196 -83
  61. data/lib/rack/rewindable_input.rb +5 -14
  62. data/lib/rack/runtime.rb +12 -18
  63. data/lib/rack/sendfile.rb +19 -14
  64. data/lib/rack/server.rb +118 -41
  65. data/lib/rack/session/abstract/id.rb +139 -94
  66. data/lib/rack/session/cookie.rb +34 -26
  67. data/lib/rack/session/memcache.rb +4 -93
  68. data/lib/rack/session/pool.rb +12 -10
  69. data/lib/rack/show_exceptions.rb +392 -0
  70. data/lib/rack/{showstatus.rb → show_status.rb} +7 -5
  71. data/lib/rack/static.rb +41 -11
  72. data/lib/rack/tempfile_reaper.rb +4 -2
  73. data/lib/rack/urlmap.rb +25 -15
  74. data/lib/rack/utils.rb +203 -277
  75. data/lib/rack.rb +76 -24
  76. data/rack.gemspec +25 -14
  77. metadata +62 -183
  78. data/HISTORY.md +0 -375
  79. data/KNOWN-ISSUES +0 -44
  80. data/lib/rack/backports/uri/common_18.rb +0 -56
  81. data/lib/rack/backports/uri/common_192.rb +0 -52
  82. data/lib/rack/backports/uri/common_193.rb +0 -29
  83. data/lib/rack/handler/evented_mongrel.rb +0 -8
  84. data/lib/rack/handler/mongrel.rb +0 -106
  85. data/lib/rack/handler/swiftiplied_mongrel.rb +0 -8
  86. data/lib/rack/showexceptions.rb +0 -387
  87. data/lib/rack/utils/okjson.rb +0 -600
  88. data/test/builder/anything.rb +0 -5
  89. data/test/builder/comment.ru +0 -4
  90. data/test/builder/end.ru +0 -5
  91. data/test/builder/line.ru +0 -1
  92. data/test/builder/options.ru +0 -2
  93. data/test/cgi/assets/folder/test.js +0 -1
  94. data/test/cgi/assets/fonts/font.eot +0 -1
  95. data/test/cgi/assets/images/image.png +0 -1
  96. data/test/cgi/assets/index.html +0 -1
  97. data/test/cgi/assets/javascripts/app.js +0 -1
  98. data/test/cgi/assets/stylesheets/app.css +0 -1
  99. data/test/cgi/lighttpd.conf +0 -26
  100. data/test/cgi/rackup_stub.rb +0 -6
  101. data/test/cgi/sample_rackup.ru +0 -5
  102. data/test/cgi/test +0 -9
  103. data/test/cgi/test+directory/test+file +0 -1
  104. data/test/cgi/test.fcgi +0 -8
  105. data/test/cgi/test.ru +0 -5
  106. data/test/gemloader.rb +0 -10
  107. data/test/multipart/bad_robots +0 -259
  108. data/test/multipart/binary +0 -0
  109. data/test/multipart/content_type_and_no_filename +0 -6
  110. data/test/multipart/empty +0 -10
  111. data/test/multipart/fail_16384_nofile +0 -814
  112. data/test/multipart/file1.txt +0 -1
  113. data/test/multipart/filename_and_modification_param +0 -7
  114. data/test/multipart/filename_and_no_name +0 -6
  115. data/test/multipart/filename_with_escaped_quotes +0 -6
  116. data/test/multipart/filename_with_escaped_quotes_and_modification_param +0 -7
  117. data/test/multipart/filename_with_null_byte +0 -7
  118. data/test/multipart/filename_with_percent_escaped_quotes +0 -6
  119. data/test/multipart/filename_with_unescaped_percentages +0 -6
  120. data/test/multipart/filename_with_unescaped_percentages2 +0 -6
  121. data/test/multipart/filename_with_unescaped_percentages3 +0 -6
  122. data/test/multipart/filename_with_unescaped_quotes +0 -6
  123. data/test/multipart/ie +0 -6
  124. data/test/multipart/invalid_character +0 -6
  125. data/test/multipart/mixed_files +0 -21
  126. data/test/multipart/nested +0 -10
  127. data/test/multipart/none +0 -9
  128. data/test/multipart/semicolon +0 -6
  129. data/test/multipart/text +0 -15
  130. data/test/multipart/three_files_three_fields +0 -31
  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 -81
  135. data/test/spec_auth_digest.rb +0 -259
  136. data/test/spec_body_proxy.rb +0 -85
  137. data/test/spec_builder.rb +0 -223
  138. data/test/spec_cascade.rb +0 -61
  139. data/test/spec_cgi.rb +0 -102
  140. data/test/spec_chunked.rb +0 -101
  141. data/test/spec_commonlogger.rb +0 -93
  142. data/test/spec_conditionalget.rb +0 -102
  143. data/test/spec_config.rb +0 -22
  144. data/test/spec_content_length.rb +0 -85
  145. data/test/spec_content_type.rb +0 -45
  146. data/test/spec_deflater.rb +0 -339
  147. data/test/spec_directory.rb +0 -88
  148. data/test/spec_etag.rb +0 -107
  149. data/test/spec_fastcgi.rb +0 -107
  150. data/test/spec_file.rb +0 -221
  151. data/test/spec_handler.rb +0 -72
  152. data/test/spec_head.rb +0 -45
  153. data/test/spec_lint.rb +0 -550
  154. data/test/spec_lobster.rb +0 -58
  155. data/test/spec_lock.rb +0 -164
  156. data/test/spec_logger.rb +0 -23
  157. data/test/spec_methodoverride.rb +0 -111
  158. data/test/spec_mime.rb +0 -51
  159. data/test/spec_mock.rb +0 -297
  160. data/test/spec_mongrel.rb +0 -182
  161. data/test/spec_multipart.rb +0 -600
  162. data/test/spec_nulllogger.rb +0 -20
  163. data/test/spec_recursive.rb +0 -72
  164. data/test/spec_request.rb +0 -1232
  165. data/test/spec_response.rb +0 -407
  166. data/test/spec_rewindable_input.rb +0 -118
  167. data/test/spec_runtime.rb +0 -49
  168. data/test/spec_sendfile.rb +0 -130
  169. data/test/spec_server.rb +0 -167
  170. data/test/spec_session_abstract_id.rb +0 -53
  171. data/test/spec_session_cookie.rb +0 -410
  172. data/test/spec_session_memcache.rb +0 -358
  173. data/test/spec_session_persisted_secure_secure_session_hash.rb +0 -73
  174. data/test/spec_session_pool.rb +0 -246
  175. data/test/spec_showexceptions.rb +0 -98
  176. data/test/spec_showstatus.rb +0 -103
  177. data/test/spec_static.rb +0 -145
  178. data/test/spec_tempfile_reaper.rb +0 -63
  179. data/test/spec_thin.rb +0 -91
  180. data/test/spec_urlmap.rb +0 -236
  181. data/test/spec_utils.rb +0 -647
  182. data/test/spec_version.rb +0 -17
  183. data/test/spec_webrick.rb +0 -184
  184. data/test/static/another/index.html +0 -1
  185. data/test/static/index.html +0 -1
  186. data/test/testrequest.rb +0 -78
  187. data/test/unregistered_handler/rack/handler/unregistered.rb +0 -7
  188. data/test/unregistered_handler/rack/handler/unregistered_long_one.rb +0 -7
@@ -1,252 +1,383 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'rack/utils'
4
+ require 'strscan'
5
+ require 'rack/core_ext/regexp'
2
6
 
3
7
  module Rack
4
8
  module Multipart
5
9
  class MultipartPartLimitError < Errno::EMFILE; end
10
+ class MultipartTotalPartLimitError < StandardError; end
6
11
 
7
12
  class Parser
8
- BUFSIZE = 16384
9
- DUMMY = Struct.new(:parse).new
13
+ using ::Rack::RegexpExtensions
10
14
 
11
- def self.create(env)
12
- return DUMMY unless env['CONTENT_TYPE'] =~ MULTIPART
15
+ BUFSIZE = 1_048_576
16
+ TEXT_PLAIN = "text/plain"
17
+ TEMPFILE_FACTORY = lambda { |filename, content_type|
18
+ Tempfile.new(["RackMultipart", ::File.extname(filename.gsub("\0", '%00'))])
19
+ }
13
20
 
14
- io = env['rack.input']
15
- io.rewind
21
+ BOUNDARY_REGEX = /\A([^\n]*(?:\n|\Z))/
16
22
 
17
- content_length = env['CONTENT_LENGTH']
18
- content_length = content_length.to_i if content_length
23
+ class BoundedIO # :nodoc:
24
+ def initialize(io, content_length)
25
+ @io = io
26
+ @content_length = content_length
27
+ @cursor = 0
28
+ end
19
29
 
20
- tempfile = env['rack.multipart.tempfile_factory'] ||
21
- lambda { |filename, content_type| Tempfile.new(["RackMultipart", ::File.extname(filename.gsub("\0".freeze, '%00'.freeze))]) }
22
- bufsize = env['rack.multipart.buffer_size'] || BUFSIZE
30
+ def read(size, outbuf = nil)
31
+ return if @cursor >= @content_length
23
32
 
24
- new($1, io, content_length, env, tempfile, bufsize)
25
- end
33
+ left = @content_length - @cursor
26
34
 
27
- def initialize(boundary, io, content_length, env, tempfile, bufsize)
28
- @buf = ""
35
+ str = if left < size
36
+ @io.read left, outbuf
37
+ else
38
+ @io.read size, outbuf
39
+ end
29
40
 
30
- if @buf.respond_to? :force_encoding
31
- @buf.force_encoding Encoding::ASCII_8BIT
32
- end
41
+ if str
42
+ @cursor += str.bytesize
43
+ else
44
+ # Raise an error for mismatching Content-Length and actual contents
45
+ raise EOFError, "bad content body"
46
+ end
33
47
 
34
- @params = Utils::KeySpaceConstrainedParams.new
35
- @boundary = "--#{boundary}"
36
- @io = io
37
- @content_length = content_length
38
- @boundary_size = Utils.bytesize(@boundary) + EOL.size
39
- @env = env
40
- @tempfile = tempfile
41
- @bufsize = bufsize
48
+ str
49
+ end
42
50
 
43
- if @content_length
44
- @content_length -= @boundary_size
51
+ def rewind
52
+ @io.rewind
45
53
  end
54
+ end
55
+
56
+ MultipartInfo = Struct.new :params, :tmp_files
57
+ EMPTY = MultipartInfo.new(nil, [])
46
58
 
47
- @rx = /(?:#{EOL})?#{Regexp.quote(@boundary)}(#{EOL}|--)/n
48
- @full_boundary = @boundary + EOL
59
+ def self.parse_boundary(content_type)
60
+ return unless content_type
61
+ data = content_type.match(MULTIPART)
62
+ return unless data
63
+ data[1]
49
64
  end
50
65
 
51
- def parse
52
- fast_forward_to_first_boundary
66
+ def self.parse(io, content_length, content_type, tmpfile, bufsize, qp)
67
+ return EMPTY if 0 == content_length
68
+
69
+ boundary = parse_boundary content_type
70
+ return EMPTY unless boundary
71
+
72
+ io = BoundedIO.new(io, content_length) if content_length
73
+ outbuf = String.new
74
+
75
+ parser = new(boundary, tmpfile, bufsize, qp)
76
+ parser.on_read io.read(bufsize, outbuf)
53
77
 
54
- opened_files = 0
55
78
  loop do
79
+ break if parser.state == :DONE
80
+ parser.on_read io.read(bufsize, outbuf)
81
+ end
82
+
83
+ io.rewind
84
+ parser.result
85
+ end
56
86
 
57
- head, filename, content_type, name, body =
58
- get_current_head_and_filename_and_content_type_and_name_and_body
87
+ class Collector
88
+ class MimePart < Struct.new(:body, :head, :filename, :content_type, :name)
89
+ def get_data
90
+ data = body
91
+ if filename == ""
92
+ # filename is blank which means no file has been selected
93
+ return
94
+ elsif filename
95
+ body.rewind if body.respond_to?(:rewind)
96
+
97
+ # Take the basename of the upload's original filename.
98
+ # This handles the full Windows paths given by Internet Explorer
99
+ # (and perhaps other broken user agents) without affecting
100
+ # those which give the lone filename.
101
+ fn = filename.split(/[\/\\]/).last
102
+
103
+ data = { filename: fn, type: content_type,
104
+ name: name, tempfile: body, head: head }
105
+ elsif !filename && content_type && body.is_a?(IO)
106
+ body.rewind
107
+
108
+ # Generic multipart cases, not coming from a form
109
+ data = { type: content_type,
110
+ name: name, tempfile: body, head: head }
111
+ end
59
112
 
60
- if Utils.multipart_part_limit > 0
61
- opened_files += 1 if filename
62
- raise MultipartPartLimitError, 'Maximum file multiparts in content reached' if opened_files >= Utils.multipart_part_limit
113
+ yield data
63
114
  end
115
+ end
116
+
117
+ class BufferPart < MimePart
118
+ def file?; false; end
119
+ def close; end
120
+ end
64
121
 
65
- # Save the rest.
66
- if i = @buf.index(rx)
67
- body << @buf.slice!(0, i)
68
- @buf.slice!(0, @boundary_size+2)
122
+ class TempfilePart < MimePart
123
+ def file?; true; end
124
+ def close; body.close; end
125
+ end
69
126
 
70
- @content_length = -1 if $1 == "--"
71
- end
127
+ include Enumerable
72
128
 
73
- get_data(filename, body, content_type, name, head) do |data|
74
- tag_multipart_encoding(filename, content_type, name, data)
129
+ def initialize tempfile
130
+ @tempfile = tempfile
131
+ @mime_parts = []
132
+ @open_files = 0
133
+ end
134
+
135
+ def each
136
+ @mime_parts.each { |part| yield part }
137
+ end
75
138
 
76
- Utils.normalize_params(@params, name, data)
139
+ def on_mime_head mime_index, head, filename, content_type, name
140
+ if filename
141
+ body = @tempfile.call(filename, content_type)
142
+ body.binmode if body.respond_to?(:binmode)
143
+ klass = TempfilePart
144
+ @open_files += 1
145
+ else
146
+ body = String.new
147
+ klass = BufferPart
77
148
  end
78
149
 
79
- # break if we're at the end of a buffer, but not if it is the end of a field
80
- break if (@buf.empty? && $1 != EOL) || @content_length == -1
81
- end
150
+ @mime_parts[mime_index] = klass.new(body, head, filename, content_type, name)
82
151
 
83
- @io.rewind
152
+ check_part_limits
153
+ end
84
154
 
85
- @params.to_params_hash
86
- end
155
+ def on_mime_body mime_index, content
156
+ @mime_parts[mime_index].body << content
157
+ end
87
158
 
88
- private
89
- def full_boundary; @full_boundary; end
159
+ def on_mime_finish mime_index
160
+ end
90
161
 
91
- def rx; @rx; end
162
+ private
92
163
 
93
- def fast_forward_to_first_boundary
94
- loop do
95
- content = @io.read(@bufsize)
96
- raise EOFError, "bad content body" unless content
97
- @buf << content
164
+ def check_part_limits
165
+ file_limit = Utils.multipart_file_limit
166
+ part_limit = Utils.multipart_total_part_limit
98
167
 
99
- while @buf.gsub!(/\A([^\n]*\n)/, '')
100
- read_buffer = $1
101
- return if read_buffer == full_boundary
168
+ if file_limit && file_limit > 0
169
+ if @open_files >= file_limit
170
+ @mime_parts.each(&:close)
171
+ raise MultipartPartLimitError, 'Maximum file multiparts in content reached'
172
+ end
102
173
  end
103
174
 
104
- raise EOFError, "bad content body" if Utils.bytesize(@buf) >= @bufsize
175
+ if part_limit && part_limit > 0
176
+ if @mime_parts.size >= part_limit
177
+ @mime_parts.each(&:close)
178
+ raise MultipartTotalPartLimitError, 'Maximum total multiparts in content reached'
179
+ end
180
+ end
105
181
  end
106
182
  end
107
183
 
108
- def get_current_head_and_filename_and_content_type_and_name_and_body
109
- head = nil
110
- body = ''
184
+ attr_reader :state
111
185
 
112
- if body.respond_to? :force_encoding
113
- body.force_encoding Encoding::ASCII_8BIT
114
- end
186
+ def initialize(boundary, tempfile, bufsize, query_parser)
187
+ @query_parser = query_parser
188
+ @params = query_parser.make_params
189
+ @boundary = "--#{boundary}"
190
+ @bufsize = bufsize
115
191
 
116
- filename = content_type = name = nil
192
+ @full_boundary = @boundary
193
+ @end_boundary = @boundary + '--'
194
+ @state = :FAST_FORWARD
195
+ @mime_index = 0
196
+ @collector = Collector.new tempfile
117
197
 
118
- until head && @buf =~ rx
119
- if !head && i = @buf.index(EOL+EOL)
120
- head = @buf.slice!(0, i+2) # First \r\n
198
+ @sbuf = StringScanner.new("".dup)
199
+ @body_regex = /(?:#{EOL})?#{Regexp.quote(@boundary)}(?:#{EOL}|--)/m
200
+ @rx_max_size = EOL.size + @boundary.bytesize + [EOL.size, '--'.size].max
201
+ @head_regex = /(.*?#{EOL})#{EOL}/m
202
+ end
121
203
 
122
- @buf.slice!(0, 2) # Second \r\n
204
+ def on_read content
205
+ handle_empty_content!(content)
206
+ @sbuf.concat content
207
+ run_parser
208
+ end
123
209
 
124
- content_type = head[MULTIPART_CONTENT_TYPE, 1]
125
- name = head[MULTIPART_CONTENT_DISPOSITION, 1] || head[MULTIPART_CONTENT_ID, 1]
210
+ def result
211
+ @collector.each do |part|
212
+ part.get_data do |data|
213
+ tag_multipart_encoding(part.filename, part.content_type, part.name, data)
214
+ @query_parser.normalize_params(@params, part.name, data, @query_parser.param_depth_limit)
215
+ end
216
+ end
217
+ MultipartInfo.new @params.to_params_hash, @collector.find_all(&:file?).map(&:body)
218
+ end
126
219
 
127
- filename = get_filename(head)
220
+ private
128
221
 
129
- if name.nil? || name.empty? && filename
130
- name = filename
131
- end
222
+ def run_parser
223
+ loop do
224
+ case @state
225
+ when :FAST_FORWARD
226
+ break if handle_fast_forward == :want_read
227
+ when :CONSUME_TOKEN
228
+ break if handle_consume_token == :want_read
229
+ when :MIME_HEAD
230
+ break if handle_mime_head == :want_read
231
+ when :MIME_BODY
232
+ break if handle_mime_body == :want_read
233
+ when :DONE
234
+ break
235
+ end
236
+ end
237
+ end
132
238
 
133
- if filename
134
- (@env['rack.tempfiles'] ||= []) << body = @tempfile.call(filename, content_type)
135
- body.binmode if body.respond_to?(:binmode)
136
- end
239
+ def handle_fast_forward
240
+ if consume_boundary
241
+ @state = :MIME_HEAD
242
+ else
243
+ raise EOFError, "bad content body" if @sbuf.rest_size >= @bufsize
244
+ :want_read
245
+ end
246
+ end
247
+
248
+ def handle_consume_token
249
+ tok = consume_boundary
250
+ # break if we're at the end of a buffer, but not if it is the end of a field
251
+ @state = if tok == :END_BOUNDARY || (@sbuf.eos? && tok != :BOUNDARY)
252
+ :DONE
253
+ else
254
+ :MIME_HEAD
255
+ end
256
+ end
137
257
 
138
- next
258
+ def handle_mime_head
259
+ if @sbuf.scan_until(@head_regex)
260
+ head = @sbuf[1]
261
+ content_type = head[MULTIPART_CONTENT_TYPE, 1]
262
+ if name = head[MULTIPART_CONTENT_DISPOSITION, 1]
263
+ name = Rack::Auth::Digest::Params::dequote(name)
264
+ else
265
+ name = head[MULTIPART_CONTENT_ID, 1]
139
266
  end
140
267
 
141
- # Save the read body part.
142
- if head && (@boundary_size+4 < @buf.size)
143
- body << @buf.slice!(0, @buf.size - (@boundary_size+4))
268
+ filename = get_filename(head)
269
+
270
+ if name.nil? || name.empty?
271
+ name = filename || "#{content_type || TEXT_PLAIN}[]".dup
144
272
  end
145
273
 
146
- content = @io.read(@content_length && @bufsize >= @content_length ? @content_length : @bufsize)
147
- raise EOFError, "bad content body" if content.nil? || content.empty?
274
+ @collector.on_mime_head @mime_index, head, filename, content_type, name
275
+ @state = :MIME_BODY
276
+ else
277
+ :want_read
278
+ end
279
+ end
148
280
 
149
- @buf << content
150
- @content_length -= content.size if @content_length
281
+ def handle_mime_body
282
+ if (body_with_boundary = @sbuf.check_until(@body_regex)) # check but do not advance the pointer yet
283
+ body = body_with_boundary.sub(/#{@body_regex}\z/m, '') # remove the boundary from the string
284
+ @collector.on_mime_body @mime_index, body
285
+ @sbuf.pos += body.length + 2 # skip \r\n after the content
286
+ @state = :CONSUME_TOKEN
287
+ @mime_index += 1
288
+ else
289
+ # Save what we have so far
290
+ if @rx_max_size < @sbuf.rest_size
291
+ delta = @sbuf.rest_size - @rx_max_size
292
+ @collector.on_mime_body @mime_index, @sbuf.peek(delta)
293
+ @sbuf.pos += delta
294
+ @sbuf.string = @sbuf.rest
295
+ end
296
+ :want_read
151
297
  end
298
+ end
152
299
 
153
- [head, filename, content_type, name, body]
300
+ def full_boundary; @full_boundary; end
301
+
302
+ def consume_boundary
303
+ while read_buffer = @sbuf.scan_until(BOUNDARY_REGEX)
304
+ case read_buffer.strip
305
+ when full_boundary then return :BOUNDARY
306
+ when @end_boundary then return :END_BOUNDARY
307
+ end
308
+ return if @sbuf.eos?
309
+ end
154
310
  end
155
311
 
156
312
  def get_filename(head)
157
313
  filename = nil
158
314
  case head
159
315
  when RFC2183
160
- filename = Hash[head.scan(DISPPARM)]['filename']
161
- filename = $1 if filename and filename =~ /^"(.*)"$/
162
- when BROKEN_QUOTED, BROKEN_UNQUOTED
316
+ params = Hash[*head.scan(DISPPARM).flat_map(&:compact)]
317
+
318
+ if filename = params['filename']
319
+ filename = $1 if filename =~ /^"(.*)"$/
320
+ elsif filename = params['filename*']
321
+ encoding, _, filename = filename.split("'", 3)
322
+ end
323
+ when BROKEN
163
324
  filename = $1
325
+ filename = $1 if filename =~ /^"(.*)"$/
164
326
  end
165
327
 
166
328
  return unless filename
167
329
 
168
- if filename.scan(/%.?.?/).all? { |s| s =~ /%[0-9a-fA-F]{2}/ }
169
- filename = Utils.unescape(filename)
330
+ if filename.scan(/%.?.?/).all? { |s| /%[0-9a-fA-F]{2}/.match?(s) }
331
+ filename = Utils.unescape_path(filename)
170
332
  end
171
333
 
172
- scrub_filename filename
334
+ filename.scrub!
173
335
 
174
336
  if filename !~ /\\[^\\"]/
175
337
  filename = filename.gsub(/\\(.)/, '\1')
176
338
  end
177
- filename
178
- end
179
339
 
180
- if "<3".respond_to? :valid_encoding?
181
- def scrub_filename(filename)
182
- unless filename.valid_encoding?
183
- # FIXME: this force_encoding is for Ruby 2.0 and 1.9 support.
184
- # We can remove it after they are dropped
185
- filename.force_encoding(Encoding::ASCII_8BIT)
186
- filename.encode!(:invalid => :replace, :undef => :replace)
187
- end
340
+ if encoding
341
+ filename.force_encoding ::Encoding.find(encoding)
188
342
  end
189
343
 
190
- CHARSET = "charset"
191
- TEXT_PLAIN = "text/plain"
344
+ filename
345
+ end
346
+
347
+ CHARSET = "charset"
192
348
 
193
- def tag_multipart_encoding(filename, content_type, name, body)
194
- name.force_encoding Encoding::UTF_8
349
+ def tag_multipart_encoding(filename, content_type, name, body)
350
+ name = name.to_s
351
+ encoding = Encoding::UTF_8
195
352
 
196
- return if filename
353
+ name.force_encoding(encoding)
197
354
 
198
- encoding = Encoding::UTF_8
355
+ return if filename
199
356
 
200
- if content_type
201
- list = content_type.split(';')
202
- type_subtype = list.first
203
- type_subtype.strip!
204
- if TEXT_PLAIN == type_subtype
205
- rest = list.drop 1
206
- rest.each do |param|
207
- k,v = param.split('=', 2)
208
- k.strip!
209
- v.strip!
210
- encoding = Encoding.find v if k == CHARSET
211
- end
357
+ if content_type
358
+ list = content_type.split(';')
359
+ type_subtype = list.first
360
+ type_subtype.strip!
361
+ if TEXT_PLAIN == type_subtype
362
+ rest = list.drop 1
363
+ rest.each do |param|
364
+ k, v = param.split('=', 2)
365
+ k.strip!
366
+ v.strip!
367
+ v = v[1..-2] if v.start_with?('"') && v.end_with?('"')
368
+ encoding = Encoding.find v if k == CHARSET
212
369
  end
213
370
  end
214
-
215
- name.force_encoding encoding
216
- body.force_encoding encoding
217
- end
218
- else
219
- def scrub_filename(filename)
220
- end
221
- def tag_multipart_encoding(filename, content_type, name, body)
222
371
  end
223
- end
224
-
225
- def get_data(filename, body, content_type, name, head)
226
- data = body
227
- if filename == ""
228
- # filename is blank which means no file has been selected
229
- return
230
- elsif filename
231
- body.rewind if body.respond_to?(:rewind)
232
-
233
- # Take the basename of the upload's original filename.
234
- # This handles the full Windows paths given by Internet Explorer
235
- # (and perhaps other broken user agents) without affecting
236
- # those which give the lone filename.
237
- filename = filename.split(/[\/\\]/).last
238
372
 
239
- data = {:filename => filename, :type => content_type,
240
- :name => name, :tempfile => body, :head => head}
241
- elsif !filename && content_type && body.is_a?(IO)
242
- body.rewind
373
+ name.force_encoding(encoding)
374
+ body.force_encoding(encoding)
375
+ end
243
376
 
244
- # Generic multipart cases, not coming from a form
245
- data = {:type => content_type,
246
- :name => name, :tempfile => body, :head => head}
377
+ def handle_empty_content!(content)
378
+ if content.nil? || content.empty?
379
+ raise EOFError
247
380
  end
248
-
249
- yield data
250
381
  end
251
382
  end
252
383
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Rack
2
4
  module Multipart
3
5
  class UploadedFile
@@ -11,8 +13,7 @@ module Rack
11
13
  raise "#{path} file does not exist" unless ::File.exist?(path)
12
14
  @content_type = content_type
13
15
  @original_filename = ::File.basename(path)
14
- @tempfile = Tempfile.new([@original_filename, ::File.extname(path)])
15
- @tempfile.set_encoding(Encoding::BINARY) if @tempfile.respond_to?(:set_encoding)
16
+ @tempfile = Tempfile.new([@original_filename, ::File.extname(path)], encoding: Encoding::BINARY)
16
17
  @tempfile.binmode if binary
17
18
  FileUtils.copy_file(path, @tempfile.path)
18
19
  end
@@ -1,10 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rack/multipart/parser'
4
+
1
5
  module Rack
2
6
  # A multipart form data parser, adapted from IOWA.
3
7
  #
4
8
  # Usually, Rack::Request#POST takes care of calling this.
5
9
  module Multipart
6
10
  autoload :UploadedFile, 'rack/multipart/uploaded_file'
7
- autoload :Parser, 'rack/multipart/parser'
8
11
  autoload :Generator, 'rack/multipart/generator'
9
12
 
10
13
  EOL = "\r\n"
@@ -12,17 +15,44 @@ module Rack
12
15
  MULTIPART = %r|\Amultipart/.*boundary=\"?([^\";,]+)\"?|ni
13
16
  TOKEN = /[^\s()<>,;:\\"\/\[\]?=]+/
14
17
  CONDISP = /Content-Disposition:\s*#{TOKEN}\s*/i
15
- DISPPARM = /;\s*(#{TOKEN})=("(?:\\"|[^"])*"|#{TOKEN})/
16
- RFC2183 = /^#{CONDISP}(#{DISPPARM})+$/i
17
- BROKEN_QUOTED = /^#{CONDISP}.*;\sfilename="(.*?)"(?:\s*$|\s*;\s*#{TOKEN}=)/i
18
- BROKEN_UNQUOTED = /^#{CONDISP}.*;\sfilename=(#{TOKEN})/i
18
+ VALUE = /"(?:\\"|[^"])*"|#{TOKEN}/
19
+ BROKEN = /^#{CONDISP}.*;\s*filename=(#{VALUE})/i
19
20
  MULTIPART_CONTENT_TYPE = /Content-Type: (.*)#{EOL}/ni
20
- MULTIPART_CONTENT_DISPOSITION = /Content-Disposition:.*\s+name="?([^\";]*)"?/ni
21
+ MULTIPART_CONTENT_DISPOSITION = /Content-Disposition:[^:]*;\s*name=(#{VALUE})/ni
21
22
  MULTIPART_CONTENT_ID = /Content-ID:\s*([^#{EOL}]*)/ni
23
+ # Updated definitions from RFC 2231
24
+ ATTRIBUTE_CHAR = %r{[^ \x00-\x1f\x7f)(><@,;:\\"/\[\]?='*%]}
25
+ ATTRIBUTE = /#{ATTRIBUTE_CHAR}+/
26
+ SECTION = /\*[0-9]+/
27
+ REGULAR_PARAMETER_NAME = /#{ATTRIBUTE}#{SECTION}?/
28
+ REGULAR_PARAMETER = /(#{REGULAR_PARAMETER_NAME})=(#{VALUE})/
29
+ EXTENDED_OTHER_NAME = /#{ATTRIBUTE}\*[1-9][0-9]*\*/
30
+ EXTENDED_OTHER_VALUE = /%[0-9a-fA-F]{2}|#{ATTRIBUTE_CHAR}/
31
+ EXTENDED_OTHER_PARAMETER = /(#{EXTENDED_OTHER_NAME})=(#{EXTENDED_OTHER_VALUE}*)/
32
+ EXTENDED_INITIAL_NAME = /#{ATTRIBUTE}(?:\*0)?\*/
33
+ EXTENDED_INITIAL_VALUE = /[a-zA-Z0-9\-]*'[a-zA-Z0-9\-]*'#{EXTENDED_OTHER_VALUE}*/
34
+ EXTENDED_INITIAL_PARAMETER = /(#{EXTENDED_INITIAL_NAME})=(#{EXTENDED_INITIAL_VALUE})/
35
+ EXTENDED_PARAMETER = /#{EXTENDED_INITIAL_PARAMETER}|#{EXTENDED_OTHER_PARAMETER}/
36
+ DISPPARM = /;\s*(?:#{REGULAR_PARAMETER}|#{EXTENDED_PARAMETER})\s*/
37
+ RFC2183 = /^#{CONDISP}(#{DISPPARM})+$/i
22
38
 
23
39
  class << self
24
- def parse_multipart(env)
25
- Parser.create(env).parse
40
+ def parse_multipart(env, params = Rack::Utils.default_query_parser)
41
+ extract_multipart Rack::Request.new(env), params
42
+ end
43
+
44
+ def extract_multipart(req, params = Rack::Utils.default_query_parser)
45
+ io = req.get_header(RACK_INPUT)
46
+ io.rewind
47
+ content_length = req.content_length
48
+ content_length = content_length.to_i if content_length
49
+
50
+ tempfile = req.get_header(RACK_MULTIPART_TEMPFILE_FACTORY) || Parser::TEMPFILE_FACTORY
51
+ bufsize = req.get_header(RACK_MULTIPART_BUFFER_SIZE) || Parser::BUFSIZE
52
+
53
+ info = Parser.parse io, content_length, req.get_header('CONTENT_TYPE'), tempfile, bufsize, params
54
+ req.set_header(RACK_TEMPFILES, info.tmp_files)
55
+ info.params
26
56
  end
27
57
 
28
58
  def build_multipart(params, first = true)
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Rack
2
4
  class NullLogger
3
5
  def initialize(app)
@@ -5,7 +7,7 @@ module Rack
5
7
  end
6
8
 
7
9
  def call(env)
8
- env['rack.logger'] = self
10
+ env[RACK_LOGGER] = self
9
11
  @app.call(env)
10
12
  end
11
13