rack 3.0.5 → 3.2.0
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.
Potentially problematic release.
This version of rack might be problematic. Click here for more details.
- checksums.yaml +4 -4
- data/CHANGELOG.md +383 -1
- data/CONTRIBUTING.md +11 -9
- data/README.md +95 -28
- data/SPEC.rdoc +206 -288
- data/lib/rack/auth/abstract/request.rb +2 -0
- data/lib/rack/auth/basic.rb +1 -2
- data/lib/rack/bad_request.rb +8 -0
- data/lib/rack/body_proxy.rb +18 -2
- data/lib/rack/builder.rb +29 -10
- data/lib/rack/cascade.rb +0 -3
- data/lib/rack/common_logger.rb +3 -2
- data/lib/rack/conditional_get.rb +4 -3
- data/lib/rack/constants.rb +3 -1
- data/lib/rack/etag.rb +3 -0
- data/lib/rack/head.rb +2 -3
- data/lib/rack/headers.rb +86 -2
- data/lib/rack/lint.rb +482 -425
- data/lib/rack/media_type.rb +18 -9
- data/lib/rack/mime.rb +6 -5
- data/lib/rack/mock_request.rb +10 -15
- data/lib/rack/mock_response.rb +39 -18
- data/lib/rack/multipart/parser.rb +169 -73
- data/lib/rack/multipart/uploaded_file.rb +42 -5
- data/lib/rack/multipart.rb +9 -1
- data/lib/rack/query_parser.rb +86 -97
- data/lib/rack/request.rb +67 -100
- data/lib/rack/response.rb +29 -19
- data/lib/rack/rewindable_input.rb +4 -1
- data/lib/rack/sendfile.rb +2 -2
- data/lib/rack/show_exceptions.rb +10 -4
- data/lib/rack/show_status.rb +0 -2
- data/lib/rack/static.rb +2 -1
- data/lib/rack/utils.rb +78 -110
- data/lib/rack/version.rb +3 -20
- data/lib/rack.rb +11 -17
- metadata +6 -15
- data/lib/rack/auth/digest/md5.rb +0 -1
- data/lib/rack/auth/digest/nonce.rb +0 -1
- data/lib/rack/auth/digest/params.rb +0 -1
- data/lib/rack/auth/digest/request.rb +0 -1
- data/lib/rack/auth/digest.rb +0 -256
- data/lib/rack/chunked.rb +0 -120
- data/lib/rack/file.rb +0 -9
- data/lib/rack/logger.rb +0 -22
data/lib/rack/media_type.rb
CHANGED
|
@@ -4,7 +4,7 @@ module Rack
|
|
|
4
4
|
# Rack::MediaType parse media type and parameters out of content_type string
|
|
5
5
|
|
|
6
6
|
class MediaType
|
|
7
|
-
SPLIT_PATTERN =
|
|
7
|
+
SPLIT_PATTERN = /[;,]/
|
|
8
8
|
|
|
9
9
|
class << self
|
|
10
10
|
# The media type (type/subtype) portion of the CONTENT_TYPE header
|
|
@@ -14,8 +14,11 @@ module Rack
|
|
|
14
14
|
# For more information on the use of media types in HTTP, see:
|
|
15
15
|
# http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.7
|
|
16
16
|
def type(content_type)
|
|
17
|
-
return nil unless content_type
|
|
18
|
-
content_type.split(SPLIT_PATTERN, 2).first
|
|
17
|
+
return nil unless content_type && !content_type.empty?
|
|
18
|
+
type = content_type.split(SPLIT_PATTERN, 2).first
|
|
19
|
+
type.rstrip!
|
|
20
|
+
type.downcase!
|
|
21
|
+
type
|
|
19
22
|
end
|
|
20
23
|
|
|
21
24
|
# The media type parameters provided in CONTENT_TYPE as a Hash, or
|
|
@@ -23,21 +26,27 @@ module Rack
|
|
|
23
26
|
# provided. e.g., when the CONTENT_TYPE is "text/plain;charset=utf-8",
|
|
24
27
|
# this method responds with the following Hash:
|
|
25
28
|
# { 'charset' => 'utf-8' }
|
|
29
|
+
#
|
|
30
|
+
# This will pass back parameters with empty strings in the hash if they
|
|
31
|
+
# lack a value (e.g., "text/plain;charset=" will return { 'charset' => '' },
|
|
32
|
+
# and "text/plain;charset" will return { 'charset' => '' }, similarly to
|
|
33
|
+
# the query params parser (barring the latter case, which returns nil instead)).
|
|
26
34
|
def params(content_type)
|
|
27
|
-
return {} if content_type.nil?
|
|
35
|
+
return {} if content_type.nil? || content_type.empty?
|
|
28
36
|
|
|
29
37
|
content_type.split(SPLIT_PATTERN)[1..-1].each_with_object({}) do |s, hsh|
|
|
38
|
+
s.strip!
|
|
30
39
|
k, v = s.split('=', 2)
|
|
31
|
-
|
|
32
|
-
hsh[k
|
|
40
|
+
k.downcase!
|
|
41
|
+
hsh[k] = strip_doublequotes(v)
|
|
33
42
|
end
|
|
34
43
|
end
|
|
35
44
|
|
|
36
45
|
private
|
|
37
46
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
47
|
+
def strip_doublequotes(str)
|
|
48
|
+
(str && str.start_with?('"') && str.end_with?('"')) ? str[1..-2] : str || ''
|
|
49
|
+
end
|
|
41
50
|
end
|
|
42
51
|
end
|
|
43
52
|
end
|
data/lib/rack/mime.rb
CHANGED
|
@@ -290,7 +290,7 @@ module Rack
|
|
|
290
290
|
".jpg" => "image/jpeg",
|
|
291
291
|
".jpgv" => "video/jpeg",
|
|
292
292
|
".jpm" => "video/jpm",
|
|
293
|
-
".js" => "
|
|
293
|
+
".js" => "text/javascript",
|
|
294
294
|
".json" => "application/json",
|
|
295
295
|
".karbon" => "application/vnd.kde.karbon",
|
|
296
296
|
".kfo" => "application/vnd.kde.kformula",
|
|
@@ -338,6 +338,7 @@ module Rack
|
|
|
338
338
|
".mif" => "application/vnd.mif",
|
|
339
339
|
".mime" => "message/rfc822",
|
|
340
340
|
".mj2" => "video/mj2",
|
|
341
|
+
".mjs" => "text/javascript",
|
|
341
342
|
".mlp" => "application/vnd.dolby.mlp",
|
|
342
343
|
".mmd" => "application/vnd.chipnuts.karaoke-mmd",
|
|
343
344
|
".mmf" => "application/vnd.smaf",
|
|
@@ -409,7 +410,7 @@ module Rack
|
|
|
409
410
|
".ogx" => "application/ogg",
|
|
410
411
|
".org" => "application/vnd.lotus-organizer",
|
|
411
412
|
".otc" => "application/vnd.oasis.opendocument.chart-template",
|
|
412
|
-
".otf" => "
|
|
413
|
+
".otf" => "font/otf",
|
|
413
414
|
".otg" => "application/vnd.oasis.opendocument.graphics-template",
|
|
414
415
|
".oth" => "application/vnd.oasis.opendocument.text-web",
|
|
415
416
|
".oti" => "application/vnd.oasis.opendocument.image-template",
|
|
@@ -590,7 +591,7 @@ module Rack
|
|
|
590
591
|
".trm" => "application/x-msterminal",
|
|
591
592
|
".ts" => "video/mp2t",
|
|
592
593
|
".tsv" => "text/tab-separated-values",
|
|
593
|
-
".ttf" => "
|
|
594
|
+
".ttf" => "font/ttf",
|
|
594
595
|
".twd" => "application/vnd.simtech-mindmapper",
|
|
595
596
|
".txd" => "application/vnd.genomatix.tuxedo",
|
|
596
597
|
".txf" => "application/vnd.mobius.txf",
|
|
@@ -636,8 +637,8 @@ module Rack
|
|
|
636
637
|
".wmv" => "video/x-ms-wmv",
|
|
637
638
|
".wmx" => "video/x-ms-wmx",
|
|
638
639
|
".wmz" => "application/x-ms-wmz",
|
|
639
|
-
".woff" => "
|
|
640
|
-
".woff2" => "
|
|
640
|
+
".woff" => "font/woff",
|
|
641
|
+
".woff2" => "font/woff2",
|
|
641
642
|
".wpd" => "application/vnd.wordperfect",
|
|
642
643
|
".wpl" => "application/vnd.ms-wpl",
|
|
643
644
|
".wps" => "application/vnd.ms-works",
|
data/lib/rack/mock_request.rb
CHANGED
|
@@ -41,11 +41,6 @@ module Rack
|
|
|
41
41
|
end
|
|
42
42
|
end
|
|
43
43
|
|
|
44
|
-
DEFAULT_ENV = {
|
|
45
|
-
RACK_INPUT => StringIO.new,
|
|
46
|
-
RACK_ERRORS => StringIO.new,
|
|
47
|
-
}.freeze
|
|
48
|
-
|
|
49
44
|
def initialize(app)
|
|
50
45
|
@app = app
|
|
51
46
|
end
|
|
@@ -104,7 +99,7 @@ module Rack
|
|
|
104
99
|
uri = parse_uri_rfc2396(uri)
|
|
105
100
|
uri.path = "/#{uri.path}" unless uri.path[0] == ?/
|
|
106
101
|
|
|
107
|
-
env =
|
|
102
|
+
env = {}
|
|
108
103
|
|
|
109
104
|
env[REQUEST_METHOD] = (opts[:method] ? opts[:method].to_s.upcase : GET).b
|
|
110
105
|
env[SERVER_NAME] = (uri.host || "example.org").b
|
|
@@ -144,20 +139,20 @@ module Rack
|
|
|
144
139
|
end
|
|
145
140
|
end
|
|
146
141
|
|
|
147
|
-
opts[:input]
|
|
148
|
-
if String ===
|
|
149
|
-
rack_input = StringIO.new(
|
|
150
|
-
else
|
|
151
|
-
rack_input = opts[:input]
|
|
142
|
+
rack_input = opts[:input]
|
|
143
|
+
if String === rack_input
|
|
144
|
+
rack_input = StringIO.new(rack_input)
|
|
152
145
|
end
|
|
153
146
|
|
|
154
|
-
rack_input
|
|
155
|
-
|
|
147
|
+
if rack_input
|
|
148
|
+
rack_input.set_encoding(Encoding::BINARY) if rack_input.respond_to?(:set_encoding)
|
|
149
|
+
env[RACK_INPUT] = rack_input
|
|
156
150
|
|
|
157
|
-
|
|
151
|
+
env["CONTENT_LENGTH"] ||= env[RACK_INPUT].size.to_s if env[RACK_INPUT].respond_to?(:size)
|
|
152
|
+
end
|
|
158
153
|
|
|
159
154
|
opts.each { |field, value|
|
|
160
|
-
env[field] = value
|
|
155
|
+
env[field] = value if String === field
|
|
161
156
|
}
|
|
162
157
|
|
|
163
158
|
env
|
data/lib/rack/mock_response.rb
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require 'cgi/cookie'
|
|
4
3
|
require 'time'
|
|
5
4
|
|
|
6
5
|
require_relative 'response'
|
|
@@ -11,6 +10,30 @@ module Rack
|
|
|
11
10
|
# MockRequest.
|
|
12
11
|
|
|
13
12
|
class MockResponse < Rack::Response
|
|
13
|
+
class Cookie
|
|
14
|
+
attr_reader :name, :value, :path, :domain, :expires, :secure
|
|
15
|
+
|
|
16
|
+
def initialize(args)
|
|
17
|
+
@name = args["name"]
|
|
18
|
+
@value = args["value"]
|
|
19
|
+
@path = args["path"]
|
|
20
|
+
@domain = args["domain"]
|
|
21
|
+
@expires = args["expires"]
|
|
22
|
+
@secure = args["secure"]
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def method_missing(method_name, *args, &block)
|
|
26
|
+
@value.send(method_name, *args, &block)
|
|
27
|
+
end
|
|
28
|
+
# :nocov:
|
|
29
|
+
ruby2_keywords(:method_missing) if respond_to?(:ruby2_keywords, true)
|
|
30
|
+
# :nocov:
|
|
31
|
+
|
|
32
|
+
def respond_to_missing?(method_name, include_all = false)
|
|
33
|
+
@value.respond_to?(method_name, include_all) || super
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
14
37
|
class << self
|
|
15
38
|
alias [] new
|
|
16
39
|
end
|
|
@@ -78,22 +101,20 @@ module Rack
|
|
|
78
101
|
|
|
79
102
|
def parse_cookies_from_header
|
|
80
103
|
cookies = Hash.new
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
Array(set_cookie_header).each do |
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
cookies.store(cookie_name, parsed_cookie)
|
|
96
|
-
end
|
|
104
|
+
set_cookie_header = headers['set-cookie']
|
|
105
|
+
if set_cookie_header && !set_cookie_header.empty?
|
|
106
|
+
Array(set_cookie_header).each do |cookie|
|
|
107
|
+
cookie_name, cookie_filling = cookie.split('=', 2)
|
|
108
|
+
cookie_attributes = identify_cookie_attributes cookie_filling
|
|
109
|
+
parsed_cookie = Cookie.new(
|
|
110
|
+
'name' => cookie_name.strip,
|
|
111
|
+
'value' => cookie_attributes.fetch('value'),
|
|
112
|
+
'path' => cookie_attributes.fetch('path', nil),
|
|
113
|
+
'domain' => cookie_attributes.fetch('domain', nil),
|
|
114
|
+
'expires' => cookie_attributes.fetch('expires', nil),
|
|
115
|
+
'secure' => cookie_attributes.fetch('secure', false)
|
|
116
|
+
)
|
|
117
|
+
cookies.store(cookie_name, parsed_cookie)
|
|
97
118
|
end
|
|
98
119
|
end
|
|
99
120
|
cookies
|
|
@@ -102,7 +123,7 @@ module Rack
|
|
|
102
123
|
def identify_cookie_attributes(cookie_filling)
|
|
103
124
|
cookie_bits = cookie_filling.split(';')
|
|
104
125
|
cookie_attributes = Hash.new
|
|
105
|
-
cookie_attributes.store('value', cookie_bits[0].strip)
|
|
126
|
+
cookie_attributes.store('value', Array(cookie_bits[0].strip))
|
|
106
127
|
cookie_bits.drop(1).each do |bit|
|
|
107
128
|
if bit.include? '='
|
|
108
129
|
cookie_attribute, attribute_value = bit.split('=', 2)
|
|
@@ -3,51 +3,60 @@
|
|
|
3
3
|
require 'strscan'
|
|
4
4
|
|
|
5
5
|
require_relative '../utils'
|
|
6
|
+
require_relative '../bad_request'
|
|
6
7
|
|
|
7
8
|
module Rack
|
|
8
9
|
module Multipart
|
|
9
|
-
class MultipartPartLimitError < Errno::EMFILE
|
|
10
|
+
class MultipartPartLimitError < Errno::EMFILE
|
|
11
|
+
include BadRequest
|
|
12
|
+
end
|
|
10
13
|
|
|
11
|
-
class MultipartTotalPartLimitError < StandardError
|
|
14
|
+
class MultipartTotalPartLimitError < StandardError
|
|
15
|
+
include BadRequest
|
|
16
|
+
end
|
|
12
17
|
|
|
13
18
|
# Use specific error class when parsing multipart request
|
|
14
19
|
# that ends early.
|
|
15
|
-
class EmptyContentError < ::EOFError
|
|
20
|
+
class EmptyContentError < ::EOFError
|
|
21
|
+
include BadRequest
|
|
22
|
+
end
|
|
16
23
|
|
|
17
24
|
# Base class for multipart exceptions that do not subclass from
|
|
18
25
|
# other exception classes for backwards compatibility.
|
|
19
|
-
class
|
|
26
|
+
class BoundaryTooLongError < StandardError
|
|
27
|
+
include BadRequest
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Prefer to use the BoundaryTooLongError class or Rack::BadRequest.
|
|
31
|
+
Error = BoundaryTooLongError
|
|
20
32
|
|
|
21
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
|
|
22
36
|
MULTIPART = %r|\Amultipart/.*boundary=\"?([^\";,]+)\"?|ni
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
#
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
EXTENDED_INITIAL_NAME = /#{ATTRIBUTE}(?:\*0)?\*/
|
|
40
|
-
EXTENDED_INITIAL_VALUE = /[a-zA-Z0-9\-]*'[a-zA-Z0-9\-]*'#{EXTENDED_OTHER_VALUE}*/
|
|
41
|
-
EXTENDED_INITIAL_PARAMETER = /(#{EXTENDED_INITIAL_NAME})=(#{EXTENDED_INITIAL_VALUE})/
|
|
42
|
-
EXTENDED_PARAMETER = /#{EXTENDED_INITIAL_PARAMETER}|#{EXTENDED_OTHER_PARAMETER}/
|
|
43
|
-
DISPPARM = /;\s*(?:#{REGULAR_PARAMETER}|#{EXTENDED_PARAMETER})\s*/
|
|
44
|
-
RFC2183 = /^#{CONDISP}(#{DISPPARM})+$/i
|
|
45
|
-
|
|
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
|
|
46
53
|
class Parser
|
|
47
54
|
BUFSIZE = 1_048_576
|
|
48
55
|
TEXT_PLAIN = "text/plain"
|
|
49
56
|
TEMPFILE_FACTORY = lambda { |filename, content_type|
|
|
50
|
-
|
|
57
|
+
extension = ::File.extname(filename.gsub("\0", '%00'))[0, 129]
|
|
58
|
+
|
|
59
|
+
Tempfile.new(["RackMultipart", extension])
|
|
51
60
|
}
|
|
52
61
|
|
|
53
62
|
class BoundedIO # :nodoc:
|
|
@@ -98,7 +107,7 @@ module Rack
|
|
|
98
107
|
if boundary.length > 70
|
|
99
108
|
# RFC 1521 Section 7.2.1 imposes a 70 character maximum for the boundary.
|
|
100
109
|
# Most clients use no more than 55 characters.
|
|
101
|
-
raise
|
|
110
|
+
raise BoundaryTooLongError, "multipart boundary size too large (#{boundary.length} characters)"
|
|
102
111
|
end
|
|
103
112
|
|
|
104
113
|
io = BoundedIO.new(io, content_length) if content_length
|
|
@@ -213,6 +222,8 @@ module Rack
|
|
|
213
222
|
|
|
214
223
|
@sbuf = StringScanner.new("".dup)
|
|
215
224
|
@body_regex = /(?:#{EOL}|\A)--#{Regexp.quote(boundary)}(?:#{EOL}|--)/m
|
|
225
|
+
@body_regex_at_end = /#{@body_regex}\z/m
|
|
226
|
+
@end_boundary_size = boundary.bytesize + 4 # (-- at start, -- at finish)
|
|
216
227
|
@rx_max_size = boundary.bytesize + 6 # (\r\n-- at start, either \r\n or -- at finish)
|
|
217
228
|
@head_regex = /(.*?#{EOL})#{EOL}/m
|
|
218
229
|
end
|
|
@@ -244,7 +255,8 @@ module Rack
|
|
|
244
255
|
@collector.each do |part|
|
|
245
256
|
part.get_data do |data|
|
|
246
257
|
tag_multipart_encoding(part.filename, part.content_type, part.name, data)
|
|
247
|
-
|
|
258
|
+
name, data = handle_dummy_encoding(part.name, data)
|
|
259
|
+
@query_parser.normalize_params(@params, name, data)
|
|
248
260
|
end
|
|
249
261
|
end
|
|
250
262
|
MultipartInfo.new @params.to_params_hash, @collector.find_all(&:file?).map(&:body)
|
|
@@ -252,12 +264,6 @@ module Rack
|
|
|
252
264
|
|
|
253
265
|
private
|
|
254
266
|
|
|
255
|
-
def dequote(str) # From WEBrick::HTTPUtils
|
|
256
|
-
ret = (/\A"(.*)"\Z/ =~ str) ? $1 : str.dup
|
|
257
|
-
ret.gsub!(/\\(.)/, "\\1")
|
|
258
|
-
ret
|
|
259
|
-
end
|
|
260
|
-
|
|
261
267
|
def read_data(io, outbuf)
|
|
262
268
|
content = io.read(@bufsize, outbuf)
|
|
263
269
|
handle_empty_content!(content)
|
|
@@ -279,7 +285,14 @@ module Rack
|
|
|
279
285
|
@state = :MIME_HEAD
|
|
280
286
|
return
|
|
281
287
|
when :END_BOUNDARY
|
|
282
|
-
# invalid multipart upload
|
|
288
|
+
# invalid multipart upload
|
|
289
|
+
if @sbuf.pos == @end_boundary_size && @sbuf.rest == EOL
|
|
290
|
+
# stop parsing a buffer if a buffer is only an end boundary.
|
|
291
|
+
@state = :DONE
|
|
292
|
+
return
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
# retry for opening boundary
|
|
283
296
|
else
|
|
284
297
|
# no boundary found, keep reading data
|
|
285
298
|
return :want_read
|
|
@@ -297,17 +310,101 @@ module Rack
|
|
|
297
310
|
end
|
|
298
311
|
end
|
|
299
312
|
|
|
313
|
+
CONTENT_DISPOSITION_MAX_PARAMS = 16
|
|
314
|
+
CONTENT_DISPOSITION_MAX_BYTES = 1536
|
|
300
315
|
def handle_mime_head
|
|
301
316
|
if @sbuf.scan_until(@head_regex)
|
|
302
317
|
head = @sbuf[1]
|
|
303
318
|
content_type = head[MULTIPART_CONTENT_TYPE, 1]
|
|
304
|
-
if
|
|
305
|
-
|
|
319
|
+
if (disposition = head[MULTIPART_CONTENT_DISPOSITION, 1]) &&
|
|
320
|
+
disposition.bytesize <= CONTENT_DISPOSITION_MAX_BYTES
|
|
321
|
+
|
|
322
|
+
# ignore actual content-disposition value (should always be form-data)
|
|
323
|
+
i = disposition.index(';')
|
|
324
|
+
disposition.slice!(0, i+1)
|
|
325
|
+
param = nil
|
|
326
|
+
num_params = 0
|
|
327
|
+
|
|
328
|
+
# Parse parameter list
|
|
329
|
+
while i = disposition.index('=')
|
|
330
|
+
# Only parse up to max parameters, to avoid potential denial of service
|
|
331
|
+
num_params += 1
|
|
332
|
+
break if num_params > CONTENT_DISPOSITION_MAX_PARAMS
|
|
333
|
+
|
|
334
|
+
# Found end of parameter name, ensure forward progress in loop
|
|
335
|
+
param = disposition.slice!(0, i+1)
|
|
336
|
+
|
|
337
|
+
# Remove ending equals and preceding whitespace from parameter name
|
|
338
|
+
param.chomp!('=')
|
|
339
|
+
param.lstrip!
|
|
340
|
+
|
|
341
|
+
if disposition[0] == '"'
|
|
342
|
+
# Parameter value is quoted, parse it, handling backslash escapes
|
|
343
|
+
disposition.slice!(0, 1)
|
|
344
|
+
value = String.new
|
|
345
|
+
|
|
346
|
+
while i = disposition.index(/(["\\])/)
|
|
347
|
+
c = $1
|
|
348
|
+
|
|
349
|
+
# Append all content until ending quote or escape
|
|
350
|
+
value << disposition.slice!(0, i)
|
|
351
|
+
|
|
352
|
+
# Remove either backslash or ending quote,
|
|
353
|
+
# ensures forward progress in loop
|
|
354
|
+
disposition.slice!(0, 1)
|
|
355
|
+
|
|
356
|
+
# stop parsing parameter value if found ending quote
|
|
357
|
+
break if c == '"'
|
|
358
|
+
|
|
359
|
+
escaped_char = disposition.slice!(0, 1)
|
|
360
|
+
if param == 'filename' && escaped_char != '"'
|
|
361
|
+
# Possible IE uploaded filename, append both escape backslash and value
|
|
362
|
+
value << c << escaped_char
|
|
363
|
+
else
|
|
364
|
+
# Other only append escaped value
|
|
365
|
+
value << escaped_char
|
|
366
|
+
end
|
|
367
|
+
end
|
|
368
|
+
else
|
|
369
|
+
if i = disposition.index(';')
|
|
370
|
+
# Parameter value unquoted (which may be invalid), value ends at semicolon
|
|
371
|
+
value = disposition.slice!(0, i)
|
|
372
|
+
else
|
|
373
|
+
# If no ending semicolon, assume remainder of line is value and stop
|
|
374
|
+
# parsing
|
|
375
|
+
disposition.strip!
|
|
376
|
+
value = disposition
|
|
377
|
+
disposition = ''
|
|
378
|
+
end
|
|
379
|
+
end
|
|
380
|
+
|
|
381
|
+
case param
|
|
382
|
+
when 'name'
|
|
383
|
+
name = value
|
|
384
|
+
when 'filename'
|
|
385
|
+
filename = value
|
|
386
|
+
when 'filename*'
|
|
387
|
+
filename_star = value
|
|
388
|
+
# else
|
|
389
|
+
# ignore other parameters
|
|
390
|
+
end
|
|
391
|
+
|
|
392
|
+
# skip trailing semicolon, to proceed to next parameter
|
|
393
|
+
if i = disposition.index(';')
|
|
394
|
+
disposition.slice!(0, i+1)
|
|
395
|
+
end
|
|
396
|
+
end
|
|
306
397
|
else
|
|
307
398
|
name = head[MULTIPART_CONTENT_ID, 1]
|
|
308
399
|
end
|
|
309
400
|
|
|
310
|
-
|
|
401
|
+
if filename_star
|
|
402
|
+
encoding, _, filename = filename_star.split("'", 3)
|
|
403
|
+
filename = normalize_filename(filename || '')
|
|
404
|
+
filename.force_encoding(find_encoding(encoding))
|
|
405
|
+
elsif filename
|
|
406
|
+
filename = normalize_filename(filename)
|
|
407
|
+
end
|
|
311
408
|
|
|
312
409
|
if name.nil? || name.empty?
|
|
313
410
|
name = filename || "#{content_type || TEXT_PLAIN}[]".dup
|
|
@@ -322,7 +419,7 @@ module Rack
|
|
|
322
419
|
|
|
323
420
|
def handle_mime_body
|
|
324
421
|
if (body_with_boundary = @sbuf.check_until(@body_regex)) # check but do not advance the pointer yet
|
|
325
|
-
body = body_with_boundary.sub(
|
|
422
|
+
body = body_with_boundary.sub(@body_regex_at_end, '') # remove the boundary from the string
|
|
326
423
|
@collector.on_mime_body @mime_index, body
|
|
327
424
|
@sbuf.pos += body.length + 2 # skip \r\n after the content
|
|
328
425
|
@state = :CONSUME_TOKEN
|
|
@@ -352,39 +449,14 @@ module Rack
|
|
|
352
449
|
end
|
|
353
450
|
end
|
|
354
451
|
|
|
355
|
-
def
|
|
356
|
-
filename = nil
|
|
357
|
-
case head
|
|
358
|
-
when RFC2183
|
|
359
|
-
params = Hash[*head.scan(DISPPARM).flat_map(&:compact)]
|
|
360
|
-
|
|
361
|
-
if filename = params['filename*']
|
|
362
|
-
encoding, _, filename = filename.split("'", 3)
|
|
363
|
-
elsif filename = params['filename']
|
|
364
|
-
filename = $1 if filename =~ /^"(.*)"$/
|
|
365
|
-
end
|
|
366
|
-
when BROKEN
|
|
367
|
-
filename = $1
|
|
368
|
-
filename = $1 if filename =~ /^"(.*)"$/
|
|
369
|
-
end
|
|
370
|
-
|
|
371
|
-
return unless filename
|
|
372
|
-
|
|
452
|
+
def normalize_filename(filename)
|
|
373
453
|
if filename.scan(/%.?.?/).all? { |s| /%[0-9a-fA-F]{2}/.match?(s) }
|
|
374
454
|
filename = Utils.unescape_path(filename)
|
|
375
455
|
end
|
|
376
456
|
|
|
377
457
|
filename.scrub!
|
|
378
458
|
|
|
379
|
-
|
|
380
|
-
filename = filename.gsub(/\\(.)/, '\1')
|
|
381
|
-
end
|
|
382
|
-
|
|
383
|
-
if encoding
|
|
384
|
-
filename.force_encoding ::Encoding.find(encoding)
|
|
385
|
-
end
|
|
386
|
-
|
|
387
|
-
filename
|
|
459
|
+
filename.split(/[\/\\]/).last || String.new
|
|
388
460
|
end
|
|
389
461
|
|
|
390
462
|
CHARSET = "charset"
|
|
@@ -410,11 +482,7 @@ module Rack
|
|
|
410
482
|
v.strip!
|
|
411
483
|
v = v[1..-2] if v.start_with?('"') && v.end_with?('"')
|
|
412
484
|
if k == "charset"
|
|
413
|
-
encoding =
|
|
414
|
-
Encoding.find v
|
|
415
|
-
rescue ArgumentError
|
|
416
|
-
Encoding::BINARY
|
|
417
|
-
end
|
|
485
|
+
encoding = find_encoding(v)
|
|
418
486
|
end
|
|
419
487
|
end
|
|
420
488
|
end
|
|
@@ -424,6 +492,34 @@ module Rack
|
|
|
424
492
|
body.force_encoding(encoding)
|
|
425
493
|
end
|
|
426
494
|
|
|
495
|
+
# Return the related Encoding object. However, because
|
|
496
|
+
# enc is submitted by the user, it may be invalid, so
|
|
497
|
+
# use a binary encoding in that case.
|
|
498
|
+
def find_encoding(enc)
|
|
499
|
+
Encoding.find enc
|
|
500
|
+
rescue ArgumentError
|
|
501
|
+
Encoding::BINARY
|
|
502
|
+
end
|
|
503
|
+
|
|
504
|
+
REENCODE_DUMMY_ENCODINGS = {
|
|
505
|
+
# ISO-2022-JP is a legacy but still widely used encoding in Japan
|
|
506
|
+
# Here we convert ISO-2022-JP to UTF-8 so that it can be handled.
|
|
507
|
+
Encoding::ISO_2022_JP => true
|
|
508
|
+
|
|
509
|
+
# Other dummy encodings are rarely used and have not been supported yet.
|
|
510
|
+
# Adding support for them will require careful considerations.
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
def handle_dummy_encoding(name, body)
|
|
514
|
+
# A string object with a 'dummy' encoding does not have full functionality and can cause errors.
|
|
515
|
+
# So here we covert it to UTF-8 so that it can be handled properly.
|
|
516
|
+
if name.encoding.dummy? && REENCODE_DUMMY_ENCODINGS[name.encoding]
|
|
517
|
+
name = name.encode(Encoding::UTF_8)
|
|
518
|
+
body = body.encode(Encoding::UTF_8)
|
|
519
|
+
end
|
|
520
|
+
return name, body
|
|
521
|
+
end
|
|
522
|
+
|
|
427
523
|
def handle_empty_content!(content)
|
|
428
524
|
if content.nil? || content.empty?
|
|
429
525
|
raise EmptyContentError
|
|
@@ -5,14 +5,47 @@ require 'fileutils'
|
|
|
5
5
|
|
|
6
6
|
module Rack
|
|
7
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.
|
|
8
19
|
class UploadedFile
|
|
9
|
-
|
|
10
|
-
#
|
|
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.
|
|
11
23
|
attr_reader :original_filename
|
|
12
24
|
|
|
13
|
-
# The content type of the
|
|
25
|
+
# The content type of the instance.
|
|
14
26
|
attr_accessor :content_type
|
|
15
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.
|
|
16
49
|
def initialize(filepath = nil, ct = "text/plain", bin = false,
|
|
17
50
|
path: filepath, content_type: ct, binary: bin, filename: nil, io: nil)
|
|
18
51
|
if io
|
|
@@ -28,15 +61,19 @@ module Rack
|
|
|
28
61
|
@content_type = content_type
|
|
29
62
|
end
|
|
30
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.
|
|
31
66
|
def path
|
|
32
67
|
@tempfile.path if @tempfile.respond_to?(:path)
|
|
33
68
|
end
|
|
34
69
|
alias_method :local_path, :path
|
|
35
70
|
|
|
36
|
-
|
|
37
|
-
|
|
71
|
+
# Return true if the tempfile responds to the method.
|
|
72
|
+
def respond_to_missing?(*args)
|
|
73
|
+
@tempfile.respond_to?(*args)
|
|
38
74
|
end
|
|
39
75
|
|
|
76
|
+
# Delegate method missing calls to the tempfile.
|
|
40
77
|
def method_missing(method_name, *args, &block) #:nodoc:
|
|
41
78
|
@tempfile.__send__(method_name, *args, &block)
|
|
42
79
|
end
|