rack 2.2.23 → 3.2.6
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +574 -71
- data/CONTRIBUTING.md +63 -55
- data/MIT-LICENSE +1 -1
- data/README.md +384 -0
- data/SPEC.rdoc +243 -277
- data/lib/rack/auth/abstract/handler.rb +3 -1
- data/lib/rack/auth/abstract/request.rb +5 -1
- data/lib/rack/auth/basic.rb +1 -3
- data/lib/rack/bad_request.rb +8 -0
- data/lib/rack/body_proxy.rb +21 -3
- data/lib/rack/builder.rb +108 -69
- data/lib/rack/cascade.rb +2 -3
- data/lib/rack/common_logger.rb +22 -17
- data/lib/rack/conditional_get.rb +20 -16
- data/lib/rack/constants.rb +68 -0
- data/lib/rack/content_length.rb +12 -16
- data/lib/rack/content_type.rb +8 -5
- data/lib/rack/deflater.rb +40 -26
- data/lib/rack/directory.rb +9 -3
- data/lib/rack/etag.rb +17 -23
- data/lib/rack/events.rb +25 -6
- data/lib/rack/files.rb +15 -17
- data/lib/rack/head.rb +8 -8
- data/lib/rack/headers.rb +238 -0
- data/lib/rack/lint.rb +817 -648
- data/lib/rack/lock.rb +2 -5
- data/lib/rack/media_type.rb +6 -7
- data/lib/rack/method_override.rb +5 -1
- data/lib/rack/mime.rb +14 -5
- data/lib/rack/mock.rb +1 -300
- data/lib/rack/mock_request.rb +161 -0
- data/lib/rack/mock_response.rb +156 -0
- data/lib/rack/multipart/generator.rb +7 -5
- data/lib/rack/multipart/parser.rb +267 -109
- data/lib/rack/multipart/uploaded_file.rb +45 -4
- data/lib/rack/multipart.rb +53 -40
- data/lib/rack/null_logger.rb +9 -0
- data/lib/rack/query_parser.rb +116 -121
- data/lib/rack/recursive.rb +2 -0
- data/lib/rack/reloader.rb +0 -2
- data/lib/rack/request.rb +272 -144
- data/lib/rack/response.rb +151 -66
- data/lib/rack/rewindable_input.rb +27 -5
- data/lib/rack/runtime.rb +7 -6
- data/lib/rack/sendfile.rb +35 -30
- data/lib/rack/show_exceptions.rb +25 -6
- data/lib/rack/show_status.rb +17 -9
- data/lib/rack/static.rb +8 -8
- data/lib/rack/tempfile_reaper.rb +15 -4
- data/lib/rack/urlmap.rb +3 -1
- data/lib/rack/utils.rb +287 -236
- data/lib/rack/version.rb +3 -15
- data/lib/rack.rb +13 -90
- metadata +14 -40
- data/README.rdoc +0 -355
- data/Rakefile +0 -130
- data/bin/rackup +0 -5
- data/contrib/rack.png +0 -0
- data/contrib/rack.svg +0 -150
- data/contrib/rack_logo.svg +0 -164
- data/contrib/rdoc.css +0 -412
- data/example/lobster.ru +0 -6
- data/example/protectedlobster.rb +0 -16
- data/example/protectedlobster.ru +0 -10
- data/lib/rack/auth/digest/md5.rb +0 -131
- data/lib/rack/auth/digest/nonce.rb +0 -53
- data/lib/rack/auth/digest/params.rb +0 -54
- data/lib/rack/auth/digest/request.rb +0 -43
- data/lib/rack/chunked.rb +0 -117
- data/lib/rack/core_ext/regexp.rb +0 -14
- data/lib/rack/file.rb +0 -7
- data/lib/rack/handler/cgi.rb +0 -59
- data/lib/rack/handler/fastcgi.rb +0 -100
- data/lib/rack/handler/lsws.rb +0 -61
- data/lib/rack/handler/scgi.rb +0 -71
- data/lib/rack/handler/thin.rb +0 -34
- data/lib/rack/handler/webrick.rb +0 -129
- data/lib/rack/handler.rb +0 -104
- data/lib/rack/lobster.rb +0 -70
- data/lib/rack/logger.rb +0 -20
- data/lib/rack/server.rb +0 -466
- data/lib/rack/session/abstract/id.rb +0 -523
- data/lib/rack/session/cookie.rb +0 -203
- data/lib/rack/session/memcache.rb +0 -10
- data/lib/rack/session/pool.rb +0 -90
- data/rack.gemspec +0 -46
|
@@ -2,14 +2,55 @@
|
|
|
2
2
|
|
|
3
3
|
require 'strscan'
|
|
4
4
|
|
|
5
|
+
require_relative '../utils'
|
|
6
|
+
require_relative '../bad_request'
|
|
7
|
+
|
|
5
8
|
module Rack
|
|
6
9
|
module Multipart
|
|
7
|
-
class MultipartPartLimitError < Errno::EMFILE
|
|
8
|
-
|
|
10
|
+
class MultipartPartLimitError < Errno::EMFILE
|
|
11
|
+
include BadRequest
|
|
12
|
+
end
|
|
9
13
|
|
|
10
|
-
class
|
|
11
|
-
|
|
14
|
+
class MultipartTotalPartLimitError < StandardError
|
|
15
|
+
include BadRequest
|
|
16
|
+
end
|
|
12
17
|
|
|
18
|
+
# Use specific error class when parsing multipart request
|
|
19
|
+
# that ends early.
|
|
20
|
+
class EmptyContentError < ::EOFError
|
|
21
|
+
include BadRequest
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Base class for multipart exceptions that do not subclass from
|
|
25
|
+
# other exception classes for backwards compatibility.
|
|
26
|
+
class BoundaryTooLongError < StandardError
|
|
27
|
+
include BadRequest
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Prefer to use the BoundaryTooLongError class or Rack::BadRequest.
|
|
31
|
+
Error = BoundaryTooLongError
|
|
32
|
+
|
|
33
|
+
EOL = "\r\n"
|
|
34
|
+
FWS = /[ \t]+(?:\r\n[ \t]+)?/ # whitespace with optional folding
|
|
35
|
+
HEADER_VALUE = "(?:[^\r\n]|\r\n[ \t])*" # anything but a non-folding CRLF
|
|
36
|
+
MULTIPART = %r|\Amultipart/.*?boundary(\s*)=\"?([^\";,]+)\"?|ni
|
|
37
|
+
MULTIPART_CONTENT_TYPE = /^Content-Type:#{FWS}?(#{HEADER_VALUE})/ni
|
|
38
|
+
MULTIPART_CONTENT_DISPOSITION = /^Content-Disposition:#{FWS}?(#{HEADER_VALUE})/ni
|
|
39
|
+
MULTIPART_CONTENT_ID = /^Content-ID:#{FWS}?(#{HEADER_VALUE})/ni
|
|
40
|
+
|
|
41
|
+
# Rack::Multipart::Parser handles parsing of multipart/form-data requests.
|
|
42
|
+
#
|
|
43
|
+
# File Parameter Contents
|
|
44
|
+
#
|
|
45
|
+
# When processing file uploads, the parser returns a hash containing
|
|
46
|
+
# information about uploaded files. For +file+ parameters, the hash includes:
|
|
47
|
+
#
|
|
48
|
+
# * +:filename+ - The original filename, already URL decoded by the parser
|
|
49
|
+
# * +:type+ - The content type of the uploaded file
|
|
50
|
+
# * +:name+ - The parameter name from the form
|
|
51
|
+
# * +:tempfile+ - A Tempfile object containing the uploaded data
|
|
52
|
+
# * +:head+ - The raw header content for this part
|
|
53
|
+
class Parser
|
|
13
54
|
BUFSIZE = 1_048_576
|
|
14
55
|
TEXT_PLAIN = "text/plain"
|
|
15
56
|
TEMPFILE_FACTORY = lambda { |filename, content_type|
|
|
@@ -18,8 +59,6 @@ module Rack
|
|
|
18
59
|
Tempfile.new(["RackMultipart", extension])
|
|
19
60
|
}
|
|
20
61
|
|
|
21
|
-
BOUNDARY_REGEX = /\A([^\n]*(?:\n|\Z))/
|
|
22
|
-
|
|
23
62
|
BOUNDARY_START_LIMIT = 16 * 1024
|
|
24
63
|
private_constant :BOUNDARY_START_LIMIT
|
|
25
64
|
|
|
@@ -45,6 +84,9 @@ module Rack
|
|
|
45
84
|
PARSER_BYTESIZE_LIMIT = bytesize_limit > 0 ? bytesize_limit : nil
|
|
46
85
|
private_constant :PARSER_BYTESIZE_LIMIT
|
|
47
86
|
|
|
87
|
+
CONTENT_DISPOSITION_QUOTED_ESCAPES_LIMIT = env_int.call("RACK_MULTIPART_CONTENT_DISPOSITION_QUOTED_ESCAPES_LIMIT", 8 * 1024)
|
|
88
|
+
private_constant :CONTENT_DISPOSITION_QUOTED_ESCAPES_LIMIT
|
|
89
|
+
|
|
48
90
|
class BoundedIO # :nodoc:
|
|
49
91
|
def initialize(io, content_length)
|
|
50
92
|
@io = io
|
|
@@ -66,16 +108,12 @@ module Rack
|
|
|
66
108
|
if str
|
|
67
109
|
@cursor += str.bytesize
|
|
68
110
|
else
|
|
69
|
-
# Raise an error for mismatching
|
|
111
|
+
# Raise an error for mismatching content-length and actual contents
|
|
70
112
|
raise EOFError, "bad content body"
|
|
71
113
|
end
|
|
72
114
|
|
|
73
115
|
str
|
|
74
116
|
end
|
|
75
|
-
|
|
76
|
-
def rewind
|
|
77
|
-
@io.rewind
|
|
78
|
-
end
|
|
79
117
|
end
|
|
80
118
|
|
|
81
119
|
MultipartInfo = Struct.new :params, :tmp_files
|
|
@@ -87,10 +125,10 @@ module Rack
|
|
|
87
125
|
return unless data
|
|
88
126
|
|
|
89
127
|
unless data[1].empty?
|
|
90
|
-
raise
|
|
128
|
+
raise Error, "whitespace between boundary parameter name and equal sign"
|
|
91
129
|
end
|
|
92
|
-
if data.post_match
|
|
93
|
-
raise
|
|
130
|
+
if data.post_match.match?(/boundary\s*=/i)
|
|
131
|
+
raise BoundaryTooLongError, "multiple boundary parameters found in multipart content type"
|
|
94
132
|
end
|
|
95
133
|
|
|
96
134
|
data[2]
|
|
@@ -103,21 +141,20 @@ module Rack
|
|
|
103
141
|
return EMPTY unless boundary
|
|
104
142
|
|
|
105
143
|
if PARSER_BYTESIZE_LIMIT && content_length && content_length > PARSER_BYTESIZE_LIMIT
|
|
106
|
-
raise
|
|
144
|
+
raise Error, "multipart Content-Length #{content_length} exceeds limit of #{PARSER_BYTESIZE_LIMIT} bytes"
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
if boundary.length > 70
|
|
148
|
+
# RFC 1521 Section 7.2.1 imposes a 70 character maximum for the boundary.
|
|
149
|
+
# Most clients use no more than 55 characters.
|
|
150
|
+
raise BoundaryTooLongError, "multipart boundary size too large (#{boundary.length} characters)"
|
|
107
151
|
end
|
|
108
152
|
|
|
109
153
|
io = BoundedIO.new(io, content_length) if content_length
|
|
110
|
-
outbuf = String.new
|
|
111
154
|
|
|
112
155
|
parser = new(boundary, tmpfile, bufsize, qp)
|
|
113
|
-
parser.
|
|
156
|
+
parser.parse(io)
|
|
114
157
|
|
|
115
|
-
loop do
|
|
116
|
-
break if parser.state == :DONE
|
|
117
|
-
parser.on_read io.read(bufsize, outbuf)
|
|
118
|
-
end
|
|
119
|
-
|
|
120
|
-
io.rewind
|
|
121
158
|
parser.result
|
|
122
159
|
end
|
|
123
160
|
|
|
@@ -217,42 +254,54 @@ module Rack
|
|
|
217
254
|
def initialize(boundary, tempfile, bufsize, query_parser)
|
|
218
255
|
@query_parser = query_parser
|
|
219
256
|
@params = query_parser.make_params
|
|
220
|
-
@boundary = "--#{boundary}"
|
|
221
257
|
@bufsize = bufsize
|
|
222
258
|
|
|
223
|
-
@full_boundary = @boundary
|
|
224
|
-
@end_boundary = @boundary + '--'
|
|
225
259
|
@state = :FAST_FORWARD
|
|
226
260
|
@mime_index = 0
|
|
227
261
|
@body_retained = nil
|
|
228
262
|
@retained_size = 0
|
|
229
263
|
@total_bytes_read = (0 if PARSER_BYTESIZE_LIMIT)
|
|
264
|
+
@content_disposition_quoted_escapes = 0
|
|
230
265
|
@collector = Collector.new tempfile
|
|
231
266
|
|
|
232
267
|
@sbuf = StringScanner.new("".dup)
|
|
233
|
-
@body_regex = /(?:#{EOL})
|
|
234
|
-
@
|
|
235
|
-
@
|
|
268
|
+
@body_regex = /(?:#{EOL}|\A)--#{Regexp.quote(boundary)}(?:#{EOL}|--)/m
|
|
269
|
+
@body_regex_at_end = /#{@body_regex}\z/m
|
|
270
|
+
@end_boundary_size = boundary.bytesize + 4 # (-- at start, -- at finish)
|
|
271
|
+
@rx_max_size = boundary.bytesize + 6 # (\r\n-- at start, either \r\n or -- at finish)
|
|
236
272
|
@head_regex = /(.*?#{EOL})#{EOL}/m
|
|
237
273
|
end
|
|
238
274
|
|
|
239
|
-
def
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
275
|
+
def parse(io)
|
|
276
|
+
@total_bytes_read &&= nil if io.is_a?(BoundedIO)
|
|
277
|
+
outbuf = String.new
|
|
278
|
+
read_data(io, outbuf)
|
|
279
|
+
|
|
280
|
+
loop do
|
|
281
|
+
status =
|
|
282
|
+
case @state
|
|
283
|
+
when :FAST_FORWARD
|
|
284
|
+
handle_fast_forward
|
|
285
|
+
when :CONSUME_TOKEN
|
|
286
|
+
handle_consume_token
|
|
287
|
+
when :MIME_HEAD
|
|
288
|
+
handle_mime_head
|
|
289
|
+
when :MIME_BODY
|
|
290
|
+
handle_mime_body
|
|
291
|
+
else # when :DONE
|
|
292
|
+
return
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
read_data(io, outbuf) if status == :want_read
|
|
246
296
|
end
|
|
247
|
-
@sbuf.concat content
|
|
248
|
-
run_parser
|
|
249
297
|
end
|
|
250
298
|
|
|
251
299
|
def result
|
|
252
300
|
@collector.each do |part|
|
|
253
301
|
part.get_data do |data|
|
|
254
302
|
tag_multipart_encoding(part.filename, part.content_type, part.name, data)
|
|
255
|
-
|
|
303
|
+
name, data = handle_dummy_encoding(part.name, data)
|
|
304
|
+
@query_parser.normalize_params(@params, name, data)
|
|
256
305
|
end
|
|
257
306
|
end
|
|
258
307
|
MultipartInfo.new @params.to_params_hash, @collector.find_all(&:file?).map(&:body)
|
|
@@ -260,40 +309,49 @@ module Rack
|
|
|
260
309
|
|
|
261
310
|
private
|
|
262
311
|
|
|
263
|
-
def
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
when :MIME_HEAD
|
|
271
|
-
break if handle_mime_head == :want_read
|
|
272
|
-
when :MIME_BODY
|
|
273
|
-
break if handle_mime_body == :want_read
|
|
274
|
-
when :DONE
|
|
275
|
-
break
|
|
312
|
+
def read_data(io, outbuf)
|
|
313
|
+
content = io.read(@bufsize, outbuf)
|
|
314
|
+
handle_empty_content!(content)
|
|
315
|
+
if @total_bytes_read
|
|
316
|
+
@total_bytes_read += content.bytesize
|
|
317
|
+
if @total_bytes_read > PARSER_BYTESIZE_LIMIT
|
|
318
|
+
raise Error, "multipart upload exceeds limit of #{PARSER_BYTESIZE_LIMIT} bytes"
|
|
276
319
|
end
|
|
277
320
|
end
|
|
321
|
+
@sbuf.concat(content)
|
|
278
322
|
end
|
|
279
323
|
|
|
324
|
+
# This handles the initial parser state. We read until we find the starting
|
|
325
|
+
# boundary, then we can transition to the next state. If we find the ending
|
|
326
|
+
# boundary, this is an invalid multipart upload, but keep scanning for opening
|
|
327
|
+
# boundary in that case. If no boundary found, we need to keep reading data
|
|
328
|
+
# and retry. It's highly unlikely the initial read will not consume the
|
|
329
|
+
# boundary. The client would have to deliberately craft a response
|
|
330
|
+
# with the opening boundary beyond the buffer size for that to happen.
|
|
280
331
|
def handle_fast_forward
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
332
|
+
while true
|
|
333
|
+
case consume_boundary
|
|
334
|
+
when :BOUNDARY
|
|
335
|
+
# found opening boundary, transition to next state
|
|
336
|
+
@state = :MIME_HEAD
|
|
337
|
+
return
|
|
338
|
+
when :END_BOUNDARY
|
|
339
|
+
# invalid multipart upload
|
|
340
|
+
if @sbuf.pos == @end_boundary_size && @sbuf.rest == EOL
|
|
341
|
+
# stop parsing a buffer if a buffer is only an end boundary.
|
|
342
|
+
@state = :DONE
|
|
343
|
+
return
|
|
344
|
+
end
|
|
290
345
|
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
346
|
+
# retry for opening boundary
|
|
347
|
+
else
|
|
348
|
+
# We raise if we don't find the multipart boundary, to avoid unbounded memory
|
|
349
|
+
# buffering. Note that the actual limit is the higher of 16KB and the buffer size (1MB by default)
|
|
350
|
+
raise Error, "multipart boundary not found within limit" if @sbuf.string.bytesize > BOUNDARY_START_LIMIT
|
|
294
351
|
|
|
295
|
-
|
|
296
|
-
|
|
352
|
+
# no boundary found, keep reading data
|
|
353
|
+
return :want_read
|
|
354
|
+
end
|
|
297
355
|
end
|
|
298
356
|
end
|
|
299
357
|
|
|
@@ -307,17 +365,114 @@ module Rack
|
|
|
307
365
|
end
|
|
308
366
|
end
|
|
309
367
|
|
|
368
|
+
CONTENT_DISPOSITION_MAX_PARAMS = 16
|
|
369
|
+
CONTENT_DISPOSITION_MAX_BYTES = 1536
|
|
370
|
+
OBS_UNFOLD = /\r\n([ \t])/
|
|
371
|
+
private_constant :OBS_UNFOLD
|
|
372
|
+
|
|
310
373
|
def handle_mime_head
|
|
311
374
|
if @sbuf.scan_until(@head_regex)
|
|
312
375
|
head = @sbuf[1]
|
|
313
376
|
content_type = head[MULTIPART_CONTENT_TYPE, 1]
|
|
314
|
-
|
|
315
|
-
|
|
377
|
+
content_type.gsub!(OBS_UNFOLD, '\1') if content_type
|
|
378
|
+
|
|
379
|
+
if (disposition = head[MULTIPART_CONTENT_DISPOSITION, 1]) &&
|
|
380
|
+
disposition.bytesize <= CONTENT_DISPOSITION_MAX_BYTES
|
|
381
|
+
|
|
382
|
+
# Implement OBS unfolding (RFC 5322 Section 2.2.3)
|
|
383
|
+
disposition.gsub!(OBS_UNFOLD, '\1')
|
|
384
|
+
|
|
385
|
+
# ignore actual content-disposition value (should always be form-data)
|
|
386
|
+
i = disposition.index(';')
|
|
387
|
+
disposition.slice!(0, i+1)
|
|
388
|
+
param = nil
|
|
389
|
+
num_params = 0
|
|
390
|
+
|
|
391
|
+
# Parse parameter list
|
|
392
|
+
while i = disposition.index('=')
|
|
393
|
+
# Only parse up to max parameters, to avoid potential denial of service
|
|
394
|
+
num_params += 1
|
|
395
|
+
break if num_params > CONTENT_DISPOSITION_MAX_PARAMS
|
|
396
|
+
|
|
397
|
+
# Found end of parameter name, ensure forward progress in loop
|
|
398
|
+
param = disposition.slice!(0, i+1)
|
|
399
|
+
|
|
400
|
+
# Remove ending equals and preceding whitespace from parameter name
|
|
401
|
+
param.chomp!('=')
|
|
402
|
+
param.lstrip!
|
|
403
|
+
|
|
404
|
+
if disposition[0] == '"'
|
|
405
|
+
# Parameter value is quoted, parse it, handling backslash escapes
|
|
406
|
+
disposition.slice!(0, 1)
|
|
407
|
+
value = String.new
|
|
408
|
+
|
|
409
|
+
while i = disposition.index(/(["\\])/)
|
|
410
|
+
c = $1
|
|
411
|
+
|
|
412
|
+
# Append all content until ending quote or escape
|
|
413
|
+
value << disposition.slice!(0, i)
|
|
414
|
+
|
|
415
|
+
# Remove either backslash or ending quote,
|
|
416
|
+
# ensures forward progress in loop
|
|
417
|
+
disposition.slice!(0, 1)
|
|
418
|
+
|
|
419
|
+
# stop parsing parameter value if found ending quote
|
|
420
|
+
break if c == '"'
|
|
421
|
+
|
|
422
|
+
@content_disposition_quoted_escapes += 1
|
|
423
|
+
if @content_disposition_quoted_escapes > CONTENT_DISPOSITION_QUOTED_ESCAPES_LIMIT
|
|
424
|
+
raise Error, "number of quoted escapes during content disposition parsing exceeds limit"
|
|
425
|
+
end
|
|
426
|
+
|
|
427
|
+
escaped_char = disposition.slice!(0, 1)
|
|
428
|
+
if param == 'filename' && escaped_char != '"'
|
|
429
|
+
# Possible IE uploaded filename, append both escape backslash and value
|
|
430
|
+
value << c << escaped_char
|
|
431
|
+
else
|
|
432
|
+
# Other only append escaped value
|
|
433
|
+
value << escaped_char
|
|
434
|
+
end
|
|
435
|
+
end
|
|
436
|
+
else
|
|
437
|
+
if i = disposition.index(';')
|
|
438
|
+
# Parameter value unquoted (which may be invalid), value ends at semicolon
|
|
439
|
+
value = disposition.slice!(0, i)
|
|
440
|
+
else
|
|
441
|
+
# If no ending semicolon, assume remainder of line is value and stop
|
|
442
|
+
# parsing
|
|
443
|
+
disposition.strip!
|
|
444
|
+
value = disposition
|
|
445
|
+
disposition = ''
|
|
446
|
+
end
|
|
447
|
+
end
|
|
448
|
+
|
|
449
|
+
case param
|
|
450
|
+
when 'name'
|
|
451
|
+
name = value
|
|
452
|
+
when 'filename'
|
|
453
|
+
filename = value
|
|
454
|
+
when 'filename*'
|
|
455
|
+
filename_star = value
|
|
456
|
+
# else
|
|
457
|
+
# ignore other parameters
|
|
458
|
+
end
|
|
459
|
+
|
|
460
|
+
# skip trailing semicolon, to proceed to next parameter
|
|
461
|
+
if i = disposition.index(';')
|
|
462
|
+
disposition.slice!(0, i+1)
|
|
463
|
+
end
|
|
464
|
+
end
|
|
316
465
|
else
|
|
317
466
|
name = head[MULTIPART_CONTENT_ID, 1]
|
|
318
467
|
end
|
|
319
468
|
|
|
320
|
-
|
|
469
|
+
if filename_star
|
|
470
|
+
encoding, _, filename = filename_star.split("'", 3)
|
|
471
|
+
filename = normalize_filename(filename || '')
|
|
472
|
+
filename.force_encoding(find_encoding(encoding))
|
|
473
|
+
elsif filename
|
|
474
|
+
filename = normalize_filename(filename)
|
|
475
|
+
end
|
|
321
476
|
|
|
322
477
|
if name.nil? || name.empty?
|
|
323
478
|
name = filename || "#{content_type || TEXT_PLAIN}[]".dup
|
|
@@ -337,7 +492,7 @@ module Rack
|
|
|
337
492
|
else
|
|
338
493
|
# We raise if the mime part header is too large, to avoid unbounded memory
|
|
339
494
|
# buffering. Note that the actual limit is the higher of 64KB and the buffer size (1MB by default)
|
|
340
|
-
raise
|
|
495
|
+
raise Error, "multipart mime part header too large" if @sbuf.rest.bytesize > MIME_HEADER_BYTESIZE_LIMIT
|
|
341
496
|
|
|
342
497
|
return :want_read
|
|
343
498
|
end
|
|
@@ -345,7 +500,7 @@ module Rack
|
|
|
345
500
|
|
|
346
501
|
def handle_mime_body
|
|
347
502
|
if (body_with_boundary = @sbuf.check_until(@body_regex)) # check but do not advance the pointer yet
|
|
348
|
-
body = body_with_boundary.sub(
|
|
503
|
+
body = body_with_boundary.sub(@body_regex_at_end, '') # remove the boundary from the string
|
|
349
504
|
update_retained_size(body.bytesize) if @body_retained
|
|
350
505
|
@collector.on_mime_body @mime_index, body
|
|
351
506
|
@sbuf.pos += body.length + 2 # skip \r\n after the content
|
|
@@ -365,12 +520,10 @@ module Rack
|
|
|
365
520
|
end
|
|
366
521
|
end
|
|
367
522
|
|
|
368
|
-
def full_boundary; @full_boundary; end
|
|
369
|
-
|
|
370
523
|
def update_retained_size(size)
|
|
371
524
|
@retained_size += size
|
|
372
525
|
if @retained_size > BUFFERED_UPLOAD_BYTESIZE_LIMIT
|
|
373
|
-
raise
|
|
526
|
+
raise Error, "multipart data over retained size limit"
|
|
374
527
|
end
|
|
375
528
|
end
|
|
376
529
|
|
|
@@ -379,51 +532,26 @@ module Rack
|
|
|
379
532
|
# end of the boundary. If we don't find the start or end of the
|
|
380
533
|
# boundary, clear the buffer and return nil.
|
|
381
534
|
def consume_boundary
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
return if @sbuf.eos?
|
|
535
|
+
if read_buffer = @sbuf.scan_until(@body_regex)
|
|
536
|
+
read_buffer.end_with?(EOL) ? :BOUNDARY : :END_BOUNDARY
|
|
537
|
+
else
|
|
538
|
+
@sbuf.terminate
|
|
539
|
+
nil
|
|
388
540
|
end
|
|
389
541
|
end
|
|
390
542
|
|
|
391
|
-
def
|
|
392
|
-
filename = nil
|
|
393
|
-
case head
|
|
394
|
-
when RFC2183
|
|
395
|
-
params = Hash[*head.scan(DISPPARM).flat_map(&:compact)]
|
|
396
|
-
|
|
397
|
-
if filename = params['filename']
|
|
398
|
-
filename = $1 if filename =~ /^"(.*)"$/
|
|
399
|
-
elsif filename = params['filename*']
|
|
400
|
-
encoding, _, filename = filename.split("'", 3)
|
|
401
|
-
end
|
|
402
|
-
when BROKEN
|
|
403
|
-
filename = $1
|
|
404
|
-
filename = $1 if filename =~ /^"(.*)"$/
|
|
405
|
-
end
|
|
406
|
-
|
|
407
|
-
return unless filename
|
|
408
|
-
|
|
543
|
+
def normalize_filename(filename)
|
|
409
544
|
if filename.scan(/%.?.?/).all? { |s| /%[0-9a-fA-F]{2}/.match?(s) }
|
|
410
545
|
filename = Utils.unescape_path(filename)
|
|
411
546
|
end
|
|
412
547
|
|
|
413
548
|
filename.scrub!
|
|
414
549
|
|
|
415
|
-
|
|
416
|
-
filename = filename.gsub(/\\(.)/, '\1')
|
|
417
|
-
end
|
|
418
|
-
|
|
419
|
-
if encoding
|
|
420
|
-
filename.force_encoding ::Encoding.find(encoding)
|
|
421
|
-
end
|
|
422
|
-
|
|
423
|
-
filename
|
|
550
|
+
filename.split(/[\/\\]/).last || String.new
|
|
424
551
|
end
|
|
425
552
|
|
|
426
553
|
CHARSET = "charset"
|
|
554
|
+
deprecate_constant :CHARSET
|
|
427
555
|
|
|
428
556
|
def tag_multipart_encoding(filename, content_type, name, body)
|
|
429
557
|
name = name.to_s
|
|
@@ -444,7 +572,9 @@ module Rack
|
|
|
444
572
|
k.strip!
|
|
445
573
|
v.strip!
|
|
446
574
|
v = v[1..-2] if v.start_with?('"') && v.end_with?('"')
|
|
447
|
-
|
|
575
|
+
if k == "charset"
|
|
576
|
+
encoding = find_encoding(v)
|
|
577
|
+
end
|
|
448
578
|
end
|
|
449
579
|
end
|
|
450
580
|
end
|
|
@@ -453,9 +583,37 @@ module Rack
|
|
|
453
583
|
body.force_encoding(encoding)
|
|
454
584
|
end
|
|
455
585
|
|
|
586
|
+
# Return the related Encoding object. However, because
|
|
587
|
+
# enc is submitted by the user, it may be invalid, so
|
|
588
|
+
# use a binary encoding in that case.
|
|
589
|
+
def find_encoding(enc)
|
|
590
|
+
Encoding.find enc
|
|
591
|
+
rescue ArgumentError
|
|
592
|
+
Encoding::BINARY
|
|
593
|
+
end
|
|
594
|
+
|
|
595
|
+
REENCODE_DUMMY_ENCODINGS = {
|
|
596
|
+
# ISO-2022-JP is a legacy but still widely used encoding in Japan
|
|
597
|
+
# Here we convert ISO-2022-JP to UTF-8 so that it can be handled.
|
|
598
|
+
Encoding::ISO_2022_JP => true
|
|
599
|
+
|
|
600
|
+
# Other dummy encodings are rarely used and have not been supported yet.
|
|
601
|
+
# Adding support for them will require careful considerations.
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
def handle_dummy_encoding(name, body)
|
|
605
|
+
# A string object with a 'dummy' encoding does not have full functionality and can cause errors.
|
|
606
|
+
# So here we covert it to UTF-8 so that it can be handled properly.
|
|
607
|
+
if name.encoding.dummy? && REENCODE_DUMMY_ENCODINGS[name.encoding]
|
|
608
|
+
name = name.encode(Encoding::UTF_8)
|
|
609
|
+
body = body.encode(Encoding::UTF_8)
|
|
610
|
+
end
|
|
611
|
+
return name, body
|
|
612
|
+
end
|
|
613
|
+
|
|
456
614
|
def handle_empty_content!(content)
|
|
457
615
|
if content.nil? || content.empty?
|
|
458
|
-
raise
|
|
616
|
+
raise EmptyContentError
|
|
459
617
|
end
|
|
460
618
|
end
|
|
461
619
|
end
|
|
@@ -1,14 +1,51 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require 'tempfile'
|
|
4
|
+
require 'fileutils'
|
|
5
|
+
|
|
3
6
|
module Rack
|
|
4
7
|
module Multipart
|
|
8
|
+
# Despite the misleading name, UploadedFile is designed for use for
|
|
9
|
+
# preparing multipart file upload bodies, generally for use in tests.
|
|
10
|
+
# It is not designed for and should not be used for handling uploaded
|
|
11
|
+
# files (there is no need for that, since Rack's multipart parser
|
|
12
|
+
# already creates Tempfiles for that). Using this with non-trusted
|
|
13
|
+
# filenames can create a security vulnerability.
|
|
14
|
+
#
|
|
15
|
+
# You should only use this class if you plan on passing the instances
|
|
16
|
+
# to Rack::MockRequest for use in creating multipart request bodies.
|
|
17
|
+
#
|
|
18
|
+
# UploadedFile delegates most methods to the tempfile it contains.
|
|
5
19
|
class UploadedFile
|
|
6
|
-
# The
|
|
20
|
+
# The provided name of the file. This generally is the basename of
|
|
21
|
+
# path provided during initialization, but it can contain slashes if they
|
|
22
|
+
# were present in the filename argument when the instance was created.
|
|
7
23
|
attr_reader :original_filename
|
|
8
24
|
|
|
9
|
-
# The content type of the
|
|
25
|
+
# The content type of the instance.
|
|
10
26
|
attr_accessor :content_type
|
|
11
27
|
|
|
28
|
+
# Create a new UploadedFile. For backwards compatibility, this accepts
|
|
29
|
+
# both positional and keyword versions of the same arguments:
|
|
30
|
+
#
|
|
31
|
+
# filepath/path :: The path to the file
|
|
32
|
+
# ct/content_type :: The content_type of the file
|
|
33
|
+
# bin/binary :: Whether to set binmode on the file before copying data into it.
|
|
34
|
+
#
|
|
35
|
+
# If both positional and keyword arguments are present, the keyword arguments
|
|
36
|
+
# take precedence.
|
|
37
|
+
#
|
|
38
|
+
# The following keyword-only arguments are also accepted:
|
|
39
|
+
#
|
|
40
|
+
# filename :: Override the filename to use for the file. This is so the
|
|
41
|
+
# filename for the upload does not need to match the basename of
|
|
42
|
+
# the file path. This should not contain slashes, unless you are
|
|
43
|
+
# trying to test how an application handles invalid filenames in
|
|
44
|
+
# multipart upload bodies.
|
|
45
|
+
# io :: Use the given IO-like instance as the tempfile, instead of creating
|
|
46
|
+
# a Tempfile instance. This is useful for building multipart file
|
|
47
|
+
# upload bodies without a file being present on the filesystem. If you are
|
|
48
|
+
# providing this, you should also provide the filename argument.
|
|
12
49
|
def initialize(filepath = nil, ct = "text/plain", bin = false,
|
|
13
50
|
path: filepath, content_type: ct, binary: bin, filename: nil, io: nil)
|
|
14
51
|
if io
|
|
@@ -24,15 +61,19 @@ module Rack
|
|
|
24
61
|
@content_type = content_type
|
|
25
62
|
end
|
|
26
63
|
|
|
64
|
+
# The path of the tempfile for the instance, if the tempfile has a path.
|
|
65
|
+
# nil if the tempfile does not have a path.
|
|
27
66
|
def path
|
|
28
67
|
@tempfile.path if @tempfile.respond_to?(:path)
|
|
29
68
|
end
|
|
30
69
|
alias_method :local_path, :path
|
|
31
70
|
|
|
32
|
-
|
|
33
|
-
|
|
71
|
+
# Return true if the tempfile responds to the method.
|
|
72
|
+
def respond_to_missing?(*args)
|
|
73
|
+
@tempfile.respond_to?(*args)
|
|
34
74
|
end
|
|
35
75
|
|
|
76
|
+
# Delegate method missing calls to the tempfile.
|
|
36
77
|
def method_missing(method_name, *args, &block) #:nodoc:
|
|
37
78
|
@tempfile.__send__(method_name, *args, &block)
|
|
38
79
|
end
|