rack 1.4.7 → 2.1.4

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 (183) hide show
  1. checksums.yaml +5 -5
  2. data/CHANGELOG.md +77 -0
  3. data/{COPYING → MIT-LICENSE} +4 -2
  4. data/README.rdoc +122 -456
  5. data/Rakefile +32 -31
  6. data/SPEC +119 -29
  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 +7 -5
  13. data/lib/rack/auth/abstract/request.rb +8 -6
  14. data/lib/rack/auth/basic.rb +5 -2
  15. data/lib/rack/auth/digest/md5.rb +10 -8
  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 +4 -2
  19. data/lib/rack/body_proxy.rb +11 -9
  20. data/lib/rack/builder.rb +63 -20
  21. data/lib/rack/cascade.rb +10 -9
  22. data/lib/rack/chunked.rb +45 -11
  23. data/lib/rack/{commonlogger.rb → common_logger.rb} +24 -15
  24. data/lib/rack/{conditionalget.rb → conditional_get.rb} +20 -6
  25. data/lib/rack/config.rb +7 -0
  26. data/lib/rack/content_length.rb +12 -6
  27. data/lib/rack/content_type.rb +4 -2
  28. data/lib/rack/core_ext/regexp.rb +14 -0
  29. data/lib/rack/deflater.rb +73 -42
  30. data/lib/rack/directory.rb +77 -56
  31. data/lib/rack/etag.rb +25 -13
  32. data/lib/rack/events.rb +156 -0
  33. data/lib/rack/file.rb +4 -143
  34. data/lib/rack/files.rb +178 -0
  35. data/lib/rack/handler/cgi.rb +18 -17
  36. data/lib/rack/handler/fastcgi.rb +21 -17
  37. data/lib/rack/handler/lsws.rb +14 -12
  38. data/lib/rack/handler/scgi.rb +27 -21
  39. data/lib/rack/handler/thin.rb +19 -5
  40. data/lib/rack/handler/webrick.rb +66 -24
  41. data/lib/rack/handler.rb +29 -19
  42. data/lib/rack/head.rb +21 -14
  43. data/lib/rack/lint.rb +259 -65
  44. data/lib/rack/lobster.rb +17 -10
  45. data/lib/rack/lock.rb +19 -10
  46. data/lib/rack/logger.rb +4 -2
  47. data/lib/rack/media_type.rb +43 -0
  48. data/lib/rack/method_override.rb +52 -0
  49. data/lib/rack/mime.rb +43 -6
  50. data/lib/rack/mock.rb +109 -44
  51. data/lib/rack/multipart/generator.rb +11 -12
  52. data/lib/rack/multipart/parser.rb +302 -115
  53. data/lib/rack/multipart/uploaded_file.rb +4 -3
  54. data/lib/rack/multipart.rb +40 -9
  55. data/lib/rack/null_logger.rb +39 -0
  56. data/lib/rack/query_parser.rb +218 -0
  57. data/lib/rack/recursive.rb +14 -11
  58. data/lib/rack/reloader.rb +12 -5
  59. data/lib/rack/request.rb +484 -270
  60. data/lib/rack/response.rb +196 -77
  61. data/lib/rack/rewindable_input.rb +5 -14
  62. data/lib/rack/runtime.rb +13 -6
  63. data/lib/rack/sendfile.rb +44 -20
  64. data/lib/rack/server.rb +175 -61
  65. data/lib/rack/session/abstract/id.rb +276 -133
  66. data/lib/rack/session/cookie.rb +75 -40
  67. data/lib/rack/session/memcache.rb +4 -87
  68. data/lib/rack/session/pool.rb +24 -18
  69. data/lib/rack/show_exceptions.rb +392 -0
  70. data/lib/rack/{showstatus.rb → show_status.rb} +11 -9
  71. data/lib/rack/static.rb +65 -38
  72. data/lib/rack/tempfile_reaper.rb +24 -0
  73. data/lib/rack/urlmap.rb +40 -15
  74. data/lib/rack/utils.rb +316 -285
  75. data/lib/rack.rb +78 -23
  76. data/rack.gemspec +26 -19
  77. metadata +44 -209
  78. data/KNOWN-ISSUES +0 -30
  79. data/lib/rack/backports/uri/common_18.rb +0 -56
  80. data/lib/rack/backports/uri/common_192.rb +0 -52
  81. data/lib/rack/backports/uri/common_193.rb +0 -29
  82. data/lib/rack/handler/evented_mongrel.rb +0 -8
  83. data/lib/rack/handler/mongrel.rb +0 -100
  84. data/lib/rack/handler/swiftiplied_mongrel.rb +0 -8
  85. data/lib/rack/methodoverride.rb +0 -33
  86. data/lib/rack/nulllogger.rb +0 -18
  87. data/lib/rack/showexceptions.rb +0 -378
  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/lighttpd.errors +0 -1
  101. data/test/cgi/rackup_stub.rb +0 -6
  102. data/test/cgi/sample_rackup.ru +0 -5
  103. data/test/cgi/test +0 -9
  104. data/test/cgi/test+directory/test+file +0 -1
  105. data/test/cgi/test.fcgi +0 -8
  106. data/test/cgi/test.ru +0 -5
  107. data/test/gemloader.rb +0 -10
  108. data/test/multipart/bad_robots +0 -259
  109. data/test/multipart/binary +0 -0
  110. data/test/multipart/content_type_and_no_filename +0 -6
  111. data/test/multipart/empty +0 -10
  112. data/test/multipart/fail_16384_nofile +0 -814
  113. data/test/multipart/file1.txt +0 -1
  114. data/test/multipart/filename_and_modification_param +0 -7
  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_percent_escaped_quotes +0 -6
  118. data/test/multipart/filename_with_unescaped_percentages +0 -6
  119. data/test/multipart/filename_with_unescaped_percentages2 +0 -6
  120. data/test/multipart/filename_with_unescaped_percentages3 +0 -6
  121. data/test/multipart/filename_with_unescaped_quotes +0 -6
  122. data/test/multipart/ie +0 -6
  123. data/test/multipart/mixed_files +0 -21
  124. data/test/multipart/nested +0 -10
  125. data/test/multipart/none +0 -9
  126. data/test/multipart/semicolon +0 -6
  127. data/test/multipart/text +0 -15
  128. data/test/multipart/three_files_three_fields +0 -31
  129. data/test/multipart/webkit +0 -32
  130. data/test/rackup/config.ru +0 -31
  131. data/test/registering_handler/rack/handler/registering_myself.rb +0 -8
  132. data/test/spec_auth.rb +0 -57
  133. data/test/spec_auth_basic.rb +0 -81
  134. data/test/spec_auth_digest.rb +0 -259
  135. data/test/spec_body_proxy.rb +0 -69
  136. data/test/spec_builder.rb +0 -207
  137. data/test/spec_cascade.rb +0 -61
  138. data/test/spec_cgi.rb +0 -102
  139. data/test/spec_chunked.rb +0 -87
  140. data/test/spec_commonlogger.rb +0 -57
  141. data/test/spec_conditionalget.rb +0 -102
  142. data/test/spec_config.rb +0 -22
  143. data/test/spec_content_length.rb +0 -86
  144. data/test/spec_content_type.rb +0 -45
  145. data/test/spec_deflater.rb +0 -187
  146. data/test/spec_directory.rb +0 -88
  147. data/test/spec_etag.rb +0 -98
  148. data/test/spec_fastcgi.rb +0 -107
  149. data/test/spec_file.rb +0 -200
  150. data/test/spec_handler.rb +0 -59
  151. data/test/spec_head.rb +0 -48
  152. data/test/spec_lint.rb +0 -515
  153. data/test/spec_lobster.rb +0 -58
  154. data/test/spec_lock.rb +0 -167
  155. data/test/spec_logger.rb +0 -23
  156. data/test/spec_methodoverride.rb +0 -72
  157. data/test/spec_mock.rb +0 -269
  158. data/test/spec_mongrel.rb +0 -182
  159. data/test/spec_multipart.rb +0 -479
  160. data/test/spec_nulllogger.rb +0 -23
  161. data/test/spec_recursive.rb +0 -72
  162. data/test/spec_request.rb +0 -955
  163. data/test/spec_response.rb +0 -313
  164. data/test/spec_rewindable_input.rb +0 -118
  165. data/test/spec_runtime.rb +0 -49
  166. data/test/spec_sendfile.rb +0 -90
  167. data/test/spec_server.rb +0 -121
  168. data/test/spec_session_abstract_id.rb +0 -43
  169. data/test/spec_session_cookie.rb +0 -361
  170. data/test/spec_session_memcache.rb +0 -321
  171. data/test/spec_session_pool.rb +0 -209
  172. data/test/spec_showexceptions.rb +0 -92
  173. data/test/spec_showstatus.rb +0 -84
  174. data/test/spec_static.rb +0 -145
  175. data/test/spec_thin.rb +0 -86
  176. data/test/spec_urlmap.rb +0 -213
  177. data/test/spec_utils.rb +0 -554
  178. data/test/spec_webrick.rb +0 -143
  179. data/test/static/another/index.html +0 -1
  180. data/test/static/index.html +0 -1
  181. data/test/testrequest.rb +0 -78
  182. data/test/unregistered_handler/rack/handler/unregistered.rb +0 -7
  183. data/test/unregistered_handler/rack/handler/unregistered_long_one.rb +0 -7
@@ -1,184 +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
- class MultipartLimitError < Errno::EMFILE; end
9
+ class MultipartPartLimitError < Errno::EMFILE; end
6
10
 
7
11
  class Parser
8
- BUFSIZE = 16384
12
+ using ::Rack::RegexpExtensions
9
13
 
10
- def initialize(env)
11
- @env = env
12
- end
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
+ }
19
+
20
+ BOUNDARY_REGEX = /\A([^\n]*(?:\n|\Z))/
13
21
 
14
- def parse
15
- return nil unless setup_parse
22
+ class BoundedIO # :nodoc:
23
+ def initialize(io, content_length)
24
+ @io = io
25
+ @content_length = content_length
26
+ @cursor = 0
27
+ end
16
28
 
17
- fast_forward_to_first_boundary
29
+ def read(size, outbuf = nil)
30
+ return if @cursor >= @content_length
18
31
 
19
- opened_files = 0
20
- loop do
32
+ left = @content_length - @cursor
21
33
 
22
- head, filename, content_type, name, body =
23
- get_current_head_and_filename_and_content_type_and_name_and_body
34
+ str = if left < size
35
+ @io.read left, outbuf
36
+ else
37
+ @io.read size, outbuf
38
+ end
24
39
 
25
- if Utils.multipart_part_limit > 0
26
- opened_files += 1 if filename
27
- raise MultipartLimitError, 'Maximum file multiparts in content reached' if opened_files >= Utils.multipart_part_limit
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"
28
45
  end
29
46
 
30
- # Save the rest.
31
- if i = @buf.index(rx)
32
- body << @buf.slice!(0, i)
33
- @buf.slice!(0, @boundary_size+2)
47
+ str
48
+ end
49
+
50
+ def rewind
51
+ @io.rewind
52
+ end
53
+ end
54
+
55
+ MultipartInfo = Struct.new :params, :tmp_files
56
+ EMPTY = MultipartInfo.new(nil, [])
57
+
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]
63
+ end
64
+
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)
76
+
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
85
+
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
34
111
 
35
- @content_length = -1 if $1 == "--"
112
+ yield data
36
113
  end
114
+ end
37
115
 
38
- filename, data = get_data(filename, body, content_type, name, head)
116
+ class BufferPart < MimePart
117
+ def file?; false; end
118
+ def close; end
119
+ end
39
120
 
40
- Utils.normalize_params(@params, name, data) unless data.nil?
121
+ class TempfilePart < MimePart
122
+ def file?; true; end
123
+ def close; body.close; end
124
+ end
41
125
 
42
- # break if we're at the end of a buffer, but not if it is the end of a field
43
- break if (@buf.empty? && $1 != EOL) || @content_length == -1
126
+ include Enumerable
127
+
128
+ def initialize tempfile
129
+ @tempfile = tempfile
130
+ @mime_parts = []
131
+ @open_files = 0
44
132
  end
45
133
 
46
- @io.rewind
134
+ def each
135
+ @mime_parts.each { |part| yield part }
136
+ end
47
137
 
48
- @params.to_params_hash
49
- end
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
50
148
 
51
- private
52
- def setup_parse
53
- return false unless @env['CONTENT_TYPE'] =~ MULTIPART
149
+ @mime_parts[mime_index] = klass.new(body, head, filename, content_type, name)
54
150
 
55
- @boundary = "--#{$1}"
151
+ check_open_files
152
+ end
56
153
 
57
- @buf = ""
58
- @params = Utils::KeySpaceConstrainedParams.new
154
+ def on_mime_body mime_index, content
155
+ @mime_parts[mime_index].body << content
156
+ end
59
157
 
60
- @io = @env['rack.input']
61
- @io.rewind
158
+ def on_mime_finish mime_index
159
+ end
62
160
 
63
- @boundary_size = Utils.bytesize(@boundary) + EOL.size
161
+ private
64
162
 
65
- if @content_length = @env['CONTENT_LENGTH']
66
- @content_length = @content_length.to_i
67
- @content_length -= @boundary_size
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
68
170
  end
69
- true
70
171
  end
71
172
 
72
- def full_boundary
73
- @boundary + EOL
74
- end
173
+ attr_reader :state
174
+
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
75
180
 
76
- def rx
77
- @rx ||= /(?:#{EOL})?#{Regexp.quote(@boundary)}(#{EOL}|--)/n
181
+ @full_boundary = @boundary
182
+ @end_boundary = @boundary + '--'
183
+ @state = :FAST_FORWARD
184
+ @mime_index = 0
185
+ @collector = Collector.new tempfile
186
+
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
78
191
  end
79
192
 
80
- def fast_forward_to_first_boundary
81
- loop do
82
- content = @io.read(BUFSIZE)
83
- raise EOFError, "bad content body" unless content
84
- @buf << content
193
+ def on_read content
194
+ handle_empty_content!(content)
195
+ @sbuf.concat content
196
+ run_parser
197
+ end
85
198
 
86
- while @buf.gsub!(/\A([^\n]*\n)/, '')
87
- read_buffer = $1
88
- return if read_buffer == full_boundary
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)
89
204
  end
90
-
91
- raise EOFError, "bad content body" if Utils.bytesize(@buf) >= BUFSIZE
92
205
  end
206
+ MultipartInfo.new @params.to_params_hash, @collector.find_all(&:file?).map(&:body)
93
207
  end
94
208
 
95
- def get_current_head_and_filename_and_content_type_and_name_and_body
96
- head = nil
97
- body = ''
98
- filename = content_type = name = nil
99
- content = nil
209
+ private
100
210
 
101
- until head && @buf =~ rx
102
- if !head && i = @buf.index(EOL+EOL)
103
- head = @buf.slice!(0, i+2) # First \r\n
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
104
227
 
105
- @buf.slice!(0, 2) # Second \r\n
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
106
236
 
107
- content_type = head[MULTIPART_CONTENT_TYPE, 1]
108
- name = head[MULTIPART_CONTENT_DISPOSITION, 1] || head[MULTIPART_CONTENT_ID, 1]
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
109
246
 
110
- filename = get_filename(head)
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]
255
+ end
111
256
 
112
- if filename
113
- body = Tempfile.new("RackMultipart")
114
- body.binmode if body.respond_to?(:binmode)
115
- end
257
+ filename = get_filename(head)
116
258
 
117
- next
259
+ if name.nil? || name.empty?
260
+ name = filename || "#{content_type || TEXT_PLAIN}[]".dup
118
261
  end
119
262
 
120
- # Save the read body part.
121
- if head && (@boundary_size+4 < @buf.size)
122
- body << @buf.slice!(0, @buf.size - (@boundary_size+4))
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
269
+
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
123
284
  end
285
+ :want_read
286
+ end
287
+ end
124
288
 
125
- content = @io.read(@content_length && BUFSIZE >= @content_length ? @content_length : BUFSIZE)
126
- raise EOFError, "bad content body" if content.nil? || content.empty?
289
+ def full_boundary; @full_boundary; end
127
290
 
128
- @buf << content
129
- @content_length -= content.size if @content_length
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?
130
298
  end
131
-
132
- [head, filename, content_type, name, body]
133
299
  end
134
300
 
135
301
  def get_filename(head)
136
302
  filename = nil
137
- if head =~ RFC2183
138
- filename = Hash[head.scan(DISPPARM)]['filename']
139
- filename = $1 if filename and filename =~ /^"(.*)"$/
140
- elsif head =~ BROKEN_QUOTED
141
- filename = $1
142
- elsif head =~ BROKEN_UNQUOTED
303
+ case head
304
+ when RFC2183
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
312
+ when BROKEN_QUOTED, BROKEN_UNQUOTED
143
313
  filename = $1
144
314
  end
145
315
 
146
- if filename && filename.scan(/%.?.?/).all? { |s| s =~ /%[0-9a-fA-F]{2}/ }
147
- filename = Utils.unescape(filename)
316
+ return unless filename
317
+
318
+ if filename.scan(/%.?.?/).all? { |s| /%[0-9a-fA-F]{2}/.match?(s) }
319
+ filename = Utils.unescape_path(filename)
148
320
  end
149
- if filename && filename !~ /\\[^\\"]/
321
+
322
+ filename.scrub!
323
+
324
+ if filename !~ /\\[^\\"]/
150
325
  filename = filename.gsub(/\\(.)/, '\1')
151
326
  end
327
+
328
+ if encoding
329
+ filename.force_encoding ::Encoding.find(encoding)
330
+ end
331
+
152
332
  filename
153
333
  end
154
334
 
155
- def get_data(filename, body, content_type, name, head)
156
- data = nil
157
- if filename == ""
158
- # filename is blank which means no file has been selected
159
- return data
160
- elsif filename
161
- body.rewind
162
-
163
- # Take the basename of the upload's original filename.
164
- # This handles the full Windows paths given by Internet Explorer
165
- # (and perhaps other broken user agents) without affecting
166
- # those which give the lone filename.
167
- filename = filename.split(/[\/\\]/).last
168
-
169
- data = {:filename => filename, :type => content_type,
170
- :name => name, :tempfile => body, :head => head}
171
- elsif !filename && content_type && body.is_a?(IO)
172
- body.rewind
173
-
174
- # Generic multipart cases, not coming from a form
175
- data = {:type => content_type,
176
- :name => name, :tempfile => body, :head => head}
177
- else
178
- data = body
335
+ CHARSET = "charset"
336
+
337
+ def tag_multipart_encoding(filename, content_type, name, body)
338
+ name = name.to_s
339
+ encoding = Encoding::UTF_8
340
+
341
+ name.force_encoding(encoding)
342
+
343
+ return if filename
344
+
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
357
+ end
358
+ end
179
359
  end
180
360
 
181
- [filename, data]
361
+ name.force_encoding(encoding)
362
+ body.force_encoding(encoding)
363
+ end
364
+
365
+ def handle_empty_content!(content)
366
+ if content.nil? || content.empty?
367
+ raise EOFError
368
+ end
182
369
  end
183
370
  end
184
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)
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
@@ -31,4 +32,4 @@ module Rack
31
32
  end
32
33
  end
33
34
  end
34
- end
35
+ end
@@ -1,28 +1,59 @@
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"
11
14
  MULTIPART_BOUNDARY = "AaB03x"
12
- MULTIPART = %r|\Amultipart/.*boundary=\"?([^\";,]+)\"?|n
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.new(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)
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rack
4
+ class NullLogger
5
+ def initialize(app)
6
+ @app = app
7
+ end
8
+
9
+ def call(env)
10
+ env[RACK_LOGGER] = self
11
+ @app.call(env)
12
+ end
13
+
14
+ def info(progname = nil, &block); end
15
+ def debug(progname = nil, &block); end
16
+ def warn(progname = nil, &block); end
17
+ def error(progname = nil, &block); end
18
+ def fatal(progname = nil, &block); end
19
+ def unknown(progname = nil, &block); end
20
+ def info? ; end
21
+ def debug? ; end
22
+ def warn? ; end
23
+ def error? ; end
24
+ def fatal? ; end
25
+ def level ; end
26
+ def progname ; end
27
+ def datetime_format ; end
28
+ def formatter ; end
29
+ def sev_threshold ; end
30
+ def level=(level); end
31
+ def progname=(progname); end
32
+ def datetime_format=(datetime_format); end
33
+ def formatter=(formatter); end
34
+ def sev_threshold=(sev_threshold); end
35
+ def close ; end
36
+ def add(severity, message = nil, progname = nil, &block); end
37
+ def <<(msg); end
38
+ end
39
+ end