rack 2.2.9 → 3.1.7
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +330 -88
- data/CONTRIBUTING.md +63 -55
- data/MIT-LICENSE +1 -1
- data/README.md +328 -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 -4
- 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 +23 -18
- 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 +14 -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 -271
- data/lib/rack/mock_request.rb +161 -0
- data/lib/rack/mock_response.rb +124 -0
- data/lib/rack/multipart/generator.rb +7 -5
- data/lib/rack/multipart/parser.rb +213 -95
- 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 +81 -102
- data/lib/rack/recursive.rb +2 -0
- data/lib/rack/reloader.rb +0 -2
- data/lib/rack/request.rb +260 -123
- 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 +30 -25
- 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 +232 -233
- data/lib/rack/version.rb +1 -9
- data/lib/rack.rb +13 -89
- metadata +15 -41
- data/README.rdoc +0 -320
- 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 -54
- 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 -36
- 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 -204
- data/lib/rack/session/memcache.rb +0 -10
- data/lib/rack/session/pool.rb +0 -85
- data/rack.gemspec +0 -46
@@ -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
|
+
return @buffered_body if defined?(@buffered_body)
|
49
|
+
|
50
|
+
# FIXME: apparently users of MockResponse expect the return value of
|
51
|
+
# MockResponse#body to be a string. However, the real response object
|
52
|
+
# returns the body as a list.
|
53
|
+
#
|
54
|
+
# See spec_showstatus.rb:
|
55
|
+
#
|
56
|
+
# should "not replace existing messages" do
|
57
|
+
# ...
|
58
|
+
# res.body.should == "foo!"
|
59
|
+
# end
|
60
|
+
buffer = @buffered_body = String.new
|
61
|
+
|
62
|
+
@body.each do |chunk|
|
63
|
+
buffer << chunk
|
64
|
+
end
|
65
|
+
|
66
|
+
return buffer
|
67
|
+
end
|
68
|
+
|
69
|
+
def empty?
|
70
|
+
[201, 204, 304].include? status
|
71
|
+
end
|
72
|
+
|
73
|
+
def cookie(name)
|
74
|
+
cookies.fetch(name, nil)
|
75
|
+
end
|
76
|
+
|
77
|
+
private
|
78
|
+
|
79
|
+
def parse_cookies_from_header
|
80
|
+
cookies = Hash.new
|
81
|
+
set_cookie_header = headers['set-cookie']
|
82
|
+
if set_cookie_header && !set_cookie_header.empty?
|
83
|
+
Array(set_cookie_header).each do |cookie|
|
84
|
+
cookie_name, cookie_filling = cookie.split('=', 2)
|
85
|
+
cookie_attributes = identify_cookie_attributes cookie_filling
|
86
|
+
parsed_cookie = CGI::Cookie.new(
|
87
|
+
'name' => cookie_name.strip,
|
88
|
+
'value' => cookie_attributes.fetch('value'),
|
89
|
+
'path' => cookie_attributes.fetch('path', nil),
|
90
|
+
'domain' => cookie_attributes.fetch('domain', nil),
|
91
|
+
'expires' => cookie_attributes.fetch('expires', nil),
|
92
|
+
'secure' => cookie_attributes.fetch('secure', false)
|
93
|
+
)
|
94
|
+
cookies.store(cookie_name, parsed_cookie)
|
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,14 +2,41 @@
|
|
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
|
+
MULTIPART = %r|\Amultipart/.*boundary=\"?([^\";,]+)\"?|ni
|
35
|
+
MULTIPART_CONTENT_TYPE = /Content-Type: (.*)#{EOL}/ni
|
36
|
+
MULTIPART_CONTENT_DISPOSITION = /Content-Disposition:(.*)(?=#{EOL}(\S|\z))/ni
|
37
|
+
MULTIPART_CONTENT_ID = /Content-ID:\s*([^#{EOL}]*)/ni
|
38
|
+
|
39
|
+
class Parser
|
13
40
|
BUFSIZE = 1_048_576
|
14
41
|
TEXT_PLAIN = "text/plain"
|
15
42
|
TEMPFILE_FACTORY = lambda { |filename, content_type|
|
@@ -18,8 +45,6 @@ module Rack
|
|
18
45
|
Tempfile.new(["RackMultipart", extension])
|
19
46
|
}
|
20
47
|
|
21
|
-
BOUNDARY_REGEX = /\A([^\n]*(?:\n|\Z))/
|
22
|
-
|
23
48
|
class BoundedIO # :nodoc:
|
24
49
|
def initialize(io, content_length)
|
25
50
|
@io = io
|
@@ -41,16 +66,12 @@ module Rack
|
|
41
66
|
if str
|
42
67
|
@cursor += str.bytesize
|
43
68
|
else
|
44
|
-
# Raise an error for mismatching
|
69
|
+
# Raise an error for mismatching content-length and actual contents
|
45
70
|
raise EOFError, "bad content body"
|
46
71
|
end
|
47
72
|
|
48
73
|
str
|
49
74
|
end
|
50
|
-
|
51
|
-
def rewind
|
52
|
-
@io.rewind
|
53
|
-
end
|
54
75
|
end
|
55
76
|
|
56
77
|
MultipartInfo = Struct.new :params, :tmp_files
|
@@ -69,18 +90,17 @@ module Rack
|
|
69
90
|
boundary = parse_boundary content_type
|
70
91
|
return EMPTY unless boundary
|
71
92
|
|
93
|
+
if boundary.length > 70
|
94
|
+
# RFC 1521 Section 7.2.1 imposes a 70 character maximum for the boundary.
|
95
|
+
# Most clients use no more than 55 characters.
|
96
|
+
raise BoundaryTooLongError, "multipart boundary size too large (#{boundary.length} characters)"
|
97
|
+
end
|
98
|
+
|
72
99
|
io = BoundedIO.new(io, content_length) if content_length
|
73
|
-
outbuf = String.new
|
74
100
|
|
75
101
|
parser = new(boundary, tmpfile, bufsize, qp)
|
76
|
-
parser.
|
77
|
-
|
78
|
-
loop do
|
79
|
-
break if parser.state == :DONE
|
80
|
-
parser.on_read io.read(bufsize, outbuf)
|
81
|
-
end
|
102
|
+
parser.parse(io)
|
82
103
|
|
83
|
-
io.rewind
|
84
104
|
parser.result
|
85
105
|
end
|
86
106
|
|
@@ -180,33 +200,48 @@ module Rack
|
|
180
200
|
def initialize(boundary, tempfile, bufsize, query_parser)
|
181
201
|
@query_parser = query_parser
|
182
202
|
@params = query_parser.make_params
|
183
|
-
@boundary = "--#{boundary}"
|
184
203
|
@bufsize = bufsize
|
185
204
|
|
186
|
-
@full_boundary = @boundary
|
187
|
-
@end_boundary = @boundary + '--'
|
188
205
|
@state = :FAST_FORWARD
|
189
206
|
@mime_index = 0
|
190
207
|
@collector = Collector.new tempfile
|
191
208
|
|
192
209
|
@sbuf = StringScanner.new("".dup)
|
193
|
-
@body_regex = /(?:#{EOL})
|
194
|
-
@
|
195
|
-
@
|
210
|
+
@body_regex = /(?:#{EOL}|\A)--#{Regexp.quote(boundary)}(?:#{EOL}|--)/m
|
211
|
+
@body_regex_at_end = /#{@body_regex}\z/m
|
212
|
+
@end_boundary_size = boundary.bytesize + 4 # (-- at start, -- at finish)
|
213
|
+
@rx_max_size = boundary.bytesize + 6 # (\r\n-- at start, either \r\n or -- at finish)
|
196
214
|
@head_regex = /(.*?#{EOL})#{EOL}/m
|
197
215
|
end
|
198
216
|
|
199
|
-
def
|
200
|
-
|
201
|
-
|
202
|
-
|
217
|
+
def parse(io)
|
218
|
+
outbuf = String.new
|
219
|
+
read_data(io, outbuf)
|
220
|
+
|
221
|
+
loop do
|
222
|
+
status =
|
223
|
+
case @state
|
224
|
+
when :FAST_FORWARD
|
225
|
+
handle_fast_forward
|
226
|
+
when :CONSUME_TOKEN
|
227
|
+
handle_consume_token
|
228
|
+
when :MIME_HEAD
|
229
|
+
handle_mime_head
|
230
|
+
when :MIME_BODY
|
231
|
+
handle_mime_body
|
232
|
+
else # when :DONE
|
233
|
+
return
|
234
|
+
end
|
235
|
+
|
236
|
+
read_data(io, outbuf) if status == :want_read
|
237
|
+
end
|
203
238
|
end
|
204
239
|
|
205
240
|
def result
|
206
241
|
@collector.each do |part|
|
207
242
|
part.get_data do |data|
|
208
243
|
tag_multipart_encoding(part.filename, part.content_type, part.name, data)
|
209
|
-
@query_parser.normalize_params(@params, part.name, data
|
244
|
+
@query_parser.normalize_params(@params, part.name, data)
|
210
245
|
end
|
211
246
|
end
|
212
247
|
MultipartInfo.new @params.to_params_hash, @collector.find_all(&:file?).map(&:body)
|
@@ -214,34 +249,45 @@ module Rack
|
|
214
249
|
|
215
250
|
private
|
216
251
|
|
217
|
-
def
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
break if handle_mime_body == :want_read
|
228
|
-
when :DONE
|
229
|
-
break
|
230
|
-
end
|
231
|
-
end
|
252
|
+
def dequote(str) # From WEBrick::HTTPUtils
|
253
|
+
ret = (/\A"(.*)"\Z/ =~ str) ? $1 : str.dup
|
254
|
+
ret.gsub!(/\\(.)/, "\\1")
|
255
|
+
ret
|
256
|
+
end
|
257
|
+
|
258
|
+
def read_data(io, outbuf)
|
259
|
+
content = io.read(@bufsize, outbuf)
|
260
|
+
handle_empty_content!(content)
|
261
|
+
@sbuf.concat(content)
|
232
262
|
end
|
233
263
|
|
264
|
+
# This handles the initial parser state. We read until we find the starting
|
265
|
+
# boundary, then we can transition to the next state. If we find the ending
|
266
|
+
# boundary, this is an invalid multipart upload, but keep scanning for opening
|
267
|
+
# boundary in that case. If no boundary found, we need to keep reading data
|
268
|
+
# and retry. It's highly unlikely the initial read will not consume the
|
269
|
+
# boundary. The client would have to deliberately craft a response
|
270
|
+
# with the opening boundary beyond the buffer size for that to happen.
|
234
271
|
def handle_fast_forward
|
235
|
-
|
272
|
+
while true
|
273
|
+
case consume_boundary
|
274
|
+
when :BOUNDARY
|
275
|
+
# found opening boundary, transition to next state
|
276
|
+
@state = :MIME_HEAD
|
277
|
+
return
|
278
|
+
when :END_BOUNDARY
|
279
|
+
# invalid multipart upload
|
280
|
+
if @sbuf.pos == @end_boundary_size && @sbuf.rest == EOL
|
281
|
+
# stop parsing a buffer if a buffer is only an end boundary.
|
282
|
+
@state = :DONE
|
283
|
+
return
|
284
|
+
end
|
236
285
|
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
else
|
243
|
-
raise EOFError, "bad content body" if @sbuf.rest_size >= @bufsize
|
244
|
-
:want_read
|
286
|
+
# retry for opening boundary
|
287
|
+
else
|
288
|
+
# no boundary found, keep reading data
|
289
|
+
return :want_read
|
290
|
+
end
|
245
291
|
end
|
246
292
|
end
|
247
293
|
|
@@ -255,17 +301,101 @@ module Rack
|
|
255
301
|
end
|
256
302
|
end
|
257
303
|
|
304
|
+
CONTENT_DISPOSITION_MAX_PARAMS = 16
|
305
|
+
CONTENT_DISPOSITION_MAX_BYTES = 1536
|
258
306
|
def handle_mime_head
|
259
307
|
if @sbuf.scan_until(@head_regex)
|
260
308
|
head = @sbuf[1]
|
261
309
|
content_type = head[MULTIPART_CONTENT_TYPE, 1]
|
262
|
-
if
|
263
|
-
|
310
|
+
if (disposition = head[MULTIPART_CONTENT_DISPOSITION, 1]) &&
|
311
|
+
disposition.bytesize <= CONTENT_DISPOSITION_MAX_BYTES
|
312
|
+
|
313
|
+
# ignore actual content-disposition value (should always be form-data)
|
314
|
+
i = disposition.index(';')
|
315
|
+
disposition.slice!(0, i+1)
|
316
|
+
param = nil
|
317
|
+
num_params = 0
|
318
|
+
|
319
|
+
# Parse parameter list
|
320
|
+
while i = disposition.index('=')
|
321
|
+
# Only parse up to max parameters, to avoid potential denial of service
|
322
|
+
num_params += 1
|
323
|
+
break if num_params > CONTENT_DISPOSITION_MAX_PARAMS
|
324
|
+
|
325
|
+
# Found end of parameter name, ensure forward progress in loop
|
326
|
+
param = disposition.slice!(0, i+1)
|
327
|
+
|
328
|
+
# Remove ending equals and preceding whitespace from parameter name
|
329
|
+
param.chomp!('=')
|
330
|
+
param.lstrip!
|
331
|
+
|
332
|
+
if disposition[0] == '"'
|
333
|
+
# Parameter value is quoted, parse it, handling backslash escapes
|
334
|
+
disposition.slice!(0, 1)
|
335
|
+
value = String.new
|
336
|
+
|
337
|
+
while i = disposition.index(/(["\\])/)
|
338
|
+
c = $1
|
339
|
+
|
340
|
+
# Append all content until ending quote or escape
|
341
|
+
value << disposition.slice!(0, i)
|
342
|
+
|
343
|
+
# Remove either backslash or ending quote,
|
344
|
+
# ensures forward progress in loop
|
345
|
+
disposition.slice!(0, 1)
|
346
|
+
|
347
|
+
# stop parsing parameter value if found ending quote
|
348
|
+
break if c == '"'
|
349
|
+
|
350
|
+
escaped_char = disposition.slice!(0, 1)
|
351
|
+
if param == 'filename' && escaped_char != '"'
|
352
|
+
# Possible IE uploaded filename, append both escape backslash and value
|
353
|
+
value << c << escaped_char
|
354
|
+
else
|
355
|
+
# Other only append escaped value
|
356
|
+
value << escaped_char
|
357
|
+
end
|
358
|
+
end
|
359
|
+
else
|
360
|
+
if i = disposition.index(';')
|
361
|
+
# Parameter value unquoted (which may be invalid), value ends at semicolon
|
362
|
+
value = disposition.slice!(0, i)
|
363
|
+
else
|
364
|
+
# If no ending semicolon, assume remainder of line is value and stop
|
365
|
+
# parsing
|
366
|
+
disposition.strip!
|
367
|
+
value = disposition
|
368
|
+
disposition = ''
|
369
|
+
end
|
370
|
+
end
|
371
|
+
|
372
|
+
case param
|
373
|
+
when 'name'
|
374
|
+
name = value
|
375
|
+
when 'filename'
|
376
|
+
filename = value
|
377
|
+
when 'filename*'
|
378
|
+
filename_star = value
|
379
|
+
# else
|
380
|
+
# ignore other parameters
|
381
|
+
end
|
382
|
+
|
383
|
+
# skip trailing semicolon, to proceed to next parameter
|
384
|
+
if i = disposition.index(';')
|
385
|
+
disposition.slice!(0, i+1)
|
386
|
+
end
|
387
|
+
end
|
264
388
|
else
|
265
389
|
name = head[MULTIPART_CONTENT_ID, 1]
|
266
390
|
end
|
267
391
|
|
268
|
-
|
392
|
+
if filename_star
|
393
|
+
encoding, _, filename = filename_star.split("'", 3)
|
394
|
+
filename = normalize_filename(filename || '')
|
395
|
+
filename.force_encoding(find_encoding(encoding))
|
396
|
+
elsif filename
|
397
|
+
filename = normalize_filename(filename)
|
398
|
+
end
|
269
399
|
|
270
400
|
if name.nil? || name.empty?
|
271
401
|
name = filename || "#{content_type || TEXT_PLAIN}[]".dup
|
@@ -280,7 +410,7 @@ module Rack
|
|
280
410
|
|
281
411
|
def handle_mime_body
|
282
412
|
if (body_with_boundary = @sbuf.check_until(@body_regex)) # check but do not advance the pointer yet
|
283
|
-
body = body_with_boundary.sub(
|
413
|
+
body = body_with_boundary.sub(@body_regex_at_end, '') # remove the boundary from the string
|
284
414
|
@collector.on_mime_body @mime_index, body
|
285
415
|
@sbuf.pos += body.length + 2 # skip \r\n after the content
|
286
416
|
@state = :CONSUME_TOKEN
|
@@ -297,54 +427,31 @@ module Rack
|
|
297
427
|
end
|
298
428
|
end
|
299
429
|
|
300
|
-
|
301
|
-
|
430
|
+
# Scan until the we find the start or end of the boundary.
|
431
|
+
# If we find it, return the appropriate symbol for the start or
|
432
|
+
# end of the boundary. If we don't find the start or end of the
|
433
|
+
# boundary, clear the buffer and return nil.
|
302
434
|
def consume_boundary
|
303
|
-
|
304
|
-
|
305
|
-
|
306
|
-
|
307
|
-
|
308
|
-
return if @sbuf.eos?
|
435
|
+
if read_buffer = @sbuf.scan_until(@body_regex)
|
436
|
+
read_buffer.end_with?(EOL) ? :BOUNDARY : :END_BOUNDARY
|
437
|
+
else
|
438
|
+
@sbuf.terminate
|
439
|
+
nil
|
309
440
|
end
|
310
441
|
end
|
311
442
|
|
312
|
-
def
|
313
|
-
filename = nil
|
314
|
-
case head
|
315
|
-
when RFC2183
|
316
|
-
params = Hash[*head.scan(DISPPARM).flat_map(&:compact)]
|
317
|
-
|
318
|
-
if filename = params['filename']
|
319
|
-
filename = $1 if filename =~ /^"(.*)"$/
|
320
|
-
elsif filename = params['filename*']
|
321
|
-
encoding, _, filename = filename.split("'", 3)
|
322
|
-
end
|
323
|
-
when BROKEN
|
324
|
-
filename = $1
|
325
|
-
filename = $1 if filename =~ /^"(.*)"$/
|
326
|
-
end
|
327
|
-
|
328
|
-
return unless filename
|
329
|
-
|
443
|
+
def normalize_filename(filename)
|
330
444
|
if filename.scan(/%.?.?/).all? { |s| /%[0-9a-fA-F]{2}/.match?(s) }
|
331
445
|
filename = Utils.unescape_path(filename)
|
332
446
|
end
|
333
447
|
|
334
448
|
filename.scrub!
|
335
449
|
|
336
|
-
|
337
|
-
filename = filename.gsub(/\\(.)/, '\1')
|
338
|
-
end
|
339
|
-
|
340
|
-
if encoding
|
341
|
-
filename.force_encoding ::Encoding.find(encoding)
|
342
|
-
end
|
343
|
-
|
344
|
-
filename
|
450
|
+
filename.split(/[\/\\]/).last || String.new
|
345
451
|
end
|
346
452
|
|
347
453
|
CHARSET = "charset"
|
454
|
+
deprecate_constant :CHARSET
|
348
455
|
|
349
456
|
def tag_multipart_encoding(filename, content_type, name, body)
|
350
457
|
name = name.to_s
|
@@ -365,7 +472,9 @@ module Rack
|
|
365
472
|
k.strip!
|
366
473
|
v.strip!
|
367
474
|
v = v[1..-2] if v.start_with?('"') && v.end_with?('"')
|
368
|
-
|
475
|
+
if k == "charset"
|
476
|
+
encoding = find_encoding(v)
|
477
|
+
end
|
369
478
|
end
|
370
479
|
end
|
371
480
|
end
|
@@ -374,9 +483,18 @@ module Rack
|
|
374
483
|
body.force_encoding(encoding)
|
375
484
|
end
|
376
485
|
|
486
|
+
# Return the related Encoding object. However, because
|
487
|
+
# enc is submitted by the user, it may be invalid, so
|
488
|
+
# use a binary encoding in that case.
|
489
|
+
def find_encoding(enc)
|
490
|
+
Encoding.find enc
|
491
|
+
rescue ArgumentError
|
492
|
+
Encoding::BINARY
|
493
|
+
end
|
494
|
+
|
377
495
|
def handle_empty_content!(content)
|
378
496
|
if content.nil? || content.empty?
|
379
|
-
raise
|
497
|
+
raise EmptyContentError
|
380
498
|
end
|
381
499
|
end
|
382
500
|
end
|