rack 2.2.21 → 3.1.19
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 +459 -73
- data/CONTRIBUTING.md +63 -55
- data/MIT-LICENSE +1 -1
- data/README.md +363 -0
- data/SPEC.rdoc +204 -131
- data/lib/rack/auth/abstract/handler.rb +3 -1
- data/lib/rack/auth/abstract/request.rb +3 -1
- data/lib/rack/auth/basic.rb +1 -3
- data/lib/rack/bad_request.rb +8 -0
- data/lib/rack/body_proxy.rb +21 -3
- data/lib/rack/builder.rb +102 -69
- data/lib/rack/cascade.rb +2 -3
- data/lib/rack/common_logger.rb +22 -17
- data/lib/rack/conditional_get.rb +18 -15
- data/lib/rack/constants.rb +67 -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 +17 -23
- data/lib/rack/events.rb +4 -0
- data/lib/rack/files.rb +15 -17
- data/lib/rack/head.rb +9 -8
- data/lib/rack/headers.rb +238 -0
- data/lib/rack/lint.rb +840 -644
- data/lib/rack/lock.rb +2 -5
- data/lib/rack/logger.rb +3 -0
- data/lib/rack/method_override.rb +5 -1
- data/lib/rack/mime.rb +14 -5
- data/lib/rack/mock.rb +1 -300
- data/lib/rack/mock_request.rb +161 -0
- data/lib/rack/mock_response.rb +153 -0
- data/lib/rack/multipart/generator.rb +7 -5
- data/lib/rack/multipart/parser.rb +216 -102
- data/lib/rack/multipart/uploaded_file.rb +4 -0
- data/lib/rack/multipart.rb +53 -40
- data/lib/rack/null_logger.rb +9 -0
- data/lib/rack/query_parser.rb +80 -102
- data/lib/rack/recursive.rb +2 -0
- data/lib/rack/reloader.rb +0 -2
- data/lib/rack/request.rb +263 -126
- data/lib/rack/response.rb +151 -66
- data/lib/rack/rewindable_input.rb +24 -5
- data/lib/rack/runtime.rb +7 -6
- data/lib/rack/sendfile.rb +35 -30
- data/lib/rack/show_exceptions.rb +21 -4
- data/lib/rack/show_status.rb +17 -7
- data/lib/rack/static.rb +8 -8
- data/lib/rack/tempfile_reaper.rb +15 -4
- data/lib/rack/urlmap.rb +3 -1
- data/lib/rack/utils.rb +236 -237
- data/lib/rack/version.rb +1 -9
- data/lib/rack.rb +13 -89
- metadata +13 -39
- data/README.rdoc +0 -355
- data/Rakefile +0 -130
- 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/contrib/rdoc.css +0 -412
- data/example/lobster.ru +0 -6
- data/example/protectedlobster.rb +0 -16
- data/example/protectedlobster.ru +0 -10
- data/lib/rack/auth/digest/md5.rb +0 -131
- data/lib/rack/auth/digest/nonce.rb +0 -53
- data/lib/rack/auth/digest/params.rb +0 -54
- data/lib/rack/auth/digest/request.rb +0 -43
- data/lib/rack/chunked.rb +0 -117
- data/lib/rack/core_ext/regexp.rb +0 -14
- data/lib/rack/file.rb +0 -7
- 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
- data/rack.gemspec +0 -46
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'time'
|
|
4
|
+
|
|
5
|
+
require_relative 'response'
|
|
6
|
+
|
|
7
|
+
module Rack
|
|
8
|
+
# Rack::MockResponse provides useful helpers for testing your apps.
|
|
9
|
+
# Usually, you don't create the MockResponse on your own, but use
|
|
10
|
+
# MockRequest.
|
|
11
|
+
|
|
12
|
+
class MockResponse < Rack::Response
|
|
13
|
+
begin
|
|
14
|
+
# Recent versions of the CGI gem may not provide `CGI::Cookie`.
|
|
15
|
+
require 'cgi/cookie'
|
|
16
|
+
Cookie = CGI::Cookie
|
|
17
|
+
rescue LoadError
|
|
18
|
+
class Cookie
|
|
19
|
+
attr_reader :name, :value, :path, :domain, :expires, :secure
|
|
20
|
+
|
|
21
|
+
def initialize(args)
|
|
22
|
+
@name = args["name"]
|
|
23
|
+
@value = args["value"]
|
|
24
|
+
@path = args["path"]
|
|
25
|
+
@domain = args["domain"]
|
|
26
|
+
@expires = args["expires"]
|
|
27
|
+
@secure = args["secure"]
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def method_missing(method_name, *args, &block)
|
|
31
|
+
@value.send(method_name, *args, &block)
|
|
32
|
+
end
|
|
33
|
+
# :nocov:
|
|
34
|
+
ruby2_keywords(:method_missing) if respond_to?(:ruby2_keywords, true)
|
|
35
|
+
# :nocov:
|
|
36
|
+
|
|
37
|
+
def respond_to_missing?(method_name, include_all = false)
|
|
38
|
+
@value.respond_to?(method_name, include_all) || super
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
class << self
|
|
44
|
+
alias [] new
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Headers
|
|
48
|
+
attr_reader :original_headers, :cookies
|
|
49
|
+
|
|
50
|
+
# Errors
|
|
51
|
+
attr_accessor :errors
|
|
52
|
+
|
|
53
|
+
def initialize(status, headers, body, errors = nil)
|
|
54
|
+
@original_headers = headers
|
|
55
|
+
|
|
56
|
+
if errors
|
|
57
|
+
@errors = errors.string if errors.respond_to?(:string)
|
|
58
|
+
else
|
|
59
|
+
@errors = ""
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
super(body, status, headers)
|
|
63
|
+
|
|
64
|
+
@cookies = parse_cookies_from_header
|
|
65
|
+
buffered_body!
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def =~(other)
|
|
69
|
+
body =~ other
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def match(other)
|
|
73
|
+
body.match other
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def body
|
|
77
|
+
return @buffered_body if defined?(@buffered_body)
|
|
78
|
+
|
|
79
|
+
# FIXME: apparently users of MockResponse expect the return value of
|
|
80
|
+
# MockResponse#body to be a string. However, the real response object
|
|
81
|
+
# returns the body as a list.
|
|
82
|
+
#
|
|
83
|
+
# See spec_showstatus.rb:
|
|
84
|
+
#
|
|
85
|
+
# should "not replace existing messages" do
|
|
86
|
+
# ...
|
|
87
|
+
# res.body.should == "foo!"
|
|
88
|
+
# end
|
|
89
|
+
buffer = @buffered_body = String.new
|
|
90
|
+
|
|
91
|
+
@body.each do |chunk|
|
|
92
|
+
buffer << chunk
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
return buffer
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def empty?
|
|
99
|
+
[201, 204, 304].include? status
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def cookie(name)
|
|
103
|
+
cookies.fetch(name, nil)
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
private
|
|
107
|
+
|
|
108
|
+
def parse_cookies_from_header
|
|
109
|
+
cookies = Hash.new
|
|
110
|
+
set_cookie_header = headers['set-cookie']
|
|
111
|
+
if set_cookie_header && !set_cookie_header.empty?
|
|
112
|
+
Array(set_cookie_header).each do |cookie|
|
|
113
|
+
cookie_name, cookie_filling = cookie.split('=', 2)
|
|
114
|
+
cookie_attributes = identify_cookie_attributes cookie_filling
|
|
115
|
+
parsed_cookie = Cookie.new(
|
|
116
|
+
'name' => cookie_name.strip,
|
|
117
|
+
'value' => cookie_attributes.fetch('value'),
|
|
118
|
+
'path' => cookie_attributes.fetch('path', nil),
|
|
119
|
+
'domain' => cookie_attributes.fetch('domain', nil),
|
|
120
|
+
'expires' => cookie_attributes.fetch('expires', nil),
|
|
121
|
+
'secure' => cookie_attributes.fetch('secure', false)
|
|
122
|
+
)
|
|
123
|
+
cookies.store(cookie_name, parsed_cookie)
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
cookies
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def identify_cookie_attributes(cookie_filling)
|
|
130
|
+
cookie_bits = cookie_filling.split(';')
|
|
131
|
+
cookie_attributes = Hash.new
|
|
132
|
+
cookie_attributes.store('value', Array(cookie_bits[0].strip))
|
|
133
|
+
cookie_bits.drop(1).each do |bit|
|
|
134
|
+
if bit.include? '='
|
|
135
|
+
cookie_attribute, attribute_value = bit.split('=', 2)
|
|
136
|
+
cookie_attributes.store(cookie_attribute.strip.downcase, attribute_value.strip)
|
|
137
|
+
end
|
|
138
|
+
if bit.include? 'secure'
|
|
139
|
+
cookie_attributes.store('secure', true)
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
if cookie_attributes.key? 'max-age'
|
|
144
|
+
cookie_attributes.store('expires', Time.now + cookie_attributes['max-age'].to_i)
|
|
145
|
+
elsif cookie_attributes.key? 'expires'
|
|
146
|
+
cookie_attributes.store('expires', Time.httpdate(cookie_attributes['expires']))
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
cookie_attributes
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
end
|
|
153
|
+
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,14 +2,43 @@
|
|
|
2
2
|
|
|
3
3
|
require 'strscan'
|
|
4
4
|
|
|
5
|
+
require_relative '../utils'
|
|
6
|
+
require_relative '../bad_request'
|
|
7
|
+
|
|
5
8
|
module Rack
|
|
6
9
|
module Multipart
|
|
7
|
-
class MultipartPartLimitError < Errno::EMFILE
|
|
8
|
-
|
|
10
|
+
class MultipartPartLimitError < Errno::EMFILE
|
|
11
|
+
include BadRequest
|
|
12
|
+
end
|
|
9
13
|
|
|
10
|
-
class
|
|
11
|
-
|
|
14
|
+
class MultipartTotalPartLimitError < StandardError
|
|
15
|
+
include BadRequest
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# Use specific error class when parsing multipart request
|
|
19
|
+
# that ends early.
|
|
20
|
+
class EmptyContentError < ::EOFError
|
|
21
|
+
include BadRequest
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Base class for multipart exceptions that do not subclass from
|
|
25
|
+
# other exception classes for backwards compatibility.
|
|
26
|
+
class BoundaryTooLongError < StandardError
|
|
27
|
+
include BadRequest
|
|
28
|
+
end
|
|
12
29
|
|
|
30
|
+
# Prefer to use the BoundaryTooLongError class or Rack::BadRequest.
|
|
31
|
+
Error = BoundaryTooLongError
|
|
32
|
+
|
|
33
|
+
EOL = "\r\n"
|
|
34
|
+
FWS = /[ \t]+(?:\r\n[ \t]+)?/ # whitespace with optional folding
|
|
35
|
+
HEADER_VALUE = "(?:[^\r\n]|\r\n[ \t])*" # anything but a non-folding CRLF
|
|
36
|
+
MULTIPART = %r|\Amultipart/.*boundary=\"?([^\";,]+)\"?|ni
|
|
37
|
+
MULTIPART_CONTENT_TYPE = /^Content-Type:#{FWS}?(#{HEADER_VALUE})/ni
|
|
38
|
+
MULTIPART_CONTENT_DISPOSITION = /^Content-Disposition:#{FWS}?(#{HEADER_VALUE})/ni
|
|
39
|
+
MULTIPART_CONTENT_ID = /^Content-ID:#{FWS}?(#{HEADER_VALUE})/ni
|
|
40
|
+
|
|
41
|
+
class Parser
|
|
13
42
|
BUFSIZE = 1_048_576
|
|
14
43
|
TEXT_PLAIN = "text/plain"
|
|
15
44
|
TEMPFILE_FACTORY = lambda { |filename, content_type|
|
|
@@ -18,8 +47,6 @@ module Rack
|
|
|
18
47
|
Tempfile.new(["RackMultipart", extension])
|
|
19
48
|
}
|
|
20
49
|
|
|
21
|
-
BOUNDARY_REGEX = /\A([^\n]*(?:\n|\Z))/
|
|
22
|
-
|
|
23
50
|
BOUNDARY_START_LIMIT = 16 * 1024
|
|
24
51
|
private_constant :BOUNDARY_START_LIMIT
|
|
25
52
|
|
|
@@ -62,16 +89,12 @@ module Rack
|
|
|
62
89
|
if str
|
|
63
90
|
@cursor += str.bytesize
|
|
64
91
|
else
|
|
65
|
-
# Raise an error for mismatching
|
|
92
|
+
# Raise an error for mismatching content-length and actual contents
|
|
66
93
|
raise EOFError, "bad content body"
|
|
67
94
|
end
|
|
68
95
|
|
|
69
96
|
str
|
|
70
97
|
end
|
|
71
|
-
|
|
72
|
-
def rewind
|
|
73
|
-
@io.rewind
|
|
74
|
-
end
|
|
75
98
|
end
|
|
76
99
|
|
|
77
100
|
MultipartInfo = Struct.new :params, :tmp_files
|
|
@@ -90,18 +113,17 @@ module Rack
|
|
|
90
113
|
boundary = parse_boundary content_type
|
|
91
114
|
return EMPTY unless boundary
|
|
92
115
|
|
|
116
|
+
if boundary.length > 70
|
|
117
|
+
# RFC 1521 Section 7.2.1 imposes a 70 character maximum for the boundary.
|
|
118
|
+
# Most clients use no more than 55 characters.
|
|
119
|
+
raise BoundaryTooLongError, "multipart boundary size too large (#{boundary.length} characters)"
|
|
120
|
+
end
|
|
121
|
+
|
|
93
122
|
io = BoundedIO.new(io, content_length) if content_length
|
|
94
|
-
outbuf = String.new
|
|
95
123
|
|
|
96
124
|
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
|
|
125
|
+
parser.parse(io)
|
|
103
126
|
|
|
104
|
-
io.rewind
|
|
105
127
|
parser.result
|
|
106
128
|
end
|
|
107
129
|
|
|
@@ -201,11 +223,8 @@ module Rack
|
|
|
201
223
|
def initialize(boundary, tempfile, bufsize, query_parser)
|
|
202
224
|
@query_parser = query_parser
|
|
203
225
|
@params = query_parser.make_params
|
|
204
|
-
@boundary = "--#{boundary}"
|
|
205
226
|
@bufsize = bufsize
|
|
206
227
|
|
|
207
|
-
@full_boundary = @boundary
|
|
208
|
-
@end_boundary = @boundary + '--'
|
|
209
228
|
@state = :FAST_FORWARD
|
|
210
229
|
@mime_index = 0
|
|
211
230
|
@body_retained = nil
|
|
@@ -213,23 +232,41 @@ module Rack
|
|
|
213
232
|
@collector = Collector.new tempfile
|
|
214
233
|
|
|
215
234
|
@sbuf = StringScanner.new("".dup)
|
|
216
|
-
@body_regex = /(?:#{EOL})
|
|
217
|
-
@
|
|
218
|
-
@
|
|
235
|
+
@body_regex = /(?:#{EOL}|\A)--#{Regexp.quote(boundary)}(?:#{EOL}|--)/m
|
|
236
|
+
@body_regex_at_end = /#{@body_regex}\z/m
|
|
237
|
+
@end_boundary_size = boundary.bytesize + 4 # (-- at start, -- at finish)
|
|
238
|
+
@rx_max_size = boundary.bytesize + 6 # (\r\n-- at start, either \r\n or -- at finish)
|
|
219
239
|
@head_regex = /(.*?#{EOL})#{EOL}/m
|
|
220
240
|
end
|
|
221
241
|
|
|
222
|
-
def
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
242
|
+
def parse(io)
|
|
243
|
+
outbuf = String.new
|
|
244
|
+
read_data(io, outbuf)
|
|
245
|
+
|
|
246
|
+
loop do
|
|
247
|
+
status =
|
|
248
|
+
case @state
|
|
249
|
+
when :FAST_FORWARD
|
|
250
|
+
handle_fast_forward
|
|
251
|
+
when :CONSUME_TOKEN
|
|
252
|
+
handle_consume_token
|
|
253
|
+
when :MIME_HEAD
|
|
254
|
+
handle_mime_head
|
|
255
|
+
when :MIME_BODY
|
|
256
|
+
handle_mime_body
|
|
257
|
+
else # when :DONE
|
|
258
|
+
return
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
read_data(io, outbuf) if status == :want_read
|
|
262
|
+
end
|
|
226
263
|
end
|
|
227
264
|
|
|
228
265
|
def result
|
|
229
266
|
@collector.each do |part|
|
|
230
267
|
part.get_data do |data|
|
|
231
268
|
tag_multipart_encoding(part.filename, part.content_type, part.name, data)
|
|
232
|
-
@query_parser.normalize_params(@params, part.name, data
|
|
269
|
+
@query_parser.normalize_params(@params, part.name, data)
|
|
233
270
|
end
|
|
234
271
|
end
|
|
235
272
|
MultipartInfo.new @params.to_params_hash, @collector.find_all(&:file?).map(&:body)
|
|
@@ -237,40 +274,49 @@ module Rack
|
|
|
237
274
|
|
|
238
275
|
private
|
|
239
276
|
|
|
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
|
|
277
|
+
def dequote(str) # From WEBrick::HTTPUtils
|
|
278
|
+
ret = (/\A"(.*)"\Z/ =~ str) ? $1 : str.dup
|
|
279
|
+
ret.gsub!(/\\(.)/, "\\1")
|
|
280
|
+
ret
|
|
255
281
|
end
|
|
256
282
|
|
|
257
|
-
def
|
|
258
|
-
|
|
283
|
+
def read_data(io, outbuf)
|
|
284
|
+
content = io.read(@bufsize, outbuf)
|
|
285
|
+
handle_empty_content!(content)
|
|
286
|
+
@sbuf.concat(content)
|
|
287
|
+
end
|
|
259
288
|
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
289
|
+
# This handles the initial parser state. We read until we find the starting
|
|
290
|
+
# boundary, then we can transition to the next state. If we find the ending
|
|
291
|
+
# boundary, this is an invalid multipart upload, but keep scanning for opening
|
|
292
|
+
# boundary in that case. If no boundary found, we need to keep reading data
|
|
293
|
+
# and retry. It's highly unlikely the initial read will not consume the
|
|
294
|
+
# boundary. The client would have to deliberately craft a response
|
|
295
|
+
# with the opening boundary beyond the buffer size for that to happen.
|
|
296
|
+
def handle_fast_forward
|
|
297
|
+
while true
|
|
298
|
+
case consume_boundary
|
|
299
|
+
when :BOUNDARY
|
|
300
|
+
# found opening boundary, transition to next state
|
|
301
|
+
@state = :MIME_HEAD
|
|
302
|
+
return
|
|
303
|
+
when :END_BOUNDARY
|
|
304
|
+
# invalid multipart upload
|
|
305
|
+
if @sbuf.pos == @end_boundary_size && @sbuf.rest == EOL
|
|
306
|
+
# stop parsing a buffer if a buffer is only an end boundary.
|
|
307
|
+
@state = :DONE
|
|
308
|
+
return
|
|
309
|
+
end
|
|
267
310
|
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
311
|
+
# retry for opening boundary
|
|
312
|
+
else
|
|
313
|
+
# We raise if we don't find the multipart boundary, to avoid unbounded memory
|
|
314
|
+
# buffering. Note that the actual limit is the higher of 16KB and the buffer size (1MB by default)
|
|
315
|
+
raise Error, "multipart boundary not found within limit" if @sbuf.string.bytesize > BOUNDARY_START_LIMIT
|
|
271
316
|
|
|
272
|
-
|
|
273
|
-
|
|
317
|
+
# no boundary found, keep reading data
|
|
318
|
+
return :want_read
|
|
319
|
+
end
|
|
274
320
|
end
|
|
275
321
|
end
|
|
276
322
|
|
|
@@ -284,17 +330,101 @@ module Rack
|
|
|
284
330
|
end
|
|
285
331
|
end
|
|
286
332
|
|
|
333
|
+
CONTENT_DISPOSITION_MAX_PARAMS = 16
|
|
334
|
+
CONTENT_DISPOSITION_MAX_BYTES = 1536
|
|
287
335
|
def handle_mime_head
|
|
288
336
|
if @sbuf.scan_until(@head_regex)
|
|
289
337
|
head = @sbuf[1]
|
|
290
338
|
content_type = head[MULTIPART_CONTENT_TYPE, 1]
|
|
291
|
-
if
|
|
292
|
-
|
|
339
|
+
if (disposition = head[MULTIPART_CONTENT_DISPOSITION, 1]) &&
|
|
340
|
+
disposition.bytesize <= CONTENT_DISPOSITION_MAX_BYTES
|
|
341
|
+
|
|
342
|
+
# ignore actual content-disposition value (should always be form-data)
|
|
343
|
+
i = disposition.index(';')
|
|
344
|
+
disposition.slice!(0, i+1)
|
|
345
|
+
param = nil
|
|
346
|
+
num_params = 0
|
|
347
|
+
|
|
348
|
+
# Parse parameter list
|
|
349
|
+
while i = disposition.index('=')
|
|
350
|
+
# Only parse up to max parameters, to avoid potential denial of service
|
|
351
|
+
num_params += 1
|
|
352
|
+
break if num_params > CONTENT_DISPOSITION_MAX_PARAMS
|
|
353
|
+
|
|
354
|
+
# Found end of parameter name, ensure forward progress in loop
|
|
355
|
+
param = disposition.slice!(0, i+1)
|
|
356
|
+
|
|
357
|
+
# Remove ending equals and preceding whitespace from parameter name
|
|
358
|
+
param.chomp!('=')
|
|
359
|
+
param.lstrip!
|
|
360
|
+
|
|
361
|
+
if disposition[0] == '"'
|
|
362
|
+
# Parameter value is quoted, parse it, handling backslash escapes
|
|
363
|
+
disposition.slice!(0, 1)
|
|
364
|
+
value = String.new
|
|
365
|
+
|
|
366
|
+
while i = disposition.index(/(["\\])/)
|
|
367
|
+
c = $1
|
|
368
|
+
|
|
369
|
+
# Append all content until ending quote or escape
|
|
370
|
+
value << disposition.slice!(0, i)
|
|
371
|
+
|
|
372
|
+
# Remove either backslash or ending quote,
|
|
373
|
+
# ensures forward progress in loop
|
|
374
|
+
disposition.slice!(0, 1)
|
|
375
|
+
|
|
376
|
+
# stop parsing parameter value if found ending quote
|
|
377
|
+
break if c == '"'
|
|
378
|
+
|
|
379
|
+
escaped_char = disposition.slice!(0, 1)
|
|
380
|
+
if param == 'filename' && escaped_char != '"'
|
|
381
|
+
# Possible IE uploaded filename, append both escape backslash and value
|
|
382
|
+
value << c << escaped_char
|
|
383
|
+
else
|
|
384
|
+
# Other only append escaped value
|
|
385
|
+
value << escaped_char
|
|
386
|
+
end
|
|
387
|
+
end
|
|
388
|
+
else
|
|
389
|
+
if i = disposition.index(';')
|
|
390
|
+
# Parameter value unquoted (which may be invalid), value ends at semicolon
|
|
391
|
+
value = disposition.slice!(0, i)
|
|
392
|
+
else
|
|
393
|
+
# If no ending semicolon, assume remainder of line is value and stop
|
|
394
|
+
# parsing
|
|
395
|
+
disposition.strip!
|
|
396
|
+
value = disposition
|
|
397
|
+
disposition = ''
|
|
398
|
+
end
|
|
399
|
+
end
|
|
400
|
+
|
|
401
|
+
case param
|
|
402
|
+
when 'name'
|
|
403
|
+
name = value
|
|
404
|
+
when 'filename'
|
|
405
|
+
filename = value
|
|
406
|
+
when 'filename*'
|
|
407
|
+
filename_star = value
|
|
408
|
+
# else
|
|
409
|
+
# ignore other parameters
|
|
410
|
+
end
|
|
411
|
+
|
|
412
|
+
# skip trailing semicolon, to proceed to next parameter
|
|
413
|
+
if i = disposition.index(';')
|
|
414
|
+
disposition.slice!(0, i+1)
|
|
415
|
+
end
|
|
416
|
+
end
|
|
293
417
|
else
|
|
294
418
|
name = head[MULTIPART_CONTENT_ID, 1]
|
|
295
419
|
end
|
|
296
420
|
|
|
297
|
-
|
|
421
|
+
if filename_star
|
|
422
|
+
encoding, _, filename = filename_star.split("'", 3)
|
|
423
|
+
filename = normalize_filename(filename || '')
|
|
424
|
+
filename.force_encoding(find_encoding(encoding))
|
|
425
|
+
elsif filename
|
|
426
|
+
filename = normalize_filename(filename)
|
|
427
|
+
end
|
|
298
428
|
|
|
299
429
|
if name.nil? || name.empty?
|
|
300
430
|
name = filename || "#{content_type || TEXT_PLAIN}[]".dup
|
|
@@ -314,7 +444,7 @@ module Rack
|
|
|
314
444
|
else
|
|
315
445
|
# We raise if the mime part header is too large, to avoid unbounded memory
|
|
316
446
|
# buffering. Note that the actual limit is the higher of 64KB and the buffer size (1MB by default)
|
|
317
|
-
raise
|
|
447
|
+
raise Error, "multipart mime part header too large" if @sbuf.rest.bytesize > MIME_HEADER_BYTESIZE_LIMIT
|
|
318
448
|
|
|
319
449
|
return :want_read
|
|
320
450
|
end
|
|
@@ -322,7 +452,7 @@ module Rack
|
|
|
322
452
|
|
|
323
453
|
def handle_mime_body
|
|
324
454
|
if (body_with_boundary = @sbuf.check_until(@body_regex)) # check but do not advance the pointer yet
|
|
325
|
-
body = body_with_boundary.sub(
|
|
455
|
+
body = body_with_boundary.sub(@body_regex_at_end, '') # remove the boundary from the string
|
|
326
456
|
update_retained_size(body.bytesize) if @body_retained
|
|
327
457
|
@collector.on_mime_body @mime_index, body
|
|
328
458
|
@sbuf.pos += body.length + 2 # skip \r\n after the content
|
|
@@ -342,12 +472,10 @@ module Rack
|
|
|
342
472
|
end
|
|
343
473
|
end
|
|
344
474
|
|
|
345
|
-
def full_boundary; @full_boundary; end
|
|
346
|
-
|
|
347
475
|
def update_retained_size(size)
|
|
348
476
|
@retained_size += size
|
|
349
477
|
if @retained_size > BUFFERED_UPLOAD_BYTESIZE_LIMIT
|
|
350
|
-
raise
|
|
478
|
+
raise Error, "multipart data over retained size limit"
|
|
351
479
|
end
|
|
352
480
|
end
|
|
353
481
|
|
|
@@ -356,51 +484,26 @@ module Rack
|
|
|
356
484
|
# end of the boundary. If we don't find the start or end of the
|
|
357
485
|
# boundary, clear the buffer and return nil.
|
|
358
486
|
def consume_boundary
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
return if @sbuf.eos?
|
|
487
|
+
if read_buffer = @sbuf.scan_until(@body_regex)
|
|
488
|
+
read_buffer.end_with?(EOL) ? :BOUNDARY : :END_BOUNDARY
|
|
489
|
+
else
|
|
490
|
+
@sbuf.terminate
|
|
491
|
+
nil
|
|
365
492
|
end
|
|
366
493
|
end
|
|
367
494
|
|
|
368
|
-
def
|
|
369
|
-
filename = nil
|
|
370
|
-
case head
|
|
371
|
-
when RFC2183
|
|
372
|
-
params = Hash[*head.scan(DISPPARM).flat_map(&:compact)]
|
|
373
|
-
|
|
374
|
-
if filename = params['filename']
|
|
375
|
-
filename = $1 if filename =~ /^"(.*)"$/
|
|
376
|
-
elsif filename = params['filename*']
|
|
377
|
-
encoding, _, filename = filename.split("'", 3)
|
|
378
|
-
end
|
|
379
|
-
when BROKEN
|
|
380
|
-
filename = $1
|
|
381
|
-
filename = $1 if filename =~ /^"(.*)"$/
|
|
382
|
-
end
|
|
383
|
-
|
|
384
|
-
return unless filename
|
|
385
|
-
|
|
495
|
+
def normalize_filename(filename)
|
|
386
496
|
if filename.scan(/%.?.?/).all? { |s| /%[0-9a-fA-F]{2}/.match?(s) }
|
|
387
497
|
filename = Utils.unescape_path(filename)
|
|
388
498
|
end
|
|
389
499
|
|
|
390
500
|
filename.scrub!
|
|
391
501
|
|
|
392
|
-
|
|
393
|
-
filename = filename.gsub(/\\(.)/, '\1')
|
|
394
|
-
end
|
|
395
|
-
|
|
396
|
-
if encoding
|
|
397
|
-
filename.force_encoding ::Encoding.find(encoding)
|
|
398
|
-
end
|
|
399
|
-
|
|
400
|
-
filename
|
|
502
|
+
filename.split(/[\/\\]/).last || String.new
|
|
401
503
|
end
|
|
402
504
|
|
|
403
505
|
CHARSET = "charset"
|
|
506
|
+
deprecate_constant :CHARSET
|
|
404
507
|
|
|
405
508
|
def tag_multipart_encoding(filename, content_type, name, body)
|
|
406
509
|
name = name.to_s
|
|
@@ -421,7 +524,9 @@ module Rack
|
|
|
421
524
|
k.strip!
|
|
422
525
|
v.strip!
|
|
423
526
|
v = v[1..-2] if v.start_with?('"') && v.end_with?('"')
|
|
424
|
-
|
|
527
|
+
if k == "charset"
|
|
528
|
+
encoding = find_encoding(v)
|
|
529
|
+
end
|
|
425
530
|
end
|
|
426
531
|
end
|
|
427
532
|
end
|
|
@@ -430,9 +535,18 @@ module Rack
|
|
|
430
535
|
body.force_encoding(encoding)
|
|
431
536
|
end
|
|
432
537
|
|
|
538
|
+
# Return the related Encoding object. However, because
|
|
539
|
+
# enc is submitted by the user, it may be invalid, so
|
|
540
|
+
# use a binary encoding in that case.
|
|
541
|
+
def find_encoding(enc)
|
|
542
|
+
Encoding.find enc
|
|
543
|
+
rescue ArgumentError
|
|
544
|
+
Encoding::BINARY
|
|
545
|
+
end
|
|
546
|
+
|
|
433
547
|
def handle_empty_content!(content)
|
|
434
548
|
if content.nil? || content.empty?
|
|
435
|
-
raise
|
|
549
|
+
raise EmptyContentError
|
|
436
550
|
end
|
|
437
551
|
end
|
|
438
552
|
end
|