rack 2.2.25 → 3.0.0.beta1
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 +134 -206
- data/CONTRIBUTING.md +53 -47
- data/MIT-LICENSE +1 -1
- data/README.md +287 -0
- data/Rakefile +40 -7
- data/SPEC.rdoc +166 -125
- data/contrib/LICENSE.md +7 -0
- data/contrib/logo.webp +0 -0
- data/lib/rack/auth/abstract/handler.rb +3 -1
- data/lib/rack/auth/abstract/request.rb +3 -1
- data/lib/rack/auth/basic.rb +2 -1
- data/lib/rack/auth/digest/md5.rb +1 -131
- data/lib/rack/auth/digest/nonce.rb +1 -53
- data/lib/rack/auth/digest/params.rb +1 -54
- data/lib/rack/auth/digest/request.rb +1 -43
- data/lib/rack/auth/digest.rb +256 -0
- data/lib/rack/body_proxy.rb +3 -1
- data/lib/rack/builder.rb +60 -42
- data/lib/rack/cascade.rb +2 -0
- data/lib/rack/chunked.rb +16 -13
- data/lib/rack/common_logger.rb +24 -20
- data/lib/rack/conditional_get.rb +18 -15
- data/lib/rack/constants.rb +62 -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 +12 -9
- data/lib/rack/etag.rb +14 -23
- data/lib/rack/events.rb +4 -0
- data/lib/rack/file.rb +2 -0
- data/lib/rack/files.rb +16 -18
- data/lib/rack/head.rb +9 -8
- data/lib/rack/headers.rb +154 -0
- data/lib/rack/lint.rb +740 -649
- data/lib/rack/lock.rb +2 -5
- data/lib/rack/logger.rb +2 -0
- data/lib/rack/media_type.rb +7 -17
- data/lib/rack/method_override.rb +5 -1
- data/lib/rack/mime.rb +8 -0
- data/lib/rack/mock.rb +1 -300
- data/lib/rack/mock_request.rb +166 -0
- data/lib/rack/mock_response.rb +124 -0
- data/lib/rack/multipart/generator.rb +7 -5
- data/lib/rack/multipart/parser.rb +122 -193
- data/lib/rack/multipart/uploaded_file.rb +4 -0
- data/lib/rack/multipart.rb +20 -40
- data/lib/rack/null_logger.rb +9 -0
- data/lib/rack/query_parser.rb +78 -91
- data/lib/rack/recursive.rb +2 -0
- data/lib/rack/reloader.rb +0 -2
- data/lib/rack/request.rb +190 -95
- data/lib/rack/response.rb +131 -61
- data/lib/rack/rewindable_input.rb +24 -5
- data/lib/rack/runtime.rb +7 -6
- data/lib/rack/sendfile.rb +40 -65
- data/lib/rack/show_exceptions.rb +15 -2
- data/lib/rack/show_status.rb +17 -7
- data/lib/rack/static.rb +12 -17
- data/lib/rack/tempfile_reaper.rb +15 -4
- data/lib/rack/urlmap.rb +4 -2
- data/lib/rack/utils.rb +219 -240
- data/lib/rack/version.rb +9 -4
- data/lib/rack.rb +5 -76
- data/rack.gemspec +6 -6
- metadata +22 -31
- data/README.rdoc +0 -355
- 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/lib/rack/core_ext/regexp.rb +0 -14
- 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/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
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'cgi/cookie'
|
|
4
|
+
require 'time'
|
|
5
|
+
|
|
6
|
+
require_relative 'response'
|
|
7
|
+
|
|
8
|
+
module Rack
|
|
9
|
+
# Rack::MockResponse provides useful helpers for testing your apps.
|
|
10
|
+
# Usually, you don't create the MockResponse on your own, but use
|
|
11
|
+
# MockRequest.
|
|
12
|
+
|
|
13
|
+
class MockResponse < Rack::Response
|
|
14
|
+
class << self
|
|
15
|
+
alias [] new
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# Headers
|
|
19
|
+
attr_reader :original_headers, :cookies
|
|
20
|
+
|
|
21
|
+
# Errors
|
|
22
|
+
attr_accessor :errors
|
|
23
|
+
|
|
24
|
+
def initialize(status, headers, body, errors = nil)
|
|
25
|
+
@original_headers = headers
|
|
26
|
+
|
|
27
|
+
if errors
|
|
28
|
+
@errors = errors.string if errors.respond_to?(:string)
|
|
29
|
+
else
|
|
30
|
+
@errors = ""
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
super(body, status, headers)
|
|
34
|
+
|
|
35
|
+
@cookies = parse_cookies_from_header
|
|
36
|
+
buffered_body!
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def =~(other)
|
|
40
|
+
body =~ other
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def match(other)
|
|
44
|
+
body.match other
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def body
|
|
48
|
+
# FIXME: apparently users of MockResponse expect the return value of
|
|
49
|
+
# MockResponse#body to be a string. However, the real response object
|
|
50
|
+
# returns the body as a list.
|
|
51
|
+
#
|
|
52
|
+
# See spec_showstatus.rb:
|
|
53
|
+
#
|
|
54
|
+
# should "not replace existing messages" do
|
|
55
|
+
# ...
|
|
56
|
+
# res.body.should == "foo!"
|
|
57
|
+
# end
|
|
58
|
+
buffer = String.new
|
|
59
|
+
|
|
60
|
+
super.each do |chunk|
|
|
61
|
+
buffer << chunk
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
return buffer
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def empty?
|
|
68
|
+
[201, 204, 304].include? status
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def cookie(name)
|
|
72
|
+
cookies.fetch(name, nil)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
private
|
|
76
|
+
|
|
77
|
+
def parse_cookies_from_header
|
|
78
|
+
cookies = Hash.new
|
|
79
|
+
if headers.has_key? 'set-cookie'
|
|
80
|
+
set_cookie_header = headers.fetch('set-cookie')
|
|
81
|
+
Array(set_cookie_header).each do |header_value|
|
|
82
|
+
header_value.split("\n").each do |cookie|
|
|
83
|
+
cookie_name, cookie_filling = cookie.split('=', 2)
|
|
84
|
+
cookie_attributes = identify_cookie_attributes cookie_filling
|
|
85
|
+
parsed_cookie = CGI::Cookie.new(
|
|
86
|
+
'name' => cookie_name.strip,
|
|
87
|
+
'value' => cookie_attributes.fetch('value'),
|
|
88
|
+
'path' => cookie_attributes.fetch('path', nil),
|
|
89
|
+
'domain' => cookie_attributes.fetch('domain', nil),
|
|
90
|
+
'expires' => cookie_attributes.fetch('expires', nil),
|
|
91
|
+
'secure' => cookie_attributes.fetch('secure', false)
|
|
92
|
+
)
|
|
93
|
+
cookies.store(cookie_name, parsed_cookie)
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
cookies
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def identify_cookie_attributes(cookie_filling)
|
|
101
|
+
cookie_bits = cookie_filling.split(';')
|
|
102
|
+
cookie_attributes = Hash.new
|
|
103
|
+
cookie_attributes.store('value', cookie_bits[0].strip)
|
|
104
|
+
cookie_bits.drop(1).each do |bit|
|
|
105
|
+
if bit.include? '='
|
|
106
|
+
cookie_attribute, attribute_value = bit.split('=', 2)
|
|
107
|
+
cookie_attributes.store(cookie_attribute.strip.downcase, attribute_value.strip)
|
|
108
|
+
end
|
|
109
|
+
if bit.include? 'secure'
|
|
110
|
+
cookie_attributes.store('secure', true)
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
if cookie_attributes.key? 'max-age'
|
|
115
|
+
cookie_attributes.store('expires', Time.now + cookie_attributes['max-age'].to_i)
|
|
116
|
+
elsif cookie_attributes.key? 'expires'
|
|
117
|
+
cookie_attributes.store('expires', Time.httpdate(cookie_attributes['expires']))
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
cookie_attributes
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
end
|
|
124
|
+
end
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative 'uploaded_file'
|
|
4
|
+
|
|
3
5
|
module Rack
|
|
4
6
|
module Multipart
|
|
5
7
|
class Generator
|
|
@@ -74,12 +76,12 @@ module Rack
|
|
|
74
76
|
|
|
75
77
|
def content_for_tempfile(io, file, name)
|
|
76
78
|
length = ::File.stat(file.path).size if file.path
|
|
77
|
-
filename = "; filename=\"#{Utils.
|
|
79
|
+
filename = "; filename=\"#{Utils.escape_path(file.original_filename)}\""
|
|
78
80
|
<<-EOF
|
|
79
81
|
--#{MULTIPART_BOUNDARY}\r
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
#{"
|
|
82
|
+
content-disposition: form-data; name="#{name}"#{filename}\r
|
|
83
|
+
content-type: #{file.content_type}\r
|
|
84
|
+
#{"content-length: #{length}\r\n" if length}\r
|
|
83
85
|
#{io.read}\r
|
|
84
86
|
EOF
|
|
85
87
|
end
|
|
@@ -87,7 +89,7 @@ EOF
|
|
|
87
89
|
def content_for_other(file, name)
|
|
88
90
|
<<-EOF
|
|
89
91
|
--#{MULTIPART_BOUNDARY}\r
|
|
90
|
-
|
|
92
|
+
content-disposition: form-data; name="#{name}"\r
|
|
91
93
|
\r
|
|
92
94
|
#{file}\r
|
|
93
95
|
EOF
|
|
@@ -2,56 +2,52 @@
|
|
|
2
2
|
|
|
3
3
|
require 'strscan'
|
|
4
4
|
|
|
5
|
+
require_relative '../utils'
|
|
6
|
+
|
|
5
7
|
module Rack
|
|
6
8
|
module Multipart
|
|
7
9
|
class MultipartPartLimitError < Errno::EMFILE; end
|
|
8
|
-
class MultipartTotalPartLimitError < StandardError; end
|
|
9
10
|
|
|
10
|
-
class
|
|
11
|
-
|
|
11
|
+
# Use specific error class when parsing multipart request
|
|
12
|
+
# that ends early.
|
|
13
|
+
class EmptyContentError < ::EOFError; end
|
|
14
|
+
|
|
15
|
+
# Base class for multipart exceptions that do not subclass from
|
|
16
|
+
# other exception classes for backwards compatibility.
|
|
17
|
+
class Error < StandardError; end
|
|
18
|
+
|
|
19
|
+
EOL = "\r\n"
|
|
20
|
+
MULTIPART = %r|\Amultipart/.*boundary=\"?([^\";,]+)\"?|ni
|
|
21
|
+
TOKEN = /[^\s()<>,;:\\"\/\[\]?=]+/
|
|
22
|
+
CONDISP = /Content-Disposition:\s*#{TOKEN}\s*/i
|
|
23
|
+
VALUE = /"(?:\\"|[^"])*"|#{TOKEN}/
|
|
24
|
+
BROKEN = /^#{CONDISP}.*;\s*filename=(#{VALUE})/i
|
|
25
|
+
MULTIPART_CONTENT_TYPE = /Content-Type: (.*)#{EOL}/ni
|
|
26
|
+
MULTIPART_CONTENT_DISPOSITION = /Content-Disposition:.*;\s*name=(#{VALUE})/ni
|
|
27
|
+
MULTIPART_CONTENT_ID = /Content-ID:\s*([^#{EOL}]*)/ni
|
|
28
|
+
# Updated definitions from RFC 2231
|
|
29
|
+
ATTRIBUTE_CHAR = %r{[^ \t\v\n\r)(><@,;:\\"/\[\]?='*%]}
|
|
30
|
+
ATTRIBUTE = /#{ATTRIBUTE_CHAR}+/
|
|
31
|
+
SECTION = /\*[0-9]+/
|
|
32
|
+
REGULAR_PARAMETER_NAME = /#{ATTRIBUTE}#{SECTION}?/
|
|
33
|
+
REGULAR_PARAMETER = /(#{REGULAR_PARAMETER_NAME})=(#{VALUE})/
|
|
34
|
+
EXTENDED_OTHER_NAME = /#{ATTRIBUTE}\*[1-9][0-9]*\*/
|
|
35
|
+
EXTENDED_OTHER_VALUE = /%[0-9a-fA-F]{2}|#{ATTRIBUTE_CHAR}/
|
|
36
|
+
EXTENDED_OTHER_PARAMETER = /(#{EXTENDED_OTHER_NAME})=(#{EXTENDED_OTHER_VALUE}*)/
|
|
37
|
+
EXTENDED_INITIAL_NAME = /#{ATTRIBUTE}(?:\*0)?\*/
|
|
38
|
+
EXTENDED_INITIAL_VALUE = /[a-zA-Z0-9\-]*'[a-zA-Z0-9\-]*'#{EXTENDED_OTHER_VALUE}*/
|
|
39
|
+
EXTENDED_INITIAL_PARAMETER = /(#{EXTENDED_INITIAL_NAME})=(#{EXTENDED_INITIAL_VALUE})/
|
|
40
|
+
EXTENDED_PARAMETER = /#{EXTENDED_INITIAL_PARAMETER}|#{EXTENDED_OTHER_PARAMETER}/
|
|
41
|
+
DISPPARM = /;\s*(?:#{REGULAR_PARAMETER}|#{EXTENDED_PARAMETER})\s*/
|
|
42
|
+
RFC2183 = /^#{CONDISP}(#{DISPPARM})+$/i
|
|
12
43
|
|
|
44
|
+
class Parser
|
|
13
45
|
BUFSIZE = 1_048_576
|
|
14
46
|
TEXT_PLAIN = "text/plain"
|
|
15
47
|
TEMPFILE_FACTORY = lambda { |filename, content_type|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
Tempfile.new(["RackMultipart", extension])
|
|
48
|
+
Tempfile.new(["RackMultipart", ::File.extname(filename.gsub("\0", '%00'))])
|
|
19
49
|
}
|
|
20
50
|
|
|
21
|
-
BOUNDARY_REGEX = /\A([^\n]*(?:\n|\Z))/
|
|
22
|
-
|
|
23
|
-
LINE_END = /\n/
|
|
24
|
-
private_constant :LINE_END
|
|
25
|
-
|
|
26
|
-
BOUNDARY_START_LIMIT = 16 * 1024
|
|
27
|
-
private_constant :BOUNDARY_START_LIMIT
|
|
28
|
-
|
|
29
|
-
MIME_HEADER_BYTESIZE_LIMIT = 64 * 1024
|
|
30
|
-
|
|
31
|
-
OBS_UNFOLD = /\r\n([ \t])/
|
|
32
|
-
|
|
33
|
-
private_constant :OBS_UNFOLD
|
|
34
|
-
private_constant :MIME_HEADER_BYTESIZE_LIMIT
|
|
35
|
-
|
|
36
|
-
env_int = lambda do |key, val|
|
|
37
|
-
if str_val = ENV[key]
|
|
38
|
-
begin
|
|
39
|
-
val = Integer(str_val, 10)
|
|
40
|
-
rescue ArgumentError
|
|
41
|
-
raise ArgumentError, "non-integer value provided for environment variable #{key}"
|
|
42
|
-
end
|
|
43
|
-
end
|
|
44
|
-
|
|
45
|
-
val
|
|
46
|
-
end
|
|
47
|
-
|
|
48
|
-
BUFFERED_UPLOAD_BYTESIZE_LIMIT = env_int.call("RACK_MULTIPART_BUFFERED_UPLOAD_BYTESIZE_LIMIT", 16 * 1024 * 1024)
|
|
49
|
-
private_constant :BUFFERED_UPLOAD_BYTESIZE_LIMIT
|
|
50
|
-
|
|
51
|
-
bytesize_limit = env_int.call("RACK_MULTIPART_PARSER_BYTESIZE_LIMIT", 10 * 1024 * 1024 * 1024)
|
|
52
|
-
PARSER_BYTESIZE_LIMIT = bytesize_limit > 0 ? bytesize_limit : nil
|
|
53
|
-
private_constant :PARSER_BYTESIZE_LIMIT
|
|
54
|
-
|
|
55
51
|
class BoundedIO # :nodoc:
|
|
56
52
|
def initialize(io, content_length)
|
|
57
53
|
@io = io
|
|
@@ -73,16 +69,12 @@ module Rack
|
|
|
73
69
|
if str
|
|
74
70
|
@cursor += str.bytesize
|
|
75
71
|
else
|
|
76
|
-
# Raise an error for mismatching
|
|
72
|
+
# Raise an error for mismatching content-length and actual contents
|
|
77
73
|
raise EOFError, "bad content body"
|
|
78
74
|
end
|
|
79
75
|
|
|
80
76
|
str
|
|
81
77
|
end
|
|
82
|
-
|
|
83
|
-
def rewind
|
|
84
|
-
@io.rewind
|
|
85
|
-
end
|
|
86
78
|
end
|
|
87
79
|
|
|
88
80
|
MultipartInfo = Struct.new :params, :tmp_files
|
|
@@ -92,19 +84,7 @@ module Rack
|
|
|
92
84
|
return unless content_type
|
|
93
85
|
data = content_type.match(MULTIPART)
|
|
94
86
|
return unless data
|
|
95
|
-
|
|
96
|
-
unless data[1].empty?
|
|
97
|
-
raise EOFError, "whitespace between boundary parameter name and equal sign"
|
|
98
|
-
end
|
|
99
|
-
# Count across the whole header rather than only after the match: the
|
|
100
|
-
# lazy prefix in MULTIPART can skip a first boundary parameter whose
|
|
101
|
-
# value is empty or starts with a quote, semicolon, or comma. Also
|
|
102
|
-
# count RFC 2231 boundary* parameters, which some parsers prefer.
|
|
103
|
-
if content_type.scan(/boundary(?:\*\d*\*?)?\s*=/i).size > 1
|
|
104
|
-
raise EOFError, "multiple boundary parameters found in multipart content type"
|
|
105
|
-
end
|
|
106
|
-
|
|
107
|
-
data[2]
|
|
87
|
+
data[1]
|
|
108
88
|
end
|
|
109
89
|
|
|
110
90
|
def self.parse(io, content_length, content_type, tmpfile, bufsize, qp)
|
|
@@ -113,22 +93,17 @@ module Rack
|
|
|
113
93
|
boundary = parse_boundary content_type
|
|
114
94
|
return EMPTY unless boundary
|
|
115
95
|
|
|
116
|
-
if
|
|
117
|
-
|
|
96
|
+
if boundary.length > 70
|
|
97
|
+
# RFC 1521 Section 7.2.1 imposes a 70 character maximum for the boundary.
|
|
98
|
+
# Most clients use no more than 55 characters.
|
|
99
|
+
raise Error, "multipart boundary size too large (#{boundary.length} characters)"
|
|
118
100
|
end
|
|
119
101
|
|
|
120
102
|
io = BoundedIO.new(io, content_length) if content_length
|
|
121
|
-
outbuf = String.new
|
|
122
103
|
|
|
123
104
|
parser = new(boundary, tmpfile, bufsize, qp)
|
|
124
|
-
parser.
|
|
105
|
+
parser.parse(io)
|
|
125
106
|
|
|
126
|
-
loop do
|
|
127
|
-
break if parser.state == :DONE
|
|
128
|
-
parser.on_read io.read(bufsize, outbuf)
|
|
129
|
-
end
|
|
130
|
-
|
|
131
|
-
io.rewind
|
|
132
107
|
parser.result
|
|
133
108
|
end
|
|
134
109
|
|
|
@@ -191,7 +166,7 @@ module Rack
|
|
|
191
166
|
|
|
192
167
|
@mime_parts[mime_index] = klass.new(body, head, filename, content_type, name)
|
|
193
168
|
|
|
194
|
-
|
|
169
|
+
check_open_files
|
|
195
170
|
end
|
|
196
171
|
|
|
197
172
|
def on_mime_body(mime_index, content)
|
|
@@ -203,23 +178,13 @@ module Rack
|
|
|
203
178
|
|
|
204
179
|
private
|
|
205
180
|
|
|
206
|
-
def
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
if file_limit && file_limit > 0
|
|
211
|
-
if @open_files >= file_limit
|
|
181
|
+
def check_open_files
|
|
182
|
+
if Utils.multipart_part_limit > 0
|
|
183
|
+
if @open_files >= Utils.multipart_part_limit
|
|
212
184
|
@mime_parts.each(&:close)
|
|
213
185
|
raise MultipartPartLimitError, 'Maximum file multiparts in content reached'
|
|
214
186
|
end
|
|
215
187
|
end
|
|
216
|
-
|
|
217
|
-
if part_limit && part_limit > 0
|
|
218
|
-
if @mime_parts.size >= part_limit
|
|
219
|
-
@mime_parts.each(&:close)
|
|
220
|
-
raise MultipartTotalPartLimitError, 'Maximum total multiparts in content reached'
|
|
221
|
-
end
|
|
222
|
-
end
|
|
223
188
|
end
|
|
224
189
|
end
|
|
225
190
|
|
|
@@ -228,42 +193,46 @@ module Rack
|
|
|
228
193
|
def initialize(boundary, tempfile, bufsize, query_parser)
|
|
229
194
|
@query_parser = query_parser
|
|
230
195
|
@params = query_parser.make_params
|
|
231
|
-
@boundary = "--#{boundary}"
|
|
232
196
|
@bufsize = bufsize
|
|
233
197
|
|
|
234
|
-
@full_boundary = @boundary
|
|
235
|
-
@end_boundary = @boundary + '--'
|
|
236
198
|
@state = :FAST_FORWARD
|
|
237
199
|
@mime_index = 0
|
|
238
|
-
@body_retained = nil
|
|
239
|
-
@retained_size = 0
|
|
240
|
-
@total_bytes_read = (0 if PARSER_BYTESIZE_LIMIT)
|
|
241
200
|
@collector = Collector.new tempfile
|
|
242
201
|
|
|
243
202
|
@sbuf = StringScanner.new("".dup)
|
|
244
|
-
@body_regex = /(?:#{EOL})
|
|
245
|
-
@
|
|
246
|
-
@rx_max_size = EOL.size + @boundary.bytesize + [EOL.size, '--'.size].max
|
|
203
|
+
@body_regex = /(?:#{EOL}|\A)--#{Regexp.quote(boundary)}(?:#{EOL}|--)/m
|
|
204
|
+
@rx_max_size = boundary.bytesize + 6 # (\r\n-- at start, either \r\n or -- at finish)
|
|
247
205
|
@head_regex = /(.*?#{EOL})#{EOL}/m
|
|
248
206
|
end
|
|
249
207
|
|
|
250
|
-
def
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
208
|
+
def parse(io)
|
|
209
|
+
outbuf = String.new
|
|
210
|
+
read_data(io, outbuf)
|
|
211
|
+
|
|
212
|
+
loop do
|
|
213
|
+
status =
|
|
214
|
+
case @state
|
|
215
|
+
when :FAST_FORWARD
|
|
216
|
+
handle_fast_forward
|
|
217
|
+
when :CONSUME_TOKEN
|
|
218
|
+
handle_consume_token
|
|
219
|
+
when :MIME_HEAD
|
|
220
|
+
handle_mime_head
|
|
221
|
+
when :MIME_BODY
|
|
222
|
+
handle_mime_body
|
|
223
|
+
else # when :DONE
|
|
224
|
+
return
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
read_data(io, outbuf) if status == :want_read
|
|
257
228
|
end
|
|
258
|
-
@sbuf.concat content
|
|
259
|
-
run_parser
|
|
260
229
|
end
|
|
261
230
|
|
|
262
231
|
def result
|
|
263
232
|
@collector.each do |part|
|
|
264
233
|
part.get_data do |data|
|
|
265
234
|
tag_multipart_encoding(part.filename, part.content_type, part.name, data)
|
|
266
|
-
@query_parser.normalize_params(@params, part.name, data
|
|
235
|
+
@query_parser.normalize_params(@params, part.name, data)
|
|
267
236
|
end
|
|
268
237
|
end
|
|
269
238
|
MultipartInfo.new @params.to_params_hash, @collector.find_all(&:file?).map(&:body)
|
|
@@ -271,40 +240,38 @@ module Rack
|
|
|
271
240
|
|
|
272
241
|
private
|
|
273
242
|
|
|
274
|
-
def
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
break if handle_fast_forward == :want_read
|
|
279
|
-
when :CONSUME_TOKEN
|
|
280
|
-
break if handle_consume_token == :want_read
|
|
281
|
-
when :MIME_HEAD
|
|
282
|
-
break if handle_mime_head == :want_read
|
|
283
|
-
when :MIME_BODY
|
|
284
|
-
break if handle_mime_body == :want_read
|
|
285
|
-
when :DONE
|
|
286
|
-
break
|
|
287
|
-
end
|
|
288
|
-
end
|
|
243
|
+
def dequote(str) # From WEBrick::HTTPUtils
|
|
244
|
+
ret = (/\A"(.*)"\Z/ =~ str) ? $1 : str.dup
|
|
245
|
+
ret.gsub!(/\\(.)/, "\\1")
|
|
246
|
+
ret
|
|
289
247
|
end
|
|
290
248
|
|
|
291
|
-
def
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
@state = :DONE
|
|
297
|
-
elsif tok
|
|
298
|
-
@state = :MIME_HEAD
|
|
299
|
-
else
|
|
300
|
-
raise EOFError, "bad content body" if @sbuf.rest_size >= @bufsize
|
|
301
|
-
|
|
302
|
-
# We raise if we don't find the multipart boundary, to avoid unbounded memory
|
|
303
|
-
# buffering. Note that the actual limit is the higher of 16KB and the buffer size (1MB by default)
|
|
304
|
-
raise EOFError, "multipart boundary not found within limit" if @sbuf.string.bytesize > BOUNDARY_START_LIMIT
|
|
249
|
+
def read_data(io, outbuf)
|
|
250
|
+
content = io.read(@bufsize, outbuf)
|
|
251
|
+
handle_empty_content!(content)
|
|
252
|
+
@sbuf.concat(content)
|
|
253
|
+
end
|
|
305
254
|
|
|
306
|
-
|
|
307
|
-
|
|
255
|
+
# This handles the initial parser state. We read until we find the starting
|
|
256
|
+
# boundary, then we can transition to the next state. If we find the ending
|
|
257
|
+
# boundary, this is an invalid multipart upload, but keep scanning for opening
|
|
258
|
+
# boundary in that case. If no boundary found, we need to keep reading data
|
|
259
|
+
# and retry. It's highly unlikely the initial read will not consume the
|
|
260
|
+
# boundary. The client would have to deliberately craft a response
|
|
261
|
+
# with the opening boundary beyond the buffer size for that to happen.
|
|
262
|
+
def handle_fast_forward
|
|
263
|
+
while true
|
|
264
|
+
case consume_boundary
|
|
265
|
+
when :BOUNDARY
|
|
266
|
+
# found opening boundary, transition to next state
|
|
267
|
+
@state = :MIME_HEAD
|
|
268
|
+
return
|
|
269
|
+
when :END_BOUNDARY
|
|
270
|
+
# invalid multipart upload, but retry for opening boundary
|
|
271
|
+
else
|
|
272
|
+
# no boundary found, keep reading data
|
|
273
|
+
return :want_read
|
|
274
|
+
end
|
|
308
275
|
end
|
|
309
276
|
end
|
|
310
277
|
|
|
@@ -321,47 +288,29 @@ module Rack
|
|
|
321
288
|
def handle_mime_head
|
|
322
289
|
if @sbuf.scan_until(@head_regex)
|
|
323
290
|
head = @sbuf[1]
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
content_type = unfolded_head[MULTIPART_CONTENT_TYPE, 1]
|
|
328
|
-
if name = unfolded_head[MULTIPART_CONTENT_DISPOSITION, 1]
|
|
329
|
-
name = Rack::Auth::Digest::Params::dequote(name)
|
|
291
|
+
content_type = head[MULTIPART_CONTENT_TYPE, 1]
|
|
292
|
+
if name = head[MULTIPART_CONTENT_DISPOSITION, 1]
|
|
293
|
+
name = dequote(name)
|
|
330
294
|
else
|
|
331
|
-
name =
|
|
295
|
+
name = head[MULTIPART_CONTENT_ID, 1]
|
|
332
296
|
end
|
|
333
297
|
|
|
334
|
-
filename = get_filename(
|
|
298
|
+
filename = get_filename(head)
|
|
335
299
|
|
|
336
300
|
if name.nil? || name.empty?
|
|
337
301
|
name = filename || "#{content_type || TEXT_PLAIN}[]".dup
|
|
338
302
|
end
|
|
339
303
|
|
|
340
|
-
# Mime part head data is retained for both TempfilePart and BufferPart
|
|
341
|
-
# for the entireity of the parse, even though it isn't used for BufferPart.
|
|
342
|
-
update_retained_size(head.bytesize)
|
|
343
|
-
|
|
344
|
-
# If a filename is given, a TempfilePart will be used, so the body will
|
|
345
|
-
# not be buffered in memory. However, if a filename is not given, a BufferPart
|
|
346
|
-
# will be used, and the body will be buffered in memory.
|
|
347
|
-
@body_retained = !filename
|
|
348
|
-
|
|
349
304
|
@collector.on_mime_head @mime_index, head, filename, content_type, name
|
|
350
305
|
@state = :MIME_BODY
|
|
351
306
|
else
|
|
352
|
-
|
|
353
|
-
# buffering. Note that the actual limit is the higher of 64KB and the buffer size (1MB by default)
|
|
354
|
-
raise EOFError, "multipart mime part header too large" if @sbuf.rest.bytesize > MIME_HEADER_BYTESIZE_LIMIT
|
|
355
|
-
|
|
356
|
-
@sbuf.string = @sbuf.rest
|
|
357
|
-
return :want_read
|
|
307
|
+
:want_read
|
|
358
308
|
end
|
|
359
309
|
end
|
|
360
310
|
|
|
361
311
|
def handle_mime_body
|
|
362
312
|
if (body_with_boundary = @sbuf.check_until(@body_regex)) # check but do not advance the pointer yet
|
|
363
313
|
body = body_with_boundary.sub(/#{@body_regex}\z/m, '') # remove the boundary from the string
|
|
364
|
-
update_retained_size(body.bytesize) if @body_retained
|
|
365
314
|
@collector.on_mime_body @mime_index, body
|
|
366
315
|
@sbuf.pos += body.length + 2 # skip \r\n after the content
|
|
367
316
|
@state = :CONSUME_TOKEN
|
|
@@ -370,51 +319,24 @@ module Rack
|
|
|
370
319
|
# Save what we have so far
|
|
371
320
|
if @rx_max_size < @sbuf.rest_size
|
|
372
321
|
delta = @sbuf.rest_size - @rx_max_size
|
|
373
|
-
|
|
374
|
-
update_retained_size(body.bytesize) if @body_retained
|
|
375
|
-
@collector.on_mime_body @mime_index, body
|
|
322
|
+
@collector.on_mime_body @mime_index, @sbuf.peek(delta)
|
|
376
323
|
@sbuf.pos += delta
|
|
324
|
+
@sbuf.string = @sbuf.rest
|
|
377
325
|
end
|
|
378
|
-
@sbuf.string = @sbuf.rest
|
|
379
326
|
:want_read
|
|
380
327
|
end
|
|
381
328
|
end
|
|
382
329
|
|
|
383
|
-
def full_boundary; @full_boundary; end
|
|
384
|
-
|
|
385
|
-
def update_retained_size(size)
|
|
386
|
-
@retained_size += size
|
|
387
|
-
if @retained_size > BUFFERED_UPLOAD_BYTESIZE_LIMIT
|
|
388
|
-
raise EOFError, "multipart data over retained size limit"
|
|
389
|
-
end
|
|
390
|
-
end
|
|
391
|
-
|
|
392
330
|
# Scan until the we find the start or end of the boundary.
|
|
393
331
|
# If we find it, return the appropriate symbol for the start or
|
|
394
332
|
# end of the boundary. If we don't find the start or end of the
|
|
395
333
|
# boundary, clear the buffer and return nil.
|
|
396
334
|
def consume_boundary
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
when full_boundary then return :BOUNDARY
|
|
400
|
-
when @end_boundary then return :END_BOUNDARY
|
|
401
|
-
end
|
|
402
|
-
return if @sbuf.eos?
|
|
403
|
-
end
|
|
404
|
-
end
|
|
405
|
-
|
|
406
|
-
# Return the next line of the buffer including its trailing "\n", or the
|
|
407
|
-
# rest of the buffer if it contains no "\n", and advance past it. This is
|
|
408
|
-
# equivalent to `@sbuf.scan_until(BOUNDARY_REGEX)`, but searching for the
|
|
409
|
-
# "\n" directly avoids the regexp engine's backtracking stack, which grows
|
|
410
|
-
# by one entry per byte matched by `[^\n]*` (tens of MB for a 1 MB line).
|
|
411
|
-
def scan_line
|
|
412
|
-
if line = @sbuf.scan_until(LINE_END)
|
|
413
|
-
line
|
|
335
|
+
if read_buffer = @sbuf.scan_until(@body_regex)
|
|
336
|
+
read_buffer.end_with?(EOL) ? :BOUNDARY : :END_BOUNDARY
|
|
414
337
|
else
|
|
415
|
-
line = @sbuf.rest
|
|
416
338
|
@sbuf.terminate
|
|
417
|
-
|
|
339
|
+
nil
|
|
418
340
|
end
|
|
419
341
|
end
|
|
420
342
|
|
|
@@ -424,10 +346,10 @@ module Rack
|
|
|
424
346
|
when RFC2183
|
|
425
347
|
params = Hash[*head.scan(DISPPARM).flat_map(&:compact)]
|
|
426
348
|
|
|
427
|
-
if filename = params['filename']
|
|
428
|
-
filename = $1 if filename =~ /^"(.*)"$/
|
|
429
|
-
elsif filename = params['filename*']
|
|
349
|
+
if filename = params['filename*']
|
|
430
350
|
encoding, _, filename = filename.split("'", 3)
|
|
351
|
+
elsif filename = params['filename']
|
|
352
|
+
filename = $1 if filename =~ /^"(.*)"$/
|
|
431
353
|
end
|
|
432
354
|
when BROKEN
|
|
433
355
|
filename = $1
|
|
@@ -454,6 +376,7 @@ module Rack
|
|
|
454
376
|
end
|
|
455
377
|
|
|
456
378
|
CHARSET = "charset"
|
|
379
|
+
deprecate_constant :CHARSET
|
|
457
380
|
|
|
458
381
|
def tag_multipart_encoding(filename, content_type, name, body)
|
|
459
382
|
name = name.to_s
|
|
@@ -474,7 +397,13 @@ module Rack
|
|
|
474
397
|
k.strip!
|
|
475
398
|
v.strip!
|
|
476
399
|
v = v[1..-2] if v.start_with?('"') && v.end_with?('"')
|
|
477
|
-
|
|
400
|
+
if k == "charset"
|
|
401
|
+
encoding = begin
|
|
402
|
+
Encoding.find v
|
|
403
|
+
rescue ArgumentError
|
|
404
|
+
Encoding::BINARY
|
|
405
|
+
end
|
|
406
|
+
end
|
|
478
407
|
end
|
|
479
408
|
end
|
|
480
409
|
end
|
|
@@ -485,7 +414,7 @@ module Rack
|
|
|
485
414
|
|
|
486
415
|
def handle_empty_content!(content)
|
|
487
416
|
if content.nil? || content.empty?
|
|
488
|
-
raise
|
|
417
|
+
raise EmptyContentError
|
|
489
418
|
end
|
|
490
419
|
end
|
|
491
420
|
end
|