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