rack 2.2.23 → 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 -188
- 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 +119 -160
- 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,49 +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
|
-
BOUNDARY_START_LIMIT = 16 * 1024
|
|
24
|
-
private_constant :BOUNDARY_START_LIMIT
|
|
25
|
-
|
|
26
|
-
MIME_HEADER_BYTESIZE_LIMIT = 64 * 1024
|
|
27
|
-
private_constant :MIME_HEADER_BYTESIZE_LIMIT
|
|
28
|
-
|
|
29
|
-
env_int = lambda do |key, val|
|
|
30
|
-
if str_val = ENV[key]
|
|
31
|
-
begin
|
|
32
|
-
val = Integer(str_val, 10)
|
|
33
|
-
rescue ArgumentError
|
|
34
|
-
raise ArgumentError, "non-integer value provided for environment variable #{key}"
|
|
35
|
-
end
|
|
36
|
-
end
|
|
37
|
-
|
|
38
|
-
val
|
|
39
|
-
end
|
|
40
|
-
|
|
41
|
-
BUFFERED_UPLOAD_BYTESIZE_LIMIT = env_int.call("RACK_MULTIPART_BUFFERED_UPLOAD_BYTESIZE_LIMIT", 16 * 1024 * 1024)
|
|
42
|
-
private_constant :BUFFERED_UPLOAD_BYTESIZE_LIMIT
|
|
43
|
-
|
|
44
|
-
bytesize_limit = env_int.call("RACK_MULTIPART_PARSER_BYTESIZE_LIMIT", 10 * 1024 * 1024 * 1024)
|
|
45
|
-
PARSER_BYTESIZE_LIMIT = bytesize_limit > 0 ? bytesize_limit : nil
|
|
46
|
-
private_constant :PARSER_BYTESIZE_LIMIT
|
|
47
|
-
|
|
48
51
|
class BoundedIO # :nodoc:
|
|
49
52
|
def initialize(io, content_length)
|
|
50
53
|
@io = io
|
|
@@ -66,16 +69,12 @@ module Rack
|
|
|
66
69
|
if str
|
|
67
70
|
@cursor += str.bytesize
|
|
68
71
|
else
|
|
69
|
-
# Raise an error for mismatching
|
|
72
|
+
# Raise an error for mismatching content-length and actual contents
|
|
70
73
|
raise EOFError, "bad content body"
|
|
71
74
|
end
|
|
72
75
|
|
|
73
76
|
str
|
|
74
77
|
end
|
|
75
|
-
|
|
76
|
-
def rewind
|
|
77
|
-
@io.rewind
|
|
78
|
-
end
|
|
79
78
|
end
|
|
80
79
|
|
|
81
80
|
MultipartInfo = Struct.new :params, :tmp_files
|
|
@@ -85,15 +84,7 @@ module Rack
|
|
|
85
84
|
return unless content_type
|
|
86
85
|
data = content_type.match(MULTIPART)
|
|
87
86
|
return unless data
|
|
88
|
-
|
|
89
|
-
unless data[1].empty?
|
|
90
|
-
raise EOFError, "whitespace between boundary parameter name and equal sign"
|
|
91
|
-
end
|
|
92
|
-
if data.post_match =~ /boundary\s*=/i
|
|
93
|
-
raise EOFError, "multiple boundary parameters found in multipart content type"
|
|
94
|
-
end
|
|
95
|
-
|
|
96
|
-
data[2]
|
|
87
|
+
data[1]
|
|
97
88
|
end
|
|
98
89
|
|
|
99
90
|
def self.parse(io, content_length, content_type, tmpfile, bufsize, qp)
|
|
@@ -102,22 +93,17 @@ module Rack
|
|
|
102
93
|
boundary = parse_boundary content_type
|
|
103
94
|
return EMPTY unless boundary
|
|
104
95
|
|
|
105
|
-
if
|
|
106
|
-
|
|
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)"
|
|
107
100
|
end
|
|
108
101
|
|
|
109
102
|
io = BoundedIO.new(io, content_length) if content_length
|
|
110
|
-
outbuf = String.new
|
|
111
103
|
|
|
112
104
|
parser = new(boundary, tmpfile, bufsize, qp)
|
|
113
|
-
parser.
|
|
114
|
-
|
|
115
|
-
loop do
|
|
116
|
-
break if parser.state == :DONE
|
|
117
|
-
parser.on_read io.read(bufsize, outbuf)
|
|
118
|
-
end
|
|
105
|
+
parser.parse(io)
|
|
119
106
|
|
|
120
|
-
io.rewind
|
|
121
107
|
parser.result
|
|
122
108
|
end
|
|
123
109
|
|
|
@@ -180,7 +166,7 @@ module Rack
|
|
|
180
166
|
|
|
181
167
|
@mime_parts[mime_index] = klass.new(body, head, filename, content_type, name)
|
|
182
168
|
|
|
183
|
-
|
|
169
|
+
check_open_files
|
|
184
170
|
end
|
|
185
171
|
|
|
186
172
|
def on_mime_body(mime_index, content)
|
|
@@ -192,23 +178,13 @@ module Rack
|
|
|
192
178
|
|
|
193
179
|
private
|
|
194
180
|
|
|
195
|
-
def
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
if file_limit && file_limit > 0
|
|
200
|
-
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
|
|
201
184
|
@mime_parts.each(&:close)
|
|
202
185
|
raise MultipartPartLimitError, 'Maximum file multiparts in content reached'
|
|
203
186
|
end
|
|
204
187
|
end
|
|
205
|
-
|
|
206
|
-
if part_limit && part_limit > 0
|
|
207
|
-
if @mime_parts.size >= part_limit
|
|
208
|
-
@mime_parts.each(&:close)
|
|
209
|
-
raise MultipartTotalPartLimitError, 'Maximum total multiparts in content reached'
|
|
210
|
-
end
|
|
211
|
-
end
|
|
212
188
|
end
|
|
213
189
|
end
|
|
214
190
|
|
|
@@ -217,42 +193,46 @@ module Rack
|
|
|
217
193
|
def initialize(boundary, tempfile, bufsize, query_parser)
|
|
218
194
|
@query_parser = query_parser
|
|
219
195
|
@params = query_parser.make_params
|
|
220
|
-
@boundary = "--#{boundary}"
|
|
221
196
|
@bufsize = bufsize
|
|
222
197
|
|
|
223
|
-
@full_boundary = @boundary
|
|
224
|
-
@end_boundary = @boundary + '--'
|
|
225
198
|
@state = :FAST_FORWARD
|
|
226
199
|
@mime_index = 0
|
|
227
|
-
@body_retained = nil
|
|
228
|
-
@retained_size = 0
|
|
229
|
-
@total_bytes_read = (0 if PARSER_BYTESIZE_LIMIT)
|
|
230
200
|
@collector = Collector.new tempfile
|
|
231
201
|
|
|
232
202
|
@sbuf = StringScanner.new("".dup)
|
|
233
|
-
@body_regex = /(?:#{EOL})
|
|
234
|
-
@
|
|
235
|
-
@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)
|
|
236
205
|
@head_regex = /(.*?#{EOL})#{EOL}/m
|
|
237
206
|
end
|
|
238
207
|
|
|
239
|
-
def
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
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
|
|
246
228
|
end
|
|
247
|
-
@sbuf.concat content
|
|
248
|
-
run_parser
|
|
249
229
|
end
|
|
250
230
|
|
|
251
231
|
def result
|
|
252
232
|
@collector.each do |part|
|
|
253
233
|
part.get_data do |data|
|
|
254
234
|
tag_multipart_encoding(part.filename, part.content_type, part.name, data)
|
|
255
|
-
@query_parser.normalize_params(@params, part.name, data
|
|
235
|
+
@query_parser.normalize_params(@params, part.name, data)
|
|
256
236
|
end
|
|
257
237
|
end
|
|
258
238
|
MultipartInfo.new @params.to_params_hash, @collector.find_all(&:file?).map(&:body)
|
|
@@ -260,40 +240,38 @@ module Rack
|
|
|
260
240
|
|
|
261
241
|
private
|
|
262
242
|
|
|
263
|
-
def
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
break if handle_fast_forward == :want_read
|
|
268
|
-
when :CONSUME_TOKEN
|
|
269
|
-
break if handle_consume_token == :want_read
|
|
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
|
|
276
|
-
end
|
|
277
|
-
end
|
|
243
|
+
def dequote(str) # From WEBrick::HTTPUtils
|
|
244
|
+
ret = (/\A"(.*)"\Z/ =~ str) ? $1 : str.dup
|
|
245
|
+
ret.gsub!(/\\(.)/, "\\1")
|
|
246
|
+
ret
|
|
278
247
|
end
|
|
279
248
|
|
|
280
|
-
def
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
@state = :DONE
|
|
286
|
-
elsif tok
|
|
287
|
-
@state = :MIME_HEAD
|
|
288
|
-
else
|
|
289
|
-
raise EOFError, "bad content body" if @sbuf.rest_size >= @bufsize
|
|
290
|
-
|
|
291
|
-
# We raise if we don't find the multipart boundary, to avoid unbounded memory
|
|
292
|
-
# buffering. Note that the actual limit is the higher of 16KB and the buffer size (1MB by default)
|
|
293
|
-
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
|
|
294
254
|
|
|
295
|
-
|
|
296
|
-
|
|
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
|
|
297
275
|
end
|
|
298
276
|
end
|
|
299
277
|
|
|
@@ -312,7 +290,7 @@ module Rack
|
|
|
312
290
|
head = @sbuf[1]
|
|
313
291
|
content_type = head[MULTIPART_CONTENT_TYPE, 1]
|
|
314
292
|
if name = head[MULTIPART_CONTENT_DISPOSITION, 1]
|
|
315
|
-
name =
|
|
293
|
+
name = dequote(name)
|
|
316
294
|
else
|
|
317
295
|
name = head[MULTIPART_CONTENT_ID, 1]
|
|
318
296
|
end
|
|
@@ -323,30 +301,16 @@ module Rack
|
|
|
323
301
|
name = filename || "#{content_type || TEXT_PLAIN}[]".dup
|
|
324
302
|
end
|
|
325
303
|
|
|
326
|
-
# Mime part head data is retained for both TempfilePart and BufferPart
|
|
327
|
-
# for the entireity of the parse, even though it isn't used for BufferPart.
|
|
328
|
-
update_retained_size(head.bytesize)
|
|
329
|
-
|
|
330
|
-
# If a filename is given, a TempfilePart will be used, so the body will
|
|
331
|
-
# not be buffered in memory. However, if a filename is not given, a BufferPart
|
|
332
|
-
# will be used, and the body will be buffered in memory.
|
|
333
|
-
@body_retained = !filename
|
|
334
|
-
|
|
335
304
|
@collector.on_mime_head @mime_index, head, filename, content_type, name
|
|
336
305
|
@state = :MIME_BODY
|
|
337
306
|
else
|
|
338
|
-
|
|
339
|
-
# buffering. Note that the actual limit is the higher of 64KB and the buffer size (1MB by default)
|
|
340
|
-
raise EOFError, "multipart mime part header too large" if @sbuf.rest.bytesize > MIME_HEADER_BYTESIZE_LIMIT
|
|
341
|
-
|
|
342
|
-
return :want_read
|
|
307
|
+
:want_read
|
|
343
308
|
end
|
|
344
309
|
end
|
|
345
310
|
|
|
346
311
|
def handle_mime_body
|
|
347
312
|
if (body_with_boundary = @sbuf.check_until(@body_regex)) # check but do not advance the pointer yet
|
|
348
313
|
body = body_with_boundary.sub(/#{@body_regex}\z/m, '') # remove the boundary from the string
|
|
349
|
-
update_retained_size(body.bytesize) if @body_retained
|
|
350
314
|
@collector.on_mime_body @mime_index, body
|
|
351
315
|
@sbuf.pos += body.length + 2 # skip \r\n after the content
|
|
352
316
|
@state = :CONSUME_TOKEN
|
|
@@ -355,9 +319,7 @@ module Rack
|
|
|
355
319
|
# Save what we have so far
|
|
356
320
|
if @rx_max_size < @sbuf.rest_size
|
|
357
321
|
delta = @sbuf.rest_size - @rx_max_size
|
|
358
|
-
|
|
359
|
-
update_retained_size(body.bytesize) if @body_retained
|
|
360
|
-
@collector.on_mime_body @mime_index, body
|
|
322
|
+
@collector.on_mime_body @mime_index, @sbuf.peek(delta)
|
|
361
323
|
@sbuf.pos += delta
|
|
362
324
|
@sbuf.string = @sbuf.rest
|
|
363
325
|
end
|
|
@@ -365,26 +327,16 @@ module Rack
|
|
|
365
327
|
end
|
|
366
328
|
end
|
|
367
329
|
|
|
368
|
-
def full_boundary; @full_boundary; end
|
|
369
|
-
|
|
370
|
-
def update_retained_size(size)
|
|
371
|
-
@retained_size += size
|
|
372
|
-
if @retained_size > BUFFERED_UPLOAD_BYTESIZE_LIMIT
|
|
373
|
-
raise EOFError, "multipart data over retained size limit"
|
|
374
|
-
end
|
|
375
|
-
end
|
|
376
|
-
|
|
377
330
|
# Scan until the we find the start or end of the boundary.
|
|
378
331
|
# If we find it, return the appropriate symbol for the start or
|
|
379
332
|
# end of the boundary. If we don't find the start or end of the
|
|
380
333
|
# boundary, clear the buffer and return nil.
|
|
381
334
|
def consume_boundary
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
return if @sbuf.eos?
|
|
335
|
+
if read_buffer = @sbuf.scan_until(@body_regex)
|
|
336
|
+
read_buffer.end_with?(EOL) ? :BOUNDARY : :END_BOUNDARY
|
|
337
|
+
else
|
|
338
|
+
@sbuf.terminate
|
|
339
|
+
nil
|
|
388
340
|
end
|
|
389
341
|
end
|
|
390
342
|
|
|
@@ -394,10 +346,10 @@ module Rack
|
|
|
394
346
|
when RFC2183
|
|
395
347
|
params = Hash[*head.scan(DISPPARM).flat_map(&:compact)]
|
|
396
348
|
|
|
397
|
-
if filename = params['filename']
|
|
398
|
-
filename = $1 if filename =~ /^"(.*)"$/
|
|
399
|
-
elsif filename = params['filename*']
|
|
349
|
+
if filename = params['filename*']
|
|
400
350
|
encoding, _, filename = filename.split("'", 3)
|
|
351
|
+
elsif filename = params['filename']
|
|
352
|
+
filename = $1 if filename =~ /^"(.*)"$/
|
|
401
353
|
end
|
|
402
354
|
when BROKEN
|
|
403
355
|
filename = $1
|
|
@@ -424,6 +376,7 @@ module Rack
|
|
|
424
376
|
end
|
|
425
377
|
|
|
426
378
|
CHARSET = "charset"
|
|
379
|
+
deprecate_constant :CHARSET
|
|
427
380
|
|
|
428
381
|
def tag_multipart_encoding(filename, content_type, name, body)
|
|
429
382
|
name = name.to_s
|
|
@@ -444,7 +397,13 @@ module Rack
|
|
|
444
397
|
k.strip!
|
|
445
398
|
v.strip!
|
|
446
399
|
v = v[1..-2] if v.start_with?('"') && v.end_with?('"')
|
|
447
|
-
|
|
400
|
+
if k == "charset"
|
|
401
|
+
encoding = begin
|
|
402
|
+
Encoding.find v
|
|
403
|
+
rescue ArgumentError
|
|
404
|
+
Encoding::BINARY
|
|
405
|
+
end
|
|
406
|
+
end
|
|
448
407
|
end
|
|
449
408
|
end
|
|
450
409
|
end
|
|
@@ -455,7 +414,7 @@ module Rack
|
|
|
455
414
|
|
|
456
415
|
def handle_empty_content!(content)
|
|
457
416
|
if content.nil? || content.empty?
|
|
458
|
-
raise
|
|
417
|
+
raise EmptyContentError
|
|
459
418
|
end
|
|
460
419
|
end
|
|
461
420
|
end
|