rack 1.6.11 → 2.1.4

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 (187) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +77 -0
  3. data/{COPYING → MIT-LICENSE} +4 -2
  4. data/README.rdoc +89 -139
  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} +11 -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 +54 -51
  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 +280 -161
  53. data/lib/rack/multipart/uploaded_file.rb +3 -2
  54. data/lib/rack/multipart.rb +39 -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 +215 -94
  66. data/lib/rack/session/cookie.rb +45 -28
  67. data/lib/rack/session/memcache.rb +4 -87
  68. data/lib/rack/session/pool.rb +25 -16
  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 +186 -272
  75. data/lib/rack.rb +76 -24
  76. data/rack.gemspec +25 -14
  77. metadata +62 -182
  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 -321
  173. data/test/spec_session_pool.rb +0 -209
  174. data/test/spec_showexceptions.rb +0 -98
  175. data/test/spec_showstatus.rb +0 -103
  176. data/test/spec_static.rb +0 -145
  177. data/test/spec_tempfile_reaper.rb +0 -63
  178. data/test/spec_thin.rb +0 -91
  179. data/test/spec_urlmap.rb +0 -236
  180. data/test/spec_utils.rb +0 -647
  181. data/test/spec_version.rb +0 -17
  182. data/test/spec_webrick.rb +0 -184
  183. data/test/static/another/index.html +0 -1
  184. data/test/static/index.html +0 -1
  185. data/test/testrequest.rb +0 -78
  186. data/test/unregistered_handler/rack/handler/unregistered.rb +0 -7
  187. data/test/unregistered_handler/rack/handler/unregistered_long_one.rb +0 -7
@@ -1,252 +1,371 @@
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
6
10
 
7
11
  class Parser
8
- BUFSIZE = 16384
9
- DUMMY = Struct.new(:parse).new
12
+ using ::Rack::RegexpExtensions
10
13
 
11
- def self.create(env)
12
- return DUMMY unless env['CONTENT_TYPE'] =~ MULTIPART
14
+ BUFSIZE = 1_048_576
15
+ TEXT_PLAIN = "text/plain"
16
+ TEMPFILE_FACTORY = lambda { |filename, content_type|
17
+ Tempfile.new(["RackMultipart", ::File.extname(filename.gsub("\0", '%00'))])
18
+ }
13
19
 
14
- io = env['rack.input']
15
- io.rewind
20
+ BOUNDARY_REGEX = /\A([^\n]*(?:\n|\Z))/
16
21
 
17
- content_length = env['CONTENT_LENGTH']
18
- content_length = content_length.to_i if content_length
22
+ class BoundedIO # :nodoc:
23
+ def initialize(io, content_length)
24
+ @io = io
25
+ @content_length = content_length
26
+ @cursor = 0
27
+ end
19
28
 
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
29
+ def read(size, outbuf = nil)
30
+ return if @cursor >= @content_length
23
31
 
24
- new($1, io, content_length, env, tempfile, bufsize)
25
- end
32
+ left = @content_length - @cursor
26
33
 
27
- def initialize(boundary, io, content_length, env, tempfile, bufsize)
28
- @buf = ""
34
+ str = if left < size
35
+ @io.read left, outbuf
36
+ else
37
+ @io.read size, outbuf
38
+ end
29
39
 
30
- if @buf.respond_to? :force_encoding
31
- @buf.force_encoding Encoding::ASCII_8BIT
32
- end
40
+ if str
41
+ @cursor += str.bytesize
42
+ else
43
+ # Raise an error for mismatching Content-Length and actual contents
44
+ raise EOFError, "bad content body"
45
+ end
33
46
 
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
47
+ str
48
+ end
42
49
 
43
- if @content_length
44
- @content_length -= @boundary_size
50
+ def rewind
51
+ @io.rewind
45
52
  end
53
+ end
54
+
55
+ MultipartInfo = Struct.new :params, :tmp_files
56
+ EMPTY = MultipartInfo.new(nil, [])
46
57
 
47
- @rx = /(?:#{EOL})?#{Regexp.quote(@boundary)}(#{EOL}|--)/n
48
- @full_boundary = @boundary + EOL
58
+ def self.parse_boundary(content_type)
59
+ return unless content_type
60
+ data = content_type.match(MULTIPART)
61
+ return unless data
62
+ data[1]
49
63
  end
50
64
 
51
- def parse
52
- fast_forward_to_first_boundary
65
+ def self.parse(io, content_length, content_type, tmpfile, bufsize, qp)
66
+ return EMPTY if 0 == content_length
67
+
68
+ boundary = parse_boundary content_type
69
+ return EMPTY unless boundary
70
+
71
+ io = BoundedIO.new(io, content_length) if content_length
72
+ outbuf = String.new
73
+
74
+ parser = new(boundary, tmpfile, bufsize, qp)
75
+ parser.on_read io.read(bufsize, outbuf)
53
76
 
54
- opened_files = 0
55
77
  loop do
78
+ break if parser.state == :DONE
79
+ parser.on_read io.read(bufsize, outbuf)
80
+ end
81
+
82
+ io.rewind
83
+ parser.result
84
+ end
56
85
 
57
- head, filename, content_type, name, body =
58
- get_current_head_and_filename_and_content_type_and_name_and_body
86
+ class Collector
87
+ class MimePart < Struct.new(:body, :head, :filename, :content_type, :name)
88
+ def get_data
89
+ data = body
90
+ if filename == ""
91
+ # filename is blank which means no file has been selected
92
+ return
93
+ elsif filename
94
+ body.rewind if body.respond_to?(:rewind)
95
+
96
+ # Take the basename of the upload's original filename.
97
+ # This handles the full Windows paths given by Internet Explorer
98
+ # (and perhaps other broken user agents) without affecting
99
+ # those which give the lone filename.
100
+ fn = filename.split(/[\/\\]/).last
101
+
102
+ data = { filename: fn, type: content_type,
103
+ name: name, tempfile: body, head: head }
104
+ elsif !filename && content_type && body.is_a?(IO)
105
+ body.rewind
106
+
107
+ # Generic multipart cases, not coming from a form
108
+ data = { type: content_type,
109
+ name: name, tempfile: body, head: head }
110
+ end
59
111
 
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
112
+ yield data
63
113
  end
114
+ end
64
115
 
65
- # Save the rest.
66
- if i = @buf.index(rx)
67
- body << @buf.slice!(0, i)
68
- @buf.slice!(0, @boundary_size+2)
116
+ class BufferPart < MimePart
117
+ def file?; false; end
118
+ def close; end
119
+ end
69
120
 
70
- @content_length = -1 if $1 == "--"
71
- end
121
+ class TempfilePart < MimePart
122
+ def file?; true; end
123
+ def close; body.close; end
124
+ end
72
125
 
73
- get_data(filename, body, content_type, name, head) do |data|
74
- tag_multipart_encoding(filename, content_type, name, data)
126
+ include Enumerable
75
127
 
76
- Utils.normalize_params(@params, name, data)
77
- end
128
+ def initialize tempfile
129
+ @tempfile = tempfile
130
+ @mime_parts = []
131
+ @open_files = 0
132
+ end
78
133
 
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
134
+ def each
135
+ @mime_parts.each { |part| yield part }
81
136
  end
82
137
 
83
- @io.rewind
138
+ def on_mime_head mime_index, head, filename, content_type, name
139
+ if filename
140
+ body = @tempfile.call(filename, content_type)
141
+ body.binmode if body.respond_to?(:binmode)
142
+ klass = TempfilePart
143
+ @open_files += 1
144
+ else
145
+ body = String.new
146
+ klass = BufferPart
147
+ end
84
148
 
85
- @params.to_params_hash
86
- end
149
+ @mime_parts[mime_index] = klass.new(body, head, filename, content_type, name)
87
150
 
88
- private
89
- def full_boundary; @full_boundary; end
151
+ check_open_files
152
+ end
90
153
 
91
- def rx; @rx; end
154
+ def on_mime_body mime_index, content
155
+ @mime_parts[mime_index].body << content
156
+ end
92
157
 
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
158
+ def on_mime_finish mime_index
159
+ end
98
160
 
99
- while @buf.gsub!(/\A([^\n]*\n)/, '')
100
- read_buffer = $1
101
- return if read_buffer == full_boundary
102
- end
161
+ private
103
162
 
104
- raise EOFError, "bad content body" if Utils.bytesize(@buf) >= @bufsize
163
+ def check_open_files
164
+ if Utils.multipart_part_limit > 0
165
+ if @open_files >= Utils.multipart_part_limit
166
+ @mime_parts.each(&:close)
167
+ raise MultipartPartLimitError, 'Maximum file multiparts in content reached'
168
+ end
169
+ end
105
170
  end
106
171
  end
107
172
 
108
- def get_current_head_and_filename_and_content_type_and_name_and_body
109
- head = nil
110
- body = ''
173
+ attr_reader :state
111
174
 
112
- if body.respond_to? :force_encoding
113
- body.force_encoding Encoding::ASCII_8BIT
114
- end
175
+ def initialize(boundary, tempfile, bufsize, query_parser)
176
+ @query_parser = query_parser
177
+ @params = query_parser.make_params
178
+ @boundary = "--#{boundary}"
179
+ @bufsize = bufsize
115
180
 
116
- filename = content_type = name = nil
181
+ @full_boundary = @boundary
182
+ @end_boundary = @boundary + '--'
183
+ @state = :FAST_FORWARD
184
+ @mime_index = 0
185
+ @collector = Collector.new tempfile
117
186
 
118
- until head && @buf =~ rx
119
- if !head && i = @buf.index(EOL+EOL)
120
- head = @buf.slice!(0, i+2) # First \r\n
187
+ @sbuf = StringScanner.new("".dup)
188
+ @body_regex = /(?:#{EOL})?#{Regexp.quote(@boundary)}(?:#{EOL}|--)/m
189
+ @rx_max_size = EOL.size + @boundary.bytesize + [EOL.size, '--'.size].max
190
+ @head_regex = /(.*?#{EOL})#{EOL}/m
191
+ end
121
192
 
122
- @buf.slice!(0, 2) # Second \r\n
193
+ def on_read content
194
+ handle_empty_content!(content)
195
+ @sbuf.concat content
196
+ run_parser
197
+ end
123
198
 
124
- content_type = head[MULTIPART_CONTENT_TYPE, 1]
125
- name = head[MULTIPART_CONTENT_DISPOSITION, 1] || head[MULTIPART_CONTENT_ID, 1]
199
+ def result
200
+ @collector.each do |part|
201
+ part.get_data do |data|
202
+ tag_multipart_encoding(part.filename, part.content_type, part.name, data)
203
+ @query_parser.normalize_params(@params, part.name, data, @query_parser.param_depth_limit)
204
+ end
205
+ end
206
+ MultipartInfo.new @params.to_params_hash, @collector.find_all(&:file?).map(&:body)
207
+ end
126
208
 
127
- filename = get_filename(head)
209
+ private
128
210
 
129
- if name.nil? || name.empty? && filename
130
- name = filename
131
- end
211
+ def run_parser
212
+ loop do
213
+ case @state
214
+ when :FAST_FORWARD
215
+ break if handle_fast_forward == :want_read
216
+ when :CONSUME_TOKEN
217
+ break if handle_consume_token == :want_read
218
+ when :MIME_HEAD
219
+ break if handle_mime_head == :want_read
220
+ when :MIME_BODY
221
+ break if handle_mime_body == :want_read
222
+ when :DONE
223
+ break
224
+ end
225
+ end
226
+ end
132
227
 
133
- if filename
134
- (@env['rack.tempfiles'] ||= []) << body = @tempfile.call(filename, content_type)
135
- body.binmode if body.respond_to?(:binmode)
136
- end
228
+ def handle_fast_forward
229
+ if consume_boundary
230
+ @state = :MIME_HEAD
231
+ else
232
+ raise EOFError, "bad content body" if @sbuf.rest_size >= @bufsize
233
+ :want_read
234
+ end
235
+ end
236
+
237
+ def handle_consume_token
238
+ tok = consume_boundary
239
+ # break if we're at the end of a buffer, but not if it is the end of a field
240
+ @state = if tok == :END_BOUNDARY || (@sbuf.eos? && tok != :BOUNDARY)
241
+ :DONE
242
+ else
243
+ :MIME_HEAD
244
+ end
245
+ end
137
246
 
138
- next
247
+ def handle_mime_head
248
+ if @sbuf.scan_until(@head_regex)
249
+ head = @sbuf[1]
250
+ content_type = head[MULTIPART_CONTENT_TYPE, 1]
251
+ if name = head[MULTIPART_CONTENT_DISPOSITION, 1]
252
+ name = Rack::Auth::Digest::Params::dequote(name)
253
+ else
254
+ name = head[MULTIPART_CONTENT_ID, 1]
139
255
  end
140
256
 
141
- # Save the read body part.
142
- if head && (@boundary_size+4 < @buf.size)
143
- body << @buf.slice!(0, @buf.size - (@boundary_size+4))
257
+ filename = get_filename(head)
258
+
259
+ if name.nil? || name.empty?
260
+ name = filename || "#{content_type || TEXT_PLAIN}[]".dup
144
261
  end
145
262
 
146
- content = @io.read(@content_length && @bufsize >= @content_length ? @content_length : @bufsize)
147
- raise EOFError, "bad content body" if content.nil? || content.empty?
263
+ @collector.on_mime_head @mime_index, head, filename, content_type, name
264
+ @state = :MIME_BODY
265
+ else
266
+ :want_read
267
+ end
268
+ end
148
269
 
149
- @buf << content
150
- @content_length -= content.size if @content_length
270
+ def handle_mime_body
271
+ if (body_with_boundary = @sbuf.check_until(@body_regex)) # check but do not advance the pointer yet
272
+ body = body_with_boundary.sub(/#{@body_regex}\z/m, '') # remove the boundary from the string
273
+ @collector.on_mime_body @mime_index, body
274
+ @sbuf.pos += body.length + 2 # skip \r\n after the content
275
+ @state = :CONSUME_TOKEN
276
+ @mime_index += 1
277
+ else
278
+ # Save what we have so far
279
+ if @rx_max_size < @sbuf.rest_size
280
+ delta = @sbuf.rest_size - @rx_max_size
281
+ @collector.on_mime_body @mime_index, @sbuf.peek(delta)
282
+ @sbuf.pos += delta
283
+ @sbuf.string = @sbuf.rest
284
+ end
285
+ :want_read
151
286
  end
287
+ end
152
288
 
153
- [head, filename, content_type, name, body]
289
+ def full_boundary; @full_boundary; end
290
+
291
+ def consume_boundary
292
+ while read_buffer = @sbuf.scan_until(BOUNDARY_REGEX)
293
+ case read_buffer.strip
294
+ when full_boundary then return :BOUNDARY
295
+ when @end_boundary then return :END_BOUNDARY
296
+ end
297
+ return if @sbuf.eos?
298
+ end
154
299
  end
155
300
 
156
301
  def get_filename(head)
157
302
  filename = nil
158
303
  case head
159
304
  when RFC2183
160
- filename = Hash[head.scan(DISPPARM)]['filename']
161
- filename = $1 if filename and filename =~ /^"(.*)"$/
305
+ params = Hash[*head.scan(DISPPARM).flat_map(&:compact)]
306
+
307
+ if filename = params['filename']
308
+ filename = $1 if filename =~ /^"(.*)"$/
309
+ elsif filename = params['filename*']
310
+ encoding, _, filename = filename.split("'", 3)
311
+ end
162
312
  when BROKEN_QUOTED, BROKEN_UNQUOTED
163
313
  filename = $1
164
314
  end
165
315
 
166
316
  return unless filename
167
317
 
168
- if filename.scan(/%.?.?/).all? { |s| s =~ /%[0-9a-fA-F]{2}/ }
169
- filename = Utils.unescape(filename)
318
+ if filename.scan(/%.?.?/).all? { |s| /%[0-9a-fA-F]{2}/.match?(s) }
319
+ filename = Utils.unescape_path(filename)
170
320
  end
171
321
 
172
- scrub_filename filename
322
+ filename.scrub!
173
323
 
174
324
  if filename !~ /\\[^\\"]/
175
325
  filename = filename.gsub(/\\(.)/, '\1')
176
326
  end
177
- filename
178
- end
179
327
 
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
328
+ if encoding
329
+ filename.force_encoding ::Encoding.find(encoding)
188
330
  end
189
331
 
190
- CHARSET = "charset"
191
- TEXT_PLAIN = "text/plain"
332
+ filename
333
+ end
334
+
335
+ CHARSET = "charset"
192
336
 
193
- def tag_multipart_encoding(filename, content_type, name, body)
194
- name.force_encoding Encoding::UTF_8
337
+ def tag_multipart_encoding(filename, content_type, name, body)
338
+ name = name.to_s
339
+ encoding = Encoding::UTF_8
195
340
 
196
- return if filename
341
+ name.force_encoding(encoding)
197
342
 
198
- encoding = Encoding::UTF_8
343
+ return if filename
199
344
 
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
345
+ if content_type
346
+ list = content_type.split(';')
347
+ type_subtype = list.first
348
+ type_subtype.strip!
349
+ if TEXT_PLAIN == type_subtype
350
+ rest = list.drop 1
351
+ rest.each do |param|
352
+ k, v = param.split('=', 2)
353
+ k.strip!
354
+ v.strip!
355
+ v = v[1..-2] if v.start_with?('"') && v.end_with?('"')
356
+ encoding = Encoding.find v if k == CHARSET
212
357
  end
213
358
  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
359
  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
360
 
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
361
+ name.force_encoding(encoding)
362
+ body.force_encoding(encoding)
363
+ end
243
364
 
244
- # Generic multipart cases, not coming from a form
245
- data = {:type => content_type,
246
- :name => name, :tempfile => body, :head => head}
365
+ def handle_empty_content!(content)
366
+ if content.nil? || content.empty?
367
+ raise EOFError
247
368
  end
248
-
249
- yield data
250
369
  end
251
370
  end
252
371
  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,45 @@ 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_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="?([^\";]*)"?/ni
22
+ MULTIPART_CONTENT_DISPOSITION = /Content-Disposition:.*;\s*name=(#{VALUE})/ni
21
23
  MULTIPART_CONTENT_ID = /Content-ID:\s*([^#{EOL}]*)/ni
24
+ # Updated definitions from RFC 2231
25
+ ATTRIBUTE_CHAR = %r{[^ \t\v\n\r)(><@,;:\\"/\[\]?='*%]}
26
+ ATTRIBUTE = /#{ATTRIBUTE_CHAR}+/
27
+ SECTION = /\*[0-9]+/
28
+ REGULAR_PARAMETER_NAME = /#{ATTRIBUTE}#{SECTION}?/
29
+ REGULAR_PARAMETER = /(#{REGULAR_PARAMETER_NAME})=(#{VALUE})/
30
+ EXTENDED_OTHER_NAME = /#{ATTRIBUTE}\*[1-9][0-9]*\*/
31
+ EXTENDED_OTHER_VALUE = /%[0-9a-fA-F]{2}|#{ATTRIBUTE_CHAR}/
32
+ EXTENDED_OTHER_PARAMETER = /(#{EXTENDED_OTHER_NAME})=(#{EXTENDED_OTHER_VALUE}*)/
33
+ EXTENDED_INITIAL_NAME = /#{ATTRIBUTE}(?:\*0)?\*/
34
+ EXTENDED_INITIAL_VALUE = /[a-zA-Z0-9\-]*'[a-zA-Z0-9\-]*'#{EXTENDED_OTHER_VALUE}*/
35
+ EXTENDED_INITIAL_PARAMETER = /(#{EXTENDED_INITIAL_NAME})=(#{EXTENDED_INITIAL_VALUE})/
36
+ EXTENDED_PARAMETER = /#{EXTENDED_INITIAL_PARAMETER}|#{EXTENDED_OTHER_PARAMETER}/
37
+ DISPPARM = /;\s*(?:#{REGULAR_PARAMETER}|#{EXTENDED_PARAMETER})\s*/
38
+ RFC2183 = /^#{CONDISP}(#{DISPPARM})+$/i
22
39
 
23
40
  class << self
24
- def parse_multipart(env)
25
- Parser.create(env).parse
41
+ def parse_multipart(env, params = Rack::Utils.default_query_parser)
42
+ extract_multipart Rack::Request.new(env), params
43
+ end
44
+
45
+ def extract_multipart(req, params = Rack::Utils.default_query_parser)
46
+ io = req.get_header(RACK_INPUT)
47
+ io.rewind
48
+ content_length = req.content_length
49
+ content_length = content_length.to_i if content_length
50
+
51
+ tempfile = req.get_header(RACK_MULTIPART_TEMPFILE_FACTORY) || Parser::TEMPFILE_FACTORY
52
+ bufsize = req.get_header(RACK_MULTIPART_BUFFER_SIZE) || Parser::BUFSIZE
53
+
54
+ info = Parser.parse io, content_length, req.get_header('CONTENT_TYPE'), tempfile, bufsize, params
55
+ req.set_header(RACK_TEMPFILES, info.tmp_files)
56
+ info.params
26
57
  end
27
58
 
28
59
  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