rack 2.2.19 → 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.
Potentially problematic release.
This version of rack might be problematic. Click here for more details.
- checksums.yaml +4 -4
- data/CHANGELOG.md +134 -154
- 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 +9 -3
- 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 +15 -17
- 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 +121 -139
- 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 -89
- data/lib/rack/recursive.rb +2 -0
- data/lib/rack/reloader.rb +0 -2
- data/lib/rack/request.rb +189 -91
- 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 +31 -26
- data/lib/rack/show_exceptions.rb +15 -2
- data/lib/rack/show_status.rb +17 -7
- data/lib/rack/static.rb +9 -10
- data/lib/rack/tempfile_reaper.rb +15 -4
- data/lib/rack/urlmap.rb +4 -2
- data/lib/rack/utils.rb +212 -202
- 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,45 +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
51
|
class BoundedIO # :nodoc:
|
45
52
|
def initialize(io, content_length)
|
46
53
|
@io = io
|
@@ -62,16 +69,12 @@ module Rack
|
|
62
69
|
if str
|
63
70
|
@cursor += str.bytesize
|
64
71
|
else
|
65
|
-
# Raise an error for mismatching
|
72
|
+
# Raise an error for mismatching content-length and actual contents
|
66
73
|
raise EOFError, "bad content body"
|
67
74
|
end
|
68
75
|
|
69
76
|
str
|
70
77
|
end
|
71
|
-
|
72
|
-
def rewind
|
73
|
-
@io.rewind
|
74
|
-
end
|
75
78
|
end
|
76
79
|
|
77
80
|
MultipartInfo = Struct.new :params, :tmp_files
|
@@ -90,18 +93,17 @@ module Rack
|
|
90
93
|
boundary = parse_boundary content_type
|
91
94
|
return EMPTY unless boundary
|
92
95
|
|
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)"
|
100
|
+
end
|
101
|
+
|
93
102
|
io = BoundedIO.new(io, content_length) if content_length
|
94
|
-
outbuf = String.new
|
95
103
|
|
96
104
|
parser = new(boundary, tmpfile, bufsize, qp)
|
97
|
-
parser.
|
98
|
-
|
99
|
-
loop do
|
100
|
-
break if parser.state == :DONE
|
101
|
-
parser.on_read io.read(bufsize, outbuf)
|
102
|
-
end
|
105
|
+
parser.parse(io)
|
103
106
|
|
104
|
-
io.rewind
|
105
107
|
parser.result
|
106
108
|
end
|
107
109
|
|
@@ -164,7 +166,7 @@ module Rack
|
|
164
166
|
|
165
167
|
@mime_parts[mime_index] = klass.new(body, head, filename, content_type, name)
|
166
168
|
|
167
|
-
|
169
|
+
check_open_files
|
168
170
|
end
|
169
171
|
|
170
172
|
def on_mime_body(mime_index, content)
|
@@ -176,23 +178,13 @@ module Rack
|
|
176
178
|
|
177
179
|
private
|
178
180
|
|
179
|
-
def
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
if file_limit && file_limit > 0
|
184
|
-
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
|
185
184
|
@mime_parts.each(&:close)
|
186
185
|
raise MultipartPartLimitError, 'Maximum file multiparts in content reached'
|
187
186
|
end
|
188
187
|
end
|
189
|
-
|
190
|
-
if part_limit && part_limit > 0
|
191
|
-
if @mime_parts.size >= part_limit
|
192
|
-
@mime_parts.each(&:close)
|
193
|
-
raise MultipartTotalPartLimitError, 'Maximum total multiparts in content reached'
|
194
|
-
end
|
195
|
-
end
|
196
188
|
end
|
197
189
|
end
|
198
190
|
|
@@ -201,35 +193,46 @@ module Rack
|
|
201
193
|
def initialize(boundary, tempfile, bufsize, query_parser)
|
202
194
|
@query_parser = query_parser
|
203
195
|
@params = query_parser.make_params
|
204
|
-
@boundary = "--#{boundary}"
|
205
196
|
@bufsize = bufsize
|
206
197
|
|
207
|
-
@full_boundary = @boundary
|
208
|
-
@end_boundary = @boundary + '--'
|
209
198
|
@state = :FAST_FORWARD
|
210
199
|
@mime_index = 0
|
211
|
-
@body_retained = nil
|
212
|
-
@retained_size = 0
|
213
200
|
@collector = Collector.new tempfile
|
214
201
|
|
215
202
|
@sbuf = StringScanner.new("".dup)
|
216
|
-
@body_regex = /(?:#{EOL})
|
217
|
-
@
|
218
|
-
@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)
|
219
205
|
@head_regex = /(.*?#{EOL})#{EOL}/m
|
220
206
|
end
|
221
207
|
|
222
|
-
def
|
223
|
-
|
224
|
-
|
225
|
-
|
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
|
228
|
+
end
|
226
229
|
end
|
227
230
|
|
228
231
|
def result
|
229
232
|
@collector.each do |part|
|
230
233
|
part.get_data do |data|
|
231
234
|
tag_multipart_encoding(part.filename, part.content_type, part.name, data)
|
232
|
-
@query_parser.normalize_params(@params, part.name, data
|
235
|
+
@query_parser.normalize_params(@params, part.name, data)
|
233
236
|
end
|
234
237
|
end
|
235
238
|
MultipartInfo.new @params.to_params_hash, @collector.find_all(&:file?).map(&:body)
|
@@ -237,40 +240,38 @@ module Rack
|
|
237
240
|
|
238
241
|
private
|
239
242
|
|
240
|
-
def
|
241
|
-
|
242
|
-
|
243
|
-
|
244
|
-
break if handle_fast_forward == :want_read
|
245
|
-
when :CONSUME_TOKEN
|
246
|
-
break if handle_consume_token == :want_read
|
247
|
-
when :MIME_HEAD
|
248
|
-
break if handle_mime_head == :want_read
|
249
|
-
when :MIME_BODY
|
250
|
-
break if handle_mime_body == :want_read
|
251
|
-
when :DONE
|
252
|
-
break
|
253
|
-
end
|
254
|
-
end
|
243
|
+
def dequote(str) # From WEBrick::HTTPUtils
|
244
|
+
ret = (/\A"(.*)"\Z/ =~ str) ? $1 : str.dup
|
245
|
+
ret.gsub!(/\\(.)/, "\\1")
|
246
|
+
ret
|
255
247
|
end
|
256
248
|
|
257
|
-
def
|
258
|
-
|
259
|
-
|
260
|
-
|
261
|
-
|
262
|
-
@state = :DONE
|
263
|
-
elsif tok
|
264
|
-
@state = :MIME_HEAD
|
265
|
-
else
|
266
|
-
raise EOFError, "bad content body" if @sbuf.rest_size >= @bufsize
|
267
|
-
|
268
|
-
# We raise if we don't find the multipart boundary, to avoid unbounded memory
|
269
|
-
# buffering. Note that the actual limit is the higher of 16KB and the buffer size (1MB by default)
|
270
|
-
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
|
271
254
|
|
272
|
-
|
273
|
-
|
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
|
274
275
|
end
|
275
276
|
end
|
276
277
|
|
@@ -289,7 +290,7 @@ module Rack
|
|
289
290
|
head = @sbuf[1]
|
290
291
|
content_type = head[MULTIPART_CONTENT_TYPE, 1]
|
291
292
|
if name = head[MULTIPART_CONTENT_DISPOSITION, 1]
|
292
|
-
name =
|
293
|
+
name = dequote(name)
|
293
294
|
else
|
294
295
|
name = head[MULTIPART_CONTENT_ID, 1]
|
295
296
|
end
|
@@ -300,30 +301,16 @@ module Rack
|
|
300
301
|
name = filename || "#{content_type || TEXT_PLAIN}[]".dup
|
301
302
|
end
|
302
303
|
|
303
|
-
# Mime part head data is retained for both TempfilePart and BufferPart
|
304
|
-
# for the entireity of the parse, even though it isn't used for BufferPart.
|
305
|
-
update_retained_size(head.bytesize)
|
306
|
-
|
307
|
-
# If a filename is given, a TempfilePart will be used, so the body will
|
308
|
-
# not be buffered in memory. However, if a filename is not given, a BufferPart
|
309
|
-
# will be used, and the body will be buffered in memory.
|
310
|
-
@body_retained = !filename
|
311
|
-
|
312
304
|
@collector.on_mime_head @mime_index, head, filename, content_type, name
|
313
305
|
@state = :MIME_BODY
|
314
306
|
else
|
315
|
-
|
316
|
-
# buffering. Note that the actual limit is the higher of 64KB and the buffer size (1MB by default)
|
317
|
-
raise EOFError, "multipart mime part header too large" if @sbuf.string.bytesize > MIME_HEADER_BYTESIZE_LIMIT
|
318
|
-
|
319
|
-
return :want_read
|
307
|
+
:want_read
|
320
308
|
end
|
321
309
|
end
|
322
310
|
|
323
311
|
def handle_mime_body
|
324
312
|
if (body_with_boundary = @sbuf.check_until(@body_regex)) # check but do not advance the pointer yet
|
325
313
|
body = body_with_boundary.sub(/#{@body_regex}\z/m, '') # remove the boundary from the string
|
326
|
-
update_retained_size(body.bytesize) if @body_retained
|
327
314
|
@collector.on_mime_body @mime_index, body
|
328
315
|
@sbuf.pos += body.length + 2 # skip \r\n after the content
|
329
316
|
@state = :CONSUME_TOKEN
|
@@ -332,9 +319,7 @@ module Rack
|
|
332
319
|
# Save what we have so far
|
333
320
|
if @rx_max_size < @sbuf.rest_size
|
334
321
|
delta = @sbuf.rest_size - @rx_max_size
|
335
|
-
|
336
|
-
update_retained_size(body.bytesize) if @body_retained
|
337
|
-
@collector.on_mime_body @mime_index, body
|
322
|
+
@collector.on_mime_body @mime_index, @sbuf.peek(delta)
|
338
323
|
@sbuf.pos += delta
|
339
324
|
@sbuf.string = @sbuf.rest
|
340
325
|
end
|
@@ -342,26 +327,16 @@ module Rack
|
|
342
327
|
end
|
343
328
|
end
|
344
329
|
|
345
|
-
def full_boundary; @full_boundary; end
|
346
|
-
|
347
|
-
def update_retained_size(size)
|
348
|
-
@retained_size += size
|
349
|
-
if @retained_size > BUFFERED_UPLOAD_BYTESIZE_LIMIT
|
350
|
-
raise EOFError, "multipart data over retained size limit"
|
351
|
-
end
|
352
|
-
end
|
353
|
-
|
354
330
|
# Scan until the we find the start or end of the boundary.
|
355
331
|
# If we find it, return the appropriate symbol for the start or
|
356
332
|
# end of the boundary. If we don't find the start or end of the
|
357
333
|
# boundary, clear the buffer and return nil.
|
358
334
|
def consume_boundary
|
359
|
-
|
360
|
-
|
361
|
-
|
362
|
-
|
363
|
-
|
364
|
-
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
|
365
340
|
end
|
366
341
|
end
|
367
342
|
|
@@ -371,10 +346,10 @@ module Rack
|
|
371
346
|
when RFC2183
|
372
347
|
params = Hash[*head.scan(DISPPARM).flat_map(&:compact)]
|
373
348
|
|
374
|
-
if filename = params['filename']
|
375
|
-
filename = $1 if filename =~ /^"(.*)"$/
|
376
|
-
elsif filename = params['filename*']
|
349
|
+
if filename = params['filename*']
|
377
350
|
encoding, _, filename = filename.split("'", 3)
|
351
|
+
elsif filename = params['filename']
|
352
|
+
filename = $1 if filename =~ /^"(.*)"$/
|
378
353
|
end
|
379
354
|
when BROKEN
|
380
355
|
filename = $1
|
@@ -401,6 +376,7 @@ module Rack
|
|
401
376
|
end
|
402
377
|
|
403
378
|
CHARSET = "charset"
|
379
|
+
deprecate_constant :CHARSET
|
404
380
|
|
405
381
|
def tag_multipart_encoding(filename, content_type, name, body)
|
406
382
|
name = name.to_s
|
@@ -421,7 +397,13 @@ module Rack
|
|
421
397
|
k.strip!
|
422
398
|
v.strip!
|
423
399
|
v = v[1..-2] if v.start_with?('"') && v.end_with?('"')
|
424
|
-
|
400
|
+
if k == "charset"
|
401
|
+
encoding = begin
|
402
|
+
Encoding.find v
|
403
|
+
rescue ArgumentError
|
404
|
+
Encoding::BINARY
|
405
|
+
end
|
406
|
+
end
|
425
407
|
end
|
426
408
|
end
|
427
409
|
end
|
@@ -432,7 +414,7 @@ module Rack
|
|
432
414
|
|
433
415
|
def handle_empty_content!(content)
|
434
416
|
if content.nil? || content.empty?
|
435
|
-
raise
|
417
|
+
raise EmptyContentError
|
436
418
|
end
|
437
419
|
end
|
438
420
|
end
|
data/lib/rack/multipart.rb
CHANGED
@@ -1,64 +1,44 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require_relative 'constants'
|
4
|
+
require_relative 'utils'
|
5
|
+
|
3
6
|
require_relative 'multipart/parser'
|
7
|
+
require_relative 'multipart/generator'
|
4
8
|
|
5
9
|
module Rack
|
6
10
|
# A multipart form data parser, adapted from IOWA.
|
7
11
|
#
|
8
12
|
# Usually, Rack::Request#POST takes care of calling this.
|
9
13
|
module Multipart
|
10
|
-
autoload :UploadedFile, 'rack/multipart/uploaded_file'
|
11
|
-
autoload :Generator, 'rack/multipart/generator'
|
12
|
-
|
13
|
-
EOL = "\r\n"
|
14
14
|
MULTIPART_BOUNDARY = "AaB03x"
|
15
|
-
MULTIPART = %r|\Amultipart/.*boundary=\"?([^\";,]+)\"?|ni
|
16
|
-
TOKEN = /[^\s()<>,;:\\"\/\[\]?=]+/
|
17
|
-
CONDISP = /Content-Disposition:\s*#{TOKEN}\s*/i
|
18
|
-
VALUE = /"(?:\\"|[^"])*"|#{TOKEN}/
|
19
|
-
BROKEN = /^#{CONDISP}.*;\s*filename=(#{VALUE})/i
|
20
|
-
MULTIPART_CONTENT_TYPE = /Content-Type: (.*)#{EOL}/ni
|
21
|
-
MULTIPART_CONTENT_DISPOSITION = /Content-Disposition:[^:]*;\s*name=(#{VALUE})/ni
|
22
|
-
MULTIPART_CONTENT_ID = /Content-ID:\s*([^#{EOL}]*)/ni
|
23
|
-
# Updated definitions from RFC 2231
|
24
|
-
ATTRIBUTE_CHAR = %r{[^ \x00-\x1f\x7f)(><@,;:\\"/\[\]?='*%]}
|
25
|
-
ATTRIBUTE = /#{ATTRIBUTE_CHAR}+/
|
26
|
-
SECTION = /\*[0-9]+/
|
27
|
-
REGULAR_PARAMETER_NAME = /#{ATTRIBUTE}#{SECTION}?/
|
28
|
-
REGULAR_PARAMETER = /(#{REGULAR_PARAMETER_NAME})=(#{VALUE})/
|
29
|
-
EXTENDED_OTHER_NAME = /#{ATTRIBUTE}\*[1-9][0-9]*\*/
|
30
|
-
EXTENDED_OTHER_VALUE = /%[0-9a-fA-F]{2}|#{ATTRIBUTE_CHAR}/
|
31
|
-
EXTENDED_OTHER_PARAMETER = /(#{EXTENDED_OTHER_NAME})=(#{EXTENDED_OTHER_VALUE}*)/
|
32
|
-
EXTENDED_INITIAL_NAME = /#{ATTRIBUTE}(?:\*0)?\*/
|
33
|
-
EXTENDED_INITIAL_VALUE = /[a-zA-Z0-9\-]*'[a-zA-Z0-9\-]*'#{EXTENDED_OTHER_VALUE}*/
|
34
|
-
EXTENDED_INITIAL_PARAMETER = /(#{EXTENDED_INITIAL_NAME})=(#{EXTENDED_INITIAL_VALUE})/
|
35
|
-
EXTENDED_PARAMETER = /#{EXTENDED_INITIAL_PARAMETER}|#{EXTENDED_OTHER_PARAMETER}/
|
36
|
-
DISPPARM = /;\s*(?:#{REGULAR_PARAMETER}|#{EXTENDED_PARAMETER})\s*/
|
37
|
-
RFC2183 = /^#{CONDISP}(#{DISPPARM})+$/i
|
38
15
|
|
39
16
|
class << self
|
40
17
|
def parse_multipart(env, params = Rack::Utils.default_query_parser)
|
41
|
-
|
42
|
-
|
18
|
+
io = env[RACK_INPUT]
|
19
|
+
|
20
|
+
if content_length = env['CONTENT_LENGTH']
|
21
|
+
content_length = content_length.to_i
|
22
|
+
end
|
43
23
|
|
44
|
-
|
45
|
-
io = req.get_header(RACK_INPUT)
|
46
|
-
io.rewind
|
47
|
-
content_length = req.content_length
|
48
|
-
content_length = content_length.to_i if content_length
|
24
|
+
content_type = env['CONTENT_TYPE']
|
49
25
|
|
50
|
-
tempfile =
|
51
|
-
bufsize =
|
26
|
+
tempfile = env[RACK_MULTIPART_TEMPFILE_FACTORY] || Parser::TEMPFILE_FACTORY
|
27
|
+
bufsize = env[RACK_MULTIPART_BUFFER_SIZE] || Parser::BUFSIZE
|
52
28
|
|
53
|
-
info = Parser.parse
|
54
|
-
|
55
|
-
|
29
|
+
info = Parser.parse(io, content_length, content_type, tempfile, bufsize, params)
|
30
|
+
env[RACK_TEMPFILES] = info.tmp_files
|
31
|
+
|
32
|
+
return info.params
|
33
|
+
end
|
34
|
+
|
35
|
+
def extract_multipart(request, params = Rack::Utils.default_query_parser)
|
36
|
+
parse_multipart(request.env)
|
56
37
|
end
|
57
38
|
|
58
39
|
def build_multipart(params, first = true)
|
59
40
|
Generator.new(params, first).dump
|
60
41
|
end
|
61
42
|
end
|
62
|
-
|
63
43
|
end
|
64
44
|
end
|