rack 2.2.20 → 3.2.4
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 +557 -70
- data/CONTRIBUTING.md +63 -55
- data/MIT-LICENSE +1 -1
- data/README.md +384 -0
- data/SPEC.rdoc +243 -277
- data/lib/rack/auth/abstract/handler.rb +3 -1
- data/lib/rack/auth/abstract/request.rb +5 -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 +108 -69
- data/lib/rack/cascade.rb +2 -3
- data/lib/rack/common_logger.rb +22 -17
- data/lib/rack/conditional_get.rb +20 -16
- data/lib/rack/constants.rb +68 -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 +25 -6
- data/lib/rack/files.rb +15 -17
- data/lib/rack/head.rb +8 -8
- data/lib/rack/headers.rb +238 -0
- data/lib/rack/lint.rb +817 -648
- data/lib/rack/lock.rb +2 -5
- data/lib/rack/media_type.rb +6 -7
- 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 +147 -0
- data/lib/rack/multipart/generator.rb +7 -5
- data/lib/rack/multipart/parser.rb +242 -102
- data/lib/rack/multipart/uploaded_file.rb +45 -4
- data/lib/rack/multipart.rb +53 -40
- data/lib/rack/null_logger.rb +9 -0
- data/lib/rack/query_parser.rb +116 -121
- data/lib/rack/recursive.rb +2 -0
- data/lib/rack/reloader.rb +0 -2
- data/lib/rack/request.rb +272 -144
- data/lib/rack/response.rb +151 -66
- data/lib/rack/rewindable_input.rb +27 -5
- data/lib/rack/runtime.rb +7 -6
- data/lib/rack/sendfile.rb +35 -30
- data/lib/rack/show_exceptions.rb +25 -6
- data/lib/rack/show_status.rb +17 -9
- 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 +228 -238
- data/lib/rack/version.rb +3 -15
- data/lib/rack.rb +13 -90
- metadata +14 -40
- 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/logger.rb +0 -20
- 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,147 @@
|
|
|
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
|
+
class Cookie
|
|
14
|
+
attr_reader :name, :value, :path, :domain, :expires, :secure
|
|
15
|
+
|
|
16
|
+
def initialize(args)
|
|
17
|
+
@name = args["name"]
|
|
18
|
+
@value = args["value"]
|
|
19
|
+
@path = args["path"]
|
|
20
|
+
@domain = args["domain"]
|
|
21
|
+
@expires = args["expires"]
|
|
22
|
+
@secure = args["secure"]
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def method_missing(method_name, *args, &block)
|
|
26
|
+
@value.send(method_name, *args, &block)
|
|
27
|
+
end
|
|
28
|
+
# :nocov:
|
|
29
|
+
ruby2_keywords(:method_missing) if respond_to?(:ruby2_keywords, true)
|
|
30
|
+
# :nocov:
|
|
31
|
+
|
|
32
|
+
def respond_to_missing?(method_name, include_all = false)
|
|
33
|
+
@value.respond_to?(method_name, include_all) || super
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
class << self
|
|
38
|
+
alias [] new
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Headers
|
|
42
|
+
attr_reader :original_headers, :cookies
|
|
43
|
+
|
|
44
|
+
# Errors
|
|
45
|
+
attr_accessor :errors
|
|
46
|
+
|
|
47
|
+
def initialize(status, headers, body, errors = nil)
|
|
48
|
+
@original_headers = headers
|
|
49
|
+
|
|
50
|
+
if errors
|
|
51
|
+
@errors = errors.string if errors.respond_to?(:string)
|
|
52
|
+
else
|
|
53
|
+
@errors = ""
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
super(body, status, headers)
|
|
57
|
+
|
|
58
|
+
@cookies = parse_cookies_from_header
|
|
59
|
+
buffered_body!
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def =~(other)
|
|
63
|
+
body =~ other
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def match(other)
|
|
67
|
+
body.match other
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def body
|
|
71
|
+
return @buffered_body if defined?(@buffered_body)
|
|
72
|
+
|
|
73
|
+
# FIXME: apparently users of MockResponse expect the return value of
|
|
74
|
+
# MockResponse#body to be a string. However, the real response object
|
|
75
|
+
# returns the body as a list.
|
|
76
|
+
#
|
|
77
|
+
# See spec_showstatus.rb:
|
|
78
|
+
#
|
|
79
|
+
# should "not replace existing messages" do
|
|
80
|
+
# ...
|
|
81
|
+
# res.body.should == "foo!"
|
|
82
|
+
# end
|
|
83
|
+
buffer = @buffered_body = String.new
|
|
84
|
+
|
|
85
|
+
@body.each do |chunk|
|
|
86
|
+
buffer << chunk
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
return buffer
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def empty?
|
|
93
|
+
[201, 204, 304].include? status
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def cookie(name)
|
|
97
|
+
cookies.fetch(name, nil)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
private
|
|
101
|
+
|
|
102
|
+
def parse_cookies_from_header
|
|
103
|
+
cookies = Hash.new
|
|
104
|
+
set_cookie_header = headers['set-cookie']
|
|
105
|
+
if set_cookie_header && !set_cookie_header.empty?
|
|
106
|
+
Array(set_cookie_header).each do |cookie|
|
|
107
|
+
cookie_name, cookie_filling = cookie.split('=', 2)
|
|
108
|
+
cookie_attributes = identify_cookie_attributes cookie_filling
|
|
109
|
+
parsed_cookie = Cookie.new(
|
|
110
|
+
'name' => cookie_name.strip,
|
|
111
|
+
'value' => cookie_attributes.fetch('value'),
|
|
112
|
+
'path' => cookie_attributes.fetch('path', nil),
|
|
113
|
+
'domain' => cookie_attributes.fetch('domain', nil),
|
|
114
|
+
'expires' => cookie_attributes.fetch('expires', nil),
|
|
115
|
+
'secure' => cookie_attributes.fetch('secure', false)
|
|
116
|
+
)
|
|
117
|
+
cookies.store(cookie_name, parsed_cookie)
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
cookies
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def identify_cookie_attributes(cookie_filling)
|
|
124
|
+
cookie_bits = cookie_filling.split(';')
|
|
125
|
+
cookie_attributes = Hash.new
|
|
126
|
+
cookie_attributes.store('value', Array(cookie_bits[0].strip))
|
|
127
|
+
cookie_bits.drop(1).each do |bit|
|
|
128
|
+
if bit.include? '='
|
|
129
|
+
cookie_attribute, attribute_value = bit.split('=', 2)
|
|
130
|
+
cookie_attributes.store(cookie_attribute.strip.downcase, attribute_value.strip)
|
|
131
|
+
end
|
|
132
|
+
if bit.include? 'secure'
|
|
133
|
+
cookie_attributes.store('secure', true)
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
if cookie_attributes.key? 'max-age'
|
|
138
|
+
cookie_attributes.store('expires', Time.now + cookie_attributes['max-age'].to_i)
|
|
139
|
+
elsif cookie_attributes.key? 'expires'
|
|
140
|
+
cookie_attributes.store('expires', Time.httpdate(cookie_attributes['expires']))
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
cookie_attributes
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
end
|
|
147
|
+
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,55 @@
|
|
|
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
|
+
# Rack::Multipart::Parser handles parsing of multipart/form-data requests.
|
|
42
|
+
#
|
|
43
|
+
# File Parameter Contents
|
|
44
|
+
#
|
|
45
|
+
# When processing file uploads, the parser returns a hash containing
|
|
46
|
+
# information about uploaded files. For +file+ parameters, the hash includes:
|
|
47
|
+
#
|
|
48
|
+
# * +:filename+ - The original filename, already URL decoded by the parser
|
|
49
|
+
# * +:type+ - The content type of the uploaded file
|
|
50
|
+
# * +:name+ - The parameter name from the form
|
|
51
|
+
# * +:tempfile+ - A Tempfile object containing the uploaded data
|
|
52
|
+
# * +:head+ - The raw header content for this part
|
|
53
|
+
class Parser
|
|
13
54
|
BUFSIZE = 1_048_576
|
|
14
55
|
TEXT_PLAIN = "text/plain"
|
|
15
56
|
TEMPFILE_FACTORY = lambda { |filename, content_type|
|
|
@@ -18,8 +59,6 @@ module Rack
|
|
|
18
59
|
Tempfile.new(["RackMultipart", extension])
|
|
19
60
|
}
|
|
20
61
|
|
|
21
|
-
BOUNDARY_REGEX = /\A([^\n]*(?:\n|\Z))/
|
|
22
|
-
|
|
23
62
|
BOUNDARY_START_LIMIT = 16 * 1024
|
|
24
63
|
private_constant :BOUNDARY_START_LIMIT
|
|
25
64
|
|
|
@@ -62,16 +101,12 @@ module Rack
|
|
|
62
101
|
if str
|
|
63
102
|
@cursor += str.bytesize
|
|
64
103
|
else
|
|
65
|
-
# Raise an error for mismatching
|
|
104
|
+
# Raise an error for mismatching content-length and actual contents
|
|
66
105
|
raise EOFError, "bad content body"
|
|
67
106
|
end
|
|
68
107
|
|
|
69
108
|
str
|
|
70
109
|
end
|
|
71
|
-
|
|
72
|
-
def rewind
|
|
73
|
-
@io.rewind
|
|
74
|
-
end
|
|
75
110
|
end
|
|
76
111
|
|
|
77
112
|
MultipartInfo = Struct.new :params, :tmp_files
|
|
@@ -90,18 +125,17 @@ module Rack
|
|
|
90
125
|
boundary = parse_boundary content_type
|
|
91
126
|
return EMPTY unless boundary
|
|
92
127
|
|
|
128
|
+
if boundary.length > 70
|
|
129
|
+
# RFC 1521 Section 7.2.1 imposes a 70 character maximum for the boundary.
|
|
130
|
+
# Most clients use no more than 55 characters.
|
|
131
|
+
raise BoundaryTooLongError, "multipart boundary size too large (#{boundary.length} characters)"
|
|
132
|
+
end
|
|
133
|
+
|
|
93
134
|
io = BoundedIO.new(io, content_length) if content_length
|
|
94
|
-
outbuf = String.new
|
|
95
135
|
|
|
96
136
|
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
|
|
137
|
+
parser.parse(io)
|
|
103
138
|
|
|
104
|
-
io.rewind
|
|
105
139
|
parser.result
|
|
106
140
|
end
|
|
107
141
|
|
|
@@ -201,11 +235,8 @@ module Rack
|
|
|
201
235
|
def initialize(boundary, tempfile, bufsize, query_parser)
|
|
202
236
|
@query_parser = query_parser
|
|
203
237
|
@params = query_parser.make_params
|
|
204
|
-
@boundary = "--#{boundary}"
|
|
205
238
|
@bufsize = bufsize
|
|
206
239
|
|
|
207
|
-
@full_boundary = @boundary
|
|
208
|
-
@end_boundary = @boundary + '--'
|
|
209
240
|
@state = :FAST_FORWARD
|
|
210
241
|
@mime_index = 0
|
|
211
242
|
@body_retained = nil
|
|
@@ -213,23 +244,42 @@ module Rack
|
|
|
213
244
|
@collector = Collector.new tempfile
|
|
214
245
|
|
|
215
246
|
@sbuf = StringScanner.new("".dup)
|
|
216
|
-
@body_regex = /(?:#{EOL})
|
|
217
|
-
@
|
|
218
|
-
@
|
|
247
|
+
@body_regex = /(?:#{EOL}|\A)--#{Regexp.quote(boundary)}(?:#{EOL}|--)/m
|
|
248
|
+
@body_regex_at_end = /#{@body_regex}\z/m
|
|
249
|
+
@end_boundary_size = boundary.bytesize + 4 # (-- at start, -- at finish)
|
|
250
|
+
@rx_max_size = boundary.bytesize + 6 # (\r\n-- at start, either \r\n or -- at finish)
|
|
219
251
|
@head_regex = /(.*?#{EOL})#{EOL}/m
|
|
220
252
|
end
|
|
221
253
|
|
|
222
|
-
def
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
254
|
+
def parse(io)
|
|
255
|
+
outbuf = String.new
|
|
256
|
+
read_data(io, outbuf)
|
|
257
|
+
|
|
258
|
+
loop do
|
|
259
|
+
status =
|
|
260
|
+
case @state
|
|
261
|
+
when :FAST_FORWARD
|
|
262
|
+
handle_fast_forward
|
|
263
|
+
when :CONSUME_TOKEN
|
|
264
|
+
handle_consume_token
|
|
265
|
+
when :MIME_HEAD
|
|
266
|
+
handle_mime_head
|
|
267
|
+
when :MIME_BODY
|
|
268
|
+
handle_mime_body
|
|
269
|
+
else # when :DONE
|
|
270
|
+
return
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
read_data(io, outbuf) if status == :want_read
|
|
274
|
+
end
|
|
226
275
|
end
|
|
227
276
|
|
|
228
277
|
def result
|
|
229
278
|
@collector.each do |part|
|
|
230
279
|
part.get_data do |data|
|
|
231
280
|
tag_multipart_encoding(part.filename, part.content_type, part.name, data)
|
|
232
|
-
|
|
281
|
+
name, data = handle_dummy_encoding(part.name, data)
|
|
282
|
+
@query_parser.normalize_params(@params, name, data)
|
|
233
283
|
end
|
|
234
284
|
end
|
|
235
285
|
MultipartInfo.new @params.to_params_hash, @collector.find_all(&:file?).map(&:body)
|
|
@@ -237,40 +287,43 @@ module Rack
|
|
|
237
287
|
|
|
238
288
|
private
|
|
239
289
|
|
|
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
|
|
290
|
+
def read_data(io, outbuf)
|
|
291
|
+
content = io.read(@bufsize, outbuf)
|
|
292
|
+
handle_empty_content!(content)
|
|
293
|
+
@sbuf.concat(content)
|
|
255
294
|
end
|
|
256
295
|
|
|
296
|
+
# This handles the initial parser state. We read until we find the starting
|
|
297
|
+
# boundary, then we can transition to the next state. If we find the ending
|
|
298
|
+
# boundary, this is an invalid multipart upload, but keep scanning for opening
|
|
299
|
+
# boundary in that case. If no boundary found, we need to keep reading data
|
|
300
|
+
# and retry. It's highly unlikely the initial read will not consume the
|
|
301
|
+
# boundary. The client would have to deliberately craft a response
|
|
302
|
+
# with the opening boundary beyond the buffer size for that to happen.
|
|
257
303
|
def handle_fast_forward
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
304
|
+
while true
|
|
305
|
+
case consume_boundary
|
|
306
|
+
when :BOUNDARY
|
|
307
|
+
# found opening boundary, transition to next state
|
|
308
|
+
@state = :MIME_HEAD
|
|
309
|
+
return
|
|
310
|
+
when :END_BOUNDARY
|
|
311
|
+
# invalid multipart upload
|
|
312
|
+
if @sbuf.pos == @end_boundary_size && @sbuf.rest == EOL
|
|
313
|
+
# stop parsing a buffer if a buffer is only an end boundary.
|
|
314
|
+
@state = :DONE
|
|
315
|
+
return
|
|
316
|
+
end
|
|
267
317
|
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
318
|
+
# retry for opening boundary
|
|
319
|
+
else
|
|
320
|
+
# We raise if we don't find the multipart boundary, to avoid unbounded memory
|
|
321
|
+
# buffering. Note that the actual limit is the higher of 16KB and the buffer size (1MB by default)
|
|
322
|
+
raise Error, "multipart boundary not found within limit" if @sbuf.string.bytesize > BOUNDARY_START_LIMIT
|
|
271
323
|
|
|
272
|
-
|
|
273
|
-
|
|
324
|
+
# no boundary found, keep reading data
|
|
325
|
+
return :want_read
|
|
326
|
+
end
|
|
274
327
|
end
|
|
275
328
|
end
|
|
276
329
|
|
|
@@ -284,17 +337,101 @@ module Rack
|
|
|
284
337
|
end
|
|
285
338
|
end
|
|
286
339
|
|
|
340
|
+
CONTENT_DISPOSITION_MAX_PARAMS = 16
|
|
341
|
+
CONTENT_DISPOSITION_MAX_BYTES = 1536
|
|
287
342
|
def handle_mime_head
|
|
288
343
|
if @sbuf.scan_until(@head_regex)
|
|
289
344
|
head = @sbuf[1]
|
|
290
345
|
content_type = head[MULTIPART_CONTENT_TYPE, 1]
|
|
291
|
-
if
|
|
292
|
-
|
|
346
|
+
if (disposition = head[MULTIPART_CONTENT_DISPOSITION, 1]) &&
|
|
347
|
+
disposition.bytesize <= CONTENT_DISPOSITION_MAX_BYTES
|
|
348
|
+
|
|
349
|
+
# ignore actual content-disposition value (should always be form-data)
|
|
350
|
+
i = disposition.index(';')
|
|
351
|
+
disposition.slice!(0, i+1)
|
|
352
|
+
param = nil
|
|
353
|
+
num_params = 0
|
|
354
|
+
|
|
355
|
+
# Parse parameter list
|
|
356
|
+
while i = disposition.index('=')
|
|
357
|
+
# Only parse up to max parameters, to avoid potential denial of service
|
|
358
|
+
num_params += 1
|
|
359
|
+
break if num_params > CONTENT_DISPOSITION_MAX_PARAMS
|
|
360
|
+
|
|
361
|
+
# Found end of parameter name, ensure forward progress in loop
|
|
362
|
+
param = disposition.slice!(0, i+1)
|
|
363
|
+
|
|
364
|
+
# Remove ending equals and preceding whitespace from parameter name
|
|
365
|
+
param.chomp!('=')
|
|
366
|
+
param.lstrip!
|
|
367
|
+
|
|
368
|
+
if disposition[0] == '"'
|
|
369
|
+
# Parameter value is quoted, parse it, handling backslash escapes
|
|
370
|
+
disposition.slice!(0, 1)
|
|
371
|
+
value = String.new
|
|
372
|
+
|
|
373
|
+
while i = disposition.index(/(["\\])/)
|
|
374
|
+
c = $1
|
|
375
|
+
|
|
376
|
+
# Append all content until ending quote or escape
|
|
377
|
+
value << disposition.slice!(0, i)
|
|
378
|
+
|
|
379
|
+
# Remove either backslash or ending quote,
|
|
380
|
+
# ensures forward progress in loop
|
|
381
|
+
disposition.slice!(0, 1)
|
|
382
|
+
|
|
383
|
+
# stop parsing parameter value if found ending quote
|
|
384
|
+
break if c == '"'
|
|
385
|
+
|
|
386
|
+
escaped_char = disposition.slice!(0, 1)
|
|
387
|
+
if param == 'filename' && escaped_char != '"'
|
|
388
|
+
# Possible IE uploaded filename, append both escape backslash and value
|
|
389
|
+
value << c << escaped_char
|
|
390
|
+
else
|
|
391
|
+
# Other only append escaped value
|
|
392
|
+
value << escaped_char
|
|
393
|
+
end
|
|
394
|
+
end
|
|
395
|
+
else
|
|
396
|
+
if i = disposition.index(';')
|
|
397
|
+
# Parameter value unquoted (which may be invalid), value ends at semicolon
|
|
398
|
+
value = disposition.slice!(0, i)
|
|
399
|
+
else
|
|
400
|
+
# If no ending semicolon, assume remainder of line is value and stop
|
|
401
|
+
# parsing
|
|
402
|
+
disposition.strip!
|
|
403
|
+
value = disposition
|
|
404
|
+
disposition = ''
|
|
405
|
+
end
|
|
406
|
+
end
|
|
407
|
+
|
|
408
|
+
case param
|
|
409
|
+
when 'name'
|
|
410
|
+
name = value
|
|
411
|
+
when 'filename'
|
|
412
|
+
filename = value
|
|
413
|
+
when 'filename*'
|
|
414
|
+
filename_star = value
|
|
415
|
+
# else
|
|
416
|
+
# ignore other parameters
|
|
417
|
+
end
|
|
418
|
+
|
|
419
|
+
# skip trailing semicolon, to proceed to next parameter
|
|
420
|
+
if i = disposition.index(';')
|
|
421
|
+
disposition.slice!(0, i+1)
|
|
422
|
+
end
|
|
423
|
+
end
|
|
293
424
|
else
|
|
294
425
|
name = head[MULTIPART_CONTENT_ID, 1]
|
|
295
426
|
end
|
|
296
427
|
|
|
297
|
-
|
|
428
|
+
if filename_star
|
|
429
|
+
encoding, _, filename = filename_star.split("'", 3)
|
|
430
|
+
filename = normalize_filename(filename || '')
|
|
431
|
+
filename.force_encoding(find_encoding(encoding))
|
|
432
|
+
elsif filename
|
|
433
|
+
filename = normalize_filename(filename)
|
|
434
|
+
end
|
|
298
435
|
|
|
299
436
|
if name.nil? || name.empty?
|
|
300
437
|
name = filename || "#{content_type || TEXT_PLAIN}[]".dup
|
|
@@ -314,7 +451,7 @@ module Rack
|
|
|
314
451
|
else
|
|
315
452
|
# We raise if the mime part header is too large, to avoid unbounded memory
|
|
316
453
|
# buffering. Note that the actual limit is the higher of 64KB and the buffer size (1MB by default)
|
|
317
|
-
raise
|
|
454
|
+
raise Error, "multipart mime part header too large" if @sbuf.rest.bytesize > MIME_HEADER_BYTESIZE_LIMIT
|
|
318
455
|
|
|
319
456
|
return :want_read
|
|
320
457
|
end
|
|
@@ -322,7 +459,7 @@ module Rack
|
|
|
322
459
|
|
|
323
460
|
def handle_mime_body
|
|
324
461
|
if (body_with_boundary = @sbuf.check_until(@body_regex)) # check but do not advance the pointer yet
|
|
325
|
-
body = body_with_boundary.sub(
|
|
462
|
+
body = body_with_boundary.sub(@body_regex_at_end, '') # remove the boundary from the string
|
|
326
463
|
update_retained_size(body.bytesize) if @body_retained
|
|
327
464
|
@collector.on_mime_body @mime_index, body
|
|
328
465
|
@sbuf.pos += body.length + 2 # skip \r\n after the content
|
|
@@ -342,12 +479,10 @@ module Rack
|
|
|
342
479
|
end
|
|
343
480
|
end
|
|
344
481
|
|
|
345
|
-
def full_boundary; @full_boundary; end
|
|
346
|
-
|
|
347
482
|
def update_retained_size(size)
|
|
348
483
|
@retained_size += size
|
|
349
484
|
if @retained_size > BUFFERED_UPLOAD_BYTESIZE_LIMIT
|
|
350
|
-
raise
|
|
485
|
+
raise Error, "multipart data over retained size limit"
|
|
351
486
|
end
|
|
352
487
|
end
|
|
353
488
|
|
|
@@ -356,51 +491,26 @@ module Rack
|
|
|
356
491
|
# end of the boundary. If we don't find the start or end of the
|
|
357
492
|
# boundary, clear the buffer and return nil.
|
|
358
493
|
def consume_boundary
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
return if @sbuf.eos?
|
|
494
|
+
if read_buffer = @sbuf.scan_until(@body_regex)
|
|
495
|
+
read_buffer.end_with?(EOL) ? :BOUNDARY : :END_BOUNDARY
|
|
496
|
+
else
|
|
497
|
+
@sbuf.terminate
|
|
498
|
+
nil
|
|
365
499
|
end
|
|
366
500
|
end
|
|
367
501
|
|
|
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
|
-
|
|
502
|
+
def normalize_filename(filename)
|
|
386
503
|
if filename.scan(/%.?.?/).all? { |s| /%[0-9a-fA-F]{2}/.match?(s) }
|
|
387
504
|
filename = Utils.unescape_path(filename)
|
|
388
505
|
end
|
|
389
506
|
|
|
390
507
|
filename.scrub!
|
|
391
508
|
|
|
392
|
-
|
|
393
|
-
filename = filename.gsub(/\\(.)/, '\1')
|
|
394
|
-
end
|
|
395
|
-
|
|
396
|
-
if encoding
|
|
397
|
-
filename.force_encoding ::Encoding.find(encoding)
|
|
398
|
-
end
|
|
399
|
-
|
|
400
|
-
filename
|
|
509
|
+
filename.split(/[\/\\]/).last || String.new
|
|
401
510
|
end
|
|
402
511
|
|
|
403
512
|
CHARSET = "charset"
|
|
513
|
+
deprecate_constant :CHARSET
|
|
404
514
|
|
|
405
515
|
def tag_multipart_encoding(filename, content_type, name, body)
|
|
406
516
|
name = name.to_s
|
|
@@ -421,7 +531,9 @@ module Rack
|
|
|
421
531
|
k.strip!
|
|
422
532
|
v.strip!
|
|
423
533
|
v = v[1..-2] if v.start_with?('"') && v.end_with?('"')
|
|
424
|
-
|
|
534
|
+
if k == "charset"
|
|
535
|
+
encoding = find_encoding(v)
|
|
536
|
+
end
|
|
425
537
|
end
|
|
426
538
|
end
|
|
427
539
|
end
|
|
@@ -430,9 +542,37 @@ module Rack
|
|
|
430
542
|
body.force_encoding(encoding)
|
|
431
543
|
end
|
|
432
544
|
|
|
545
|
+
# Return the related Encoding object. However, because
|
|
546
|
+
# enc is submitted by the user, it may be invalid, so
|
|
547
|
+
# use a binary encoding in that case.
|
|
548
|
+
def find_encoding(enc)
|
|
549
|
+
Encoding.find enc
|
|
550
|
+
rescue ArgumentError
|
|
551
|
+
Encoding::BINARY
|
|
552
|
+
end
|
|
553
|
+
|
|
554
|
+
REENCODE_DUMMY_ENCODINGS = {
|
|
555
|
+
# ISO-2022-JP is a legacy but still widely used encoding in Japan
|
|
556
|
+
# Here we convert ISO-2022-JP to UTF-8 so that it can be handled.
|
|
557
|
+
Encoding::ISO_2022_JP => true
|
|
558
|
+
|
|
559
|
+
# Other dummy encodings are rarely used and have not been supported yet.
|
|
560
|
+
# Adding support for them will require careful considerations.
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
def handle_dummy_encoding(name, body)
|
|
564
|
+
# A string object with a 'dummy' encoding does not have full functionality and can cause errors.
|
|
565
|
+
# So here we covert it to UTF-8 so that it can be handled properly.
|
|
566
|
+
if name.encoding.dummy? && REENCODE_DUMMY_ENCODINGS[name.encoding]
|
|
567
|
+
name = name.encode(Encoding::UTF_8)
|
|
568
|
+
body = body.encode(Encoding::UTF_8)
|
|
569
|
+
end
|
|
570
|
+
return name, body
|
|
571
|
+
end
|
|
572
|
+
|
|
433
573
|
def handle_empty_content!(content)
|
|
434
574
|
if content.nil? || content.empty?
|
|
435
|
-
raise
|
|
575
|
+
raise EmptyContentError
|
|
436
576
|
end
|
|
437
577
|
end
|
|
438
578
|
end
|