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