rack 2.2.17 → 3.2.0
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 +501 -70
- data/CONTRIBUTING.md +63 -55
- data/MIT-LICENSE +1 -1
- data/README.md +376 -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 +4 -0
- 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 +241 -95
- 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 +269 -141
- 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 +30 -25
- 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 +15 -41
- data/README.rdoc +0 -347
- 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 -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/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
|
class BoundedIO # :nodoc:
|
24
63
|
def initialize(io, content_length)
|
25
64
|
@io = io
|
@@ -41,16 +80,12 @@ module Rack
|
|
41
80
|
if str
|
42
81
|
@cursor += str.bytesize
|
43
82
|
else
|
44
|
-
# Raise an error for mismatching
|
83
|
+
# Raise an error for mismatching content-length and actual contents
|
45
84
|
raise EOFError, "bad content body"
|
46
85
|
end
|
47
86
|
|
48
87
|
str
|
49
88
|
end
|
50
|
-
|
51
|
-
def rewind
|
52
|
-
@io.rewind
|
53
|
-
end
|
54
89
|
end
|
55
90
|
|
56
91
|
MultipartInfo = Struct.new :params, :tmp_files
|
@@ -69,18 +104,17 @@ module Rack
|
|
69
104
|
boundary = parse_boundary content_type
|
70
105
|
return EMPTY unless boundary
|
71
106
|
|
107
|
+
if boundary.length > 70
|
108
|
+
# RFC 1521 Section 7.2.1 imposes a 70 character maximum for the boundary.
|
109
|
+
# Most clients use no more than 55 characters.
|
110
|
+
raise BoundaryTooLongError, "multipart boundary size too large (#{boundary.length} characters)"
|
111
|
+
end
|
112
|
+
|
72
113
|
io = BoundedIO.new(io, content_length) if content_length
|
73
|
-
outbuf = String.new
|
74
114
|
|
75
115
|
parser = new(boundary, tmpfile, bufsize, qp)
|
76
|
-
parser.
|
116
|
+
parser.parse(io)
|
77
117
|
|
78
|
-
loop do
|
79
|
-
break if parser.state == :DONE
|
80
|
-
parser.on_read io.read(bufsize, outbuf)
|
81
|
-
end
|
82
|
-
|
83
|
-
io.rewind
|
84
118
|
parser.result
|
85
119
|
end
|
86
120
|
|
@@ -180,33 +214,49 @@ module Rack
|
|
180
214
|
def initialize(boundary, tempfile, bufsize, query_parser)
|
181
215
|
@query_parser = query_parser
|
182
216
|
@params = query_parser.make_params
|
183
|
-
@boundary = "--#{boundary}"
|
184
217
|
@bufsize = bufsize
|
185
218
|
|
186
|
-
@full_boundary = @boundary
|
187
|
-
@end_boundary = @boundary + '--'
|
188
219
|
@state = :FAST_FORWARD
|
189
220
|
@mime_index = 0
|
190
221
|
@collector = Collector.new tempfile
|
191
222
|
|
192
223
|
@sbuf = StringScanner.new("".dup)
|
193
|
-
@body_regex = /(?:#{EOL})
|
194
|
-
@
|
195
|
-
@
|
224
|
+
@body_regex = /(?:#{EOL}|\A)--#{Regexp.quote(boundary)}(?:#{EOL}|--)/m
|
225
|
+
@body_regex_at_end = /#{@body_regex}\z/m
|
226
|
+
@end_boundary_size = boundary.bytesize + 4 # (-- at start, -- at finish)
|
227
|
+
@rx_max_size = boundary.bytesize + 6 # (\r\n-- at start, either \r\n or -- at finish)
|
196
228
|
@head_regex = /(.*?#{EOL})#{EOL}/m
|
197
229
|
end
|
198
230
|
|
199
|
-
def
|
200
|
-
|
201
|
-
|
202
|
-
|
231
|
+
def parse(io)
|
232
|
+
outbuf = String.new
|
233
|
+
read_data(io, outbuf)
|
234
|
+
|
235
|
+
loop do
|
236
|
+
status =
|
237
|
+
case @state
|
238
|
+
when :FAST_FORWARD
|
239
|
+
handle_fast_forward
|
240
|
+
when :CONSUME_TOKEN
|
241
|
+
handle_consume_token
|
242
|
+
when :MIME_HEAD
|
243
|
+
handle_mime_head
|
244
|
+
when :MIME_BODY
|
245
|
+
handle_mime_body
|
246
|
+
else # when :DONE
|
247
|
+
return
|
248
|
+
end
|
249
|
+
|
250
|
+
read_data(io, outbuf) if status == :want_read
|
251
|
+
end
|
203
252
|
end
|
204
253
|
|
205
254
|
def result
|
206
255
|
@collector.each do |part|
|
207
256
|
part.get_data do |data|
|
208
257
|
tag_multipart_encoding(part.filename, part.content_type, part.name, data)
|
209
|
-
|
258
|
+
name, data = handle_dummy_encoding(part.name, data)
|
259
|
+
@query_parser.normalize_params(@params, name, data)
|
210
260
|
end
|
211
261
|
end
|
212
262
|
MultipartInfo.new @params.to_params_hash, @collector.find_all(&:file?).map(&:body)
|
@@ -214,34 +264,39 @@ module Rack
|
|
214
264
|
|
215
265
|
private
|
216
266
|
|
217
|
-
def
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
break if handle_fast_forward == :want_read
|
222
|
-
when :CONSUME_TOKEN
|
223
|
-
break if handle_consume_token == :want_read
|
224
|
-
when :MIME_HEAD
|
225
|
-
break if handle_mime_head == :want_read
|
226
|
-
when :MIME_BODY
|
227
|
-
break if handle_mime_body == :want_read
|
228
|
-
when :DONE
|
229
|
-
break
|
230
|
-
end
|
231
|
-
end
|
267
|
+
def read_data(io, outbuf)
|
268
|
+
content = io.read(@bufsize, outbuf)
|
269
|
+
handle_empty_content!(content)
|
270
|
+
@sbuf.concat(content)
|
232
271
|
end
|
233
272
|
|
273
|
+
# This handles the initial parser state. We read until we find the starting
|
274
|
+
# boundary, then we can transition to the next state. If we find the ending
|
275
|
+
# boundary, this is an invalid multipart upload, but keep scanning for opening
|
276
|
+
# boundary in that case. If no boundary found, we need to keep reading data
|
277
|
+
# and retry. It's highly unlikely the initial read will not consume the
|
278
|
+
# boundary. The client would have to deliberately craft a response
|
279
|
+
# with the opening boundary beyond the buffer size for that to happen.
|
234
280
|
def handle_fast_forward
|
235
|
-
|
281
|
+
while true
|
282
|
+
case consume_boundary
|
283
|
+
when :BOUNDARY
|
284
|
+
# found opening boundary, transition to next state
|
285
|
+
@state = :MIME_HEAD
|
286
|
+
return
|
287
|
+
when :END_BOUNDARY
|
288
|
+
# invalid multipart upload
|
289
|
+
if @sbuf.pos == @end_boundary_size && @sbuf.rest == EOL
|
290
|
+
# stop parsing a buffer if a buffer is only an end boundary.
|
291
|
+
@state = :DONE
|
292
|
+
return
|
293
|
+
end
|
236
294
|
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
else
|
243
|
-
raise EOFError, "bad content body" if @sbuf.rest_size >= @bufsize
|
244
|
-
:want_read
|
295
|
+
# retry for opening boundary
|
296
|
+
else
|
297
|
+
# no boundary found, keep reading data
|
298
|
+
return :want_read
|
299
|
+
end
|
245
300
|
end
|
246
301
|
end
|
247
302
|
|
@@ -255,17 +310,101 @@ module Rack
|
|
255
310
|
end
|
256
311
|
end
|
257
312
|
|
313
|
+
CONTENT_DISPOSITION_MAX_PARAMS = 16
|
314
|
+
CONTENT_DISPOSITION_MAX_BYTES = 1536
|
258
315
|
def handle_mime_head
|
259
316
|
if @sbuf.scan_until(@head_regex)
|
260
317
|
head = @sbuf[1]
|
261
318
|
content_type = head[MULTIPART_CONTENT_TYPE, 1]
|
262
|
-
if
|
263
|
-
|
319
|
+
if (disposition = head[MULTIPART_CONTENT_DISPOSITION, 1]) &&
|
320
|
+
disposition.bytesize <= CONTENT_DISPOSITION_MAX_BYTES
|
321
|
+
|
322
|
+
# ignore actual content-disposition value (should always be form-data)
|
323
|
+
i = disposition.index(';')
|
324
|
+
disposition.slice!(0, i+1)
|
325
|
+
param = nil
|
326
|
+
num_params = 0
|
327
|
+
|
328
|
+
# Parse parameter list
|
329
|
+
while i = disposition.index('=')
|
330
|
+
# Only parse up to max parameters, to avoid potential denial of service
|
331
|
+
num_params += 1
|
332
|
+
break if num_params > CONTENT_DISPOSITION_MAX_PARAMS
|
333
|
+
|
334
|
+
# Found end of parameter name, ensure forward progress in loop
|
335
|
+
param = disposition.slice!(0, i+1)
|
336
|
+
|
337
|
+
# Remove ending equals and preceding whitespace from parameter name
|
338
|
+
param.chomp!('=')
|
339
|
+
param.lstrip!
|
340
|
+
|
341
|
+
if disposition[0] == '"'
|
342
|
+
# Parameter value is quoted, parse it, handling backslash escapes
|
343
|
+
disposition.slice!(0, 1)
|
344
|
+
value = String.new
|
345
|
+
|
346
|
+
while i = disposition.index(/(["\\])/)
|
347
|
+
c = $1
|
348
|
+
|
349
|
+
# Append all content until ending quote or escape
|
350
|
+
value << disposition.slice!(0, i)
|
351
|
+
|
352
|
+
# Remove either backslash or ending quote,
|
353
|
+
# ensures forward progress in loop
|
354
|
+
disposition.slice!(0, 1)
|
355
|
+
|
356
|
+
# stop parsing parameter value if found ending quote
|
357
|
+
break if c == '"'
|
358
|
+
|
359
|
+
escaped_char = disposition.slice!(0, 1)
|
360
|
+
if param == 'filename' && escaped_char != '"'
|
361
|
+
# Possible IE uploaded filename, append both escape backslash and value
|
362
|
+
value << c << escaped_char
|
363
|
+
else
|
364
|
+
# Other only append escaped value
|
365
|
+
value << escaped_char
|
366
|
+
end
|
367
|
+
end
|
368
|
+
else
|
369
|
+
if i = disposition.index(';')
|
370
|
+
# Parameter value unquoted (which may be invalid), value ends at semicolon
|
371
|
+
value = disposition.slice!(0, i)
|
372
|
+
else
|
373
|
+
# If no ending semicolon, assume remainder of line is value and stop
|
374
|
+
# parsing
|
375
|
+
disposition.strip!
|
376
|
+
value = disposition
|
377
|
+
disposition = ''
|
378
|
+
end
|
379
|
+
end
|
380
|
+
|
381
|
+
case param
|
382
|
+
when 'name'
|
383
|
+
name = value
|
384
|
+
when 'filename'
|
385
|
+
filename = value
|
386
|
+
when 'filename*'
|
387
|
+
filename_star = value
|
388
|
+
# else
|
389
|
+
# ignore other parameters
|
390
|
+
end
|
391
|
+
|
392
|
+
# skip trailing semicolon, to proceed to next parameter
|
393
|
+
if i = disposition.index(';')
|
394
|
+
disposition.slice!(0, i+1)
|
395
|
+
end
|
396
|
+
end
|
264
397
|
else
|
265
398
|
name = head[MULTIPART_CONTENT_ID, 1]
|
266
399
|
end
|
267
400
|
|
268
|
-
|
401
|
+
if filename_star
|
402
|
+
encoding, _, filename = filename_star.split("'", 3)
|
403
|
+
filename = normalize_filename(filename || '')
|
404
|
+
filename.force_encoding(find_encoding(encoding))
|
405
|
+
elsif filename
|
406
|
+
filename = normalize_filename(filename)
|
407
|
+
end
|
269
408
|
|
270
409
|
if name.nil? || name.empty?
|
271
410
|
name = filename || "#{content_type || TEXT_PLAIN}[]".dup
|
@@ -280,7 +419,7 @@ module Rack
|
|
280
419
|
|
281
420
|
def handle_mime_body
|
282
421
|
if (body_with_boundary = @sbuf.check_until(@body_regex)) # check but do not advance the pointer yet
|
283
|
-
body = body_with_boundary.sub(
|
422
|
+
body = body_with_boundary.sub(@body_regex_at_end, '') # remove the boundary from the string
|
284
423
|
@collector.on_mime_body @mime_index, body
|
285
424
|
@sbuf.pos += body.length + 2 # skip \r\n after the content
|
286
425
|
@state = :CONSUME_TOKEN
|
@@ -297,54 +436,31 @@ module Rack
|
|
297
436
|
end
|
298
437
|
end
|
299
438
|
|
300
|
-
|
301
|
-
|
439
|
+
# Scan until the we find the start or end of the boundary.
|
440
|
+
# If we find it, return the appropriate symbol for the start or
|
441
|
+
# end of the boundary. If we don't find the start or end of the
|
442
|
+
# boundary, clear the buffer and return nil.
|
302
443
|
def consume_boundary
|
303
|
-
|
304
|
-
|
305
|
-
|
306
|
-
|
307
|
-
|
308
|
-
return if @sbuf.eos?
|
444
|
+
if read_buffer = @sbuf.scan_until(@body_regex)
|
445
|
+
read_buffer.end_with?(EOL) ? :BOUNDARY : :END_BOUNDARY
|
446
|
+
else
|
447
|
+
@sbuf.terminate
|
448
|
+
nil
|
309
449
|
end
|
310
450
|
end
|
311
451
|
|
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
|
-
|
452
|
+
def normalize_filename(filename)
|
330
453
|
if filename.scan(/%.?.?/).all? { |s| /%[0-9a-fA-F]{2}/.match?(s) }
|
331
454
|
filename = Utils.unescape_path(filename)
|
332
455
|
end
|
333
456
|
|
334
457
|
filename.scrub!
|
335
458
|
|
336
|
-
|
337
|
-
filename = filename.gsub(/\\(.)/, '\1')
|
338
|
-
end
|
339
|
-
|
340
|
-
if encoding
|
341
|
-
filename.force_encoding ::Encoding.find(encoding)
|
342
|
-
end
|
343
|
-
|
344
|
-
filename
|
459
|
+
filename.split(/[\/\\]/).last || String.new
|
345
460
|
end
|
346
461
|
|
347
462
|
CHARSET = "charset"
|
463
|
+
deprecate_constant :CHARSET
|
348
464
|
|
349
465
|
def tag_multipart_encoding(filename, content_type, name, body)
|
350
466
|
name = name.to_s
|
@@ -365,7 +481,9 @@ module Rack
|
|
365
481
|
k.strip!
|
366
482
|
v.strip!
|
367
483
|
v = v[1..-2] if v.start_with?('"') && v.end_with?('"')
|
368
|
-
|
484
|
+
if k == "charset"
|
485
|
+
encoding = find_encoding(v)
|
486
|
+
end
|
369
487
|
end
|
370
488
|
end
|
371
489
|
end
|
@@ -374,9 +492,37 @@ module Rack
|
|
374
492
|
body.force_encoding(encoding)
|
375
493
|
end
|
376
494
|
|
495
|
+
# Return the related Encoding object. However, because
|
496
|
+
# enc is submitted by the user, it may be invalid, so
|
497
|
+
# use a binary encoding in that case.
|
498
|
+
def find_encoding(enc)
|
499
|
+
Encoding.find enc
|
500
|
+
rescue ArgumentError
|
501
|
+
Encoding::BINARY
|
502
|
+
end
|
503
|
+
|
504
|
+
REENCODE_DUMMY_ENCODINGS = {
|
505
|
+
# ISO-2022-JP is a legacy but still widely used encoding in Japan
|
506
|
+
# Here we convert ISO-2022-JP to UTF-8 so that it can be handled.
|
507
|
+
Encoding::ISO_2022_JP => true
|
508
|
+
|
509
|
+
# Other dummy encodings are rarely used and have not been supported yet.
|
510
|
+
# Adding support for them will require careful considerations.
|
511
|
+
}
|
512
|
+
|
513
|
+
def handle_dummy_encoding(name, body)
|
514
|
+
# A string object with a 'dummy' encoding does not have full functionality and can cause errors.
|
515
|
+
# So here we covert it to UTF-8 so that it can be handled properly.
|
516
|
+
if name.encoding.dummy? && REENCODE_DUMMY_ENCODINGS[name.encoding]
|
517
|
+
name = name.encode(Encoding::UTF_8)
|
518
|
+
body = body.encode(Encoding::UTF_8)
|
519
|
+
end
|
520
|
+
return name, body
|
521
|
+
end
|
522
|
+
|
377
523
|
def handle_empty_content!(content)
|
378
524
|
if content.nil? || content.empty?
|
379
|
-
raise
|
525
|
+
raise EmptyContentError
|
380
526
|
end
|
381
527
|
end
|
382
528
|
end
|