rack 2.2.6 → 3.1.2
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 +295 -72
- data/CONTRIBUTING.md +63 -55
- data/MIT-LICENSE +1 -1
- data/README.md +328 -0
- data/SPEC.rdoc +213 -136
- 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 +864 -681
- data/lib/rack/lock.rb +2 -5
- data/lib/rack/logger.rb +3 -0
- data/lib/rack/media_type.rb +9 -4
- 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 +171 -0
- data/lib/rack/mock_response.rb +124 -0
- data/lib/rack/multipart/generator.rb +7 -5
- data/lib/rack/multipart/parser.rb +232 -94
- 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 +246 -121
- data/lib/rack/response.rb +146 -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 +247 -244
- data/lib/rack/version.rb +1 -9
- data/lib/rack.rb +13 -89
- metadata +15 -41
- data/README.rdoc +0 -306
- 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 -203
- 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
|
+
if headers.has_key? 'set-cookie'
|
82
|
+
set_cookie_header = headers.fetch('set-cookie')
|
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,20 +2,48 @@
|
|
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
|
10
|
+
class MultipartPartLimitError < Errno::EMFILE
|
11
|
+
include BadRequest
|
12
|
+
end
|
8
13
|
|
9
|
-
class
|
10
|
-
|
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
|
29
|
+
|
30
|
+
# Prefer to use the BoundaryTooLongError class or Rack::BadRequest.
|
31
|
+
Error = BoundaryTooLongError
|
11
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
|
12
40
|
BUFSIZE = 1_048_576
|
13
41
|
TEXT_PLAIN = "text/plain"
|
14
42
|
TEMPFILE_FACTORY = lambda { |filename, content_type|
|
15
|
-
|
16
|
-
}
|
43
|
+
extension = ::File.extname(filename.gsub("\0", '%00'))[0, 129]
|
17
44
|
|
18
|
-
|
45
|
+
Tempfile.new(["RackMultipart", extension])
|
46
|
+
}
|
19
47
|
|
20
48
|
class BoundedIO # :nodoc:
|
21
49
|
def initialize(io, content_length)
|
@@ -38,16 +66,12 @@ module Rack
|
|
38
66
|
if str
|
39
67
|
@cursor += str.bytesize
|
40
68
|
else
|
41
|
-
# Raise an error for mismatching
|
69
|
+
# Raise an error for mismatching content-length and actual contents
|
42
70
|
raise EOFError, "bad content body"
|
43
71
|
end
|
44
72
|
|
45
73
|
str
|
46
74
|
end
|
47
|
-
|
48
|
-
def rewind
|
49
|
-
@io.rewind
|
50
|
-
end
|
51
75
|
end
|
52
76
|
|
53
77
|
MultipartInfo = Struct.new :params, :tmp_files
|
@@ -66,18 +90,17 @@ module Rack
|
|
66
90
|
boundary = parse_boundary content_type
|
67
91
|
return EMPTY unless boundary
|
68
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
|
+
|
69
99
|
io = BoundedIO.new(io, content_length) if content_length
|
70
|
-
outbuf = String.new
|
71
100
|
|
72
101
|
parser = new(boundary, tmpfile, bufsize, qp)
|
73
|
-
parser.
|
102
|
+
parser.parse(io)
|
74
103
|
|
75
|
-
loop do
|
76
|
-
break if parser.state == :DONE
|
77
|
-
parser.on_read io.read(bufsize, outbuf)
|
78
|
-
end
|
79
|
-
|
80
|
-
io.rewind
|
81
104
|
parser.result
|
82
105
|
end
|
83
106
|
|
@@ -140,7 +163,7 @@ module Rack
|
|
140
163
|
|
141
164
|
@mime_parts[mime_index] = klass.new(body, head, filename, content_type, name)
|
142
165
|
|
143
|
-
|
166
|
+
check_part_limits
|
144
167
|
end
|
145
168
|
|
146
169
|
def on_mime_body(mime_index, content)
|
@@ -152,13 +175,23 @@ module Rack
|
|
152
175
|
|
153
176
|
private
|
154
177
|
|
155
|
-
def
|
156
|
-
|
157
|
-
|
178
|
+
def check_part_limits
|
179
|
+
file_limit = Utils.multipart_file_limit
|
180
|
+
part_limit = Utils.multipart_total_part_limit
|
181
|
+
|
182
|
+
if file_limit && file_limit > 0
|
183
|
+
if @open_files >= file_limit
|
158
184
|
@mime_parts.each(&:close)
|
159
185
|
raise MultipartPartLimitError, 'Maximum file multiparts in content reached'
|
160
186
|
end
|
161
187
|
end
|
188
|
+
|
189
|
+
if part_limit && part_limit > 0
|
190
|
+
if @mime_parts.size >= part_limit
|
191
|
+
@mime_parts.each(&:close)
|
192
|
+
raise MultipartTotalPartLimitError, 'Maximum total multiparts in content reached'
|
193
|
+
end
|
194
|
+
end
|
162
195
|
end
|
163
196
|
end
|
164
197
|
|
@@ -167,32 +200,48 @@ module Rack
|
|
167
200
|
def initialize(boundary, tempfile, bufsize, query_parser)
|
168
201
|
@query_parser = query_parser
|
169
202
|
@params = query_parser.make_params
|
170
|
-
@boundary = "--#{boundary}"
|
171
203
|
@bufsize = bufsize
|
172
204
|
|
173
|
-
@full_boundary = @boundary
|
174
|
-
@end_boundary = @boundary + '--'
|
175
205
|
@state = :FAST_FORWARD
|
176
206
|
@mime_index = 0
|
177
207
|
@collector = Collector.new tempfile
|
178
208
|
|
179
209
|
@sbuf = StringScanner.new("".dup)
|
180
|
-
@body_regex = /(?:#{EOL})
|
181
|
-
@
|
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)
|
182
214
|
@head_regex = /(.*?#{EOL})#{EOL}/m
|
183
215
|
end
|
184
216
|
|
185
|
-
def
|
186
|
-
|
187
|
-
|
188
|
-
|
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
|
189
238
|
end
|
190
239
|
|
191
240
|
def result
|
192
241
|
@collector.each do |part|
|
193
242
|
part.get_data do |data|
|
194
243
|
tag_multipart_encoding(part.filename, part.content_type, part.name, data)
|
195
|
-
@query_parser.normalize_params(@params, part.name, data
|
244
|
+
@query_parser.normalize_params(@params, part.name, data)
|
196
245
|
end
|
197
246
|
end
|
198
247
|
MultipartInfo.new @params.to_params_hash, @collector.find_all(&:file?).map(&:body)
|
@@ -200,29 +249,45 @@ module Rack
|
|
200
249
|
|
201
250
|
private
|
202
251
|
|
203
|
-
def
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
break if handle_fast_forward == :want_read
|
208
|
-
when :CONSUME_TOKEN
|
209
|
-
break if handle_consume_token == :want_read
|
210
|
-
when :MIME_HEAD
|
211
|
-
break if handle_mime_head == :want_read
|
212
|
-
when :MIME_BODY
|
213
|
-
break if handle_mime_body == :want_read
|
214
|
-
when :DONE
|
215
|
-
break
|
216
|
-
end
|
217
|
-
end
|
252
|
+
def dequote(str) # From WEBrick::HTTPUtils
|
253
|
+
ret = (/\A"(.*)"\Z/ =~ str) ? $1 : str.dup
|
254
|
+
ret.gsub!(/\\(.)/, "\\1")
|
255
|
+
ret
|
218
256
|
end
|
219
257
|
|
258
|
+
def read_data(io, outbuf)
|
259
|
+
content = io.read(@bufsize, outbuf)
|
260
|
+
handle_empty_content!(content)
|
261
|
+
@sbuf.concat(content)
|
262
|
+
end
|
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.
|
220
271
|
def handle_fast_forward
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
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
|
285
|
+
|
286
|
+
# retry for opening boundary
|
287
|
+
else
|
288
|
+
# no boundary found, keep reading data
|
289
|
+
return :want_read
|
290
|
+
end
|
226
291
|
end
|
227
292
|
end
|
228
293
|
|
@@ -236,17 +301,102 @@ module Rack
|
|
236
301
|
end
|
237
302
|
end
|
238
303
|
|
304
|
+
CONTENT_DISPOSITION_MAX_PARAMS = 16
|
305
|
+
CONTENT_DISPOSITION_MAX_BYTES = 1536
|
239
306
|
def handle_mime_head
|
240
307
|
if @sbuf.scan_until(@head_regex)
|
241
308
|
head = @sbuf[1]
|
242
309
|
content_type = head[MULTIPART_CONTENT_TYPE, 1]
|
243
|
-
if
|
244
|
-
|
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
|
245
388
|
else
|
246
389
|
name = head[MULTIPART_CONTENT_ID, 1]
|
247
390
|
end
|
248
391
|
|
249
|
-
|
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 = $1 if filename =~ /^"(.*)"$/
|
398
|
+
filename = normalize_filename(filename)
|
399
|
+
end
|
250
400
|
|
251
401
|
if name.nil? || name.empty?
|
252
402
|
name = filename || "#{content_type || TEXT_PLAIN}[]".dup
|
@@ -261,7 +411,7 @@ module Rack
|
|
261
411
|
|
262
412
|
def handle_mime_body
|
263
413
|
if (body_with_boundary = @sbuf.check_until(@body_regex)) # check but do not advance the pointer yet
|
264
|
-
body = body_with_boundary.sub(
|
414
|
+
body = body_with_boundary.sub(@body_regex_at_end, '') # remove the boundary from the string
|
265
415
|
@collector.on_mime_body @mime_index, body
|
266
416
|
@sbuf.pos += body.length + 2 # skip \r\n after the content
|
267
417
|
@state = :CONSUME_TOKEN
|
@@ -278,54 +428,31 @@ module Rack
|
|
278
428
|
end
|
279
429
|
end
|
280
430
|
|
281
|
-
|
282
|
-
|
431
|
+
# Scan until the we find the start or end of the boundary.
|
432
|
+
# If we find it, return the appropriate symbol for the start or
|
433
|
+
# end of the boundary. If we don't find the start or end of the
|
434
|
+
# boundary, clear the buffer and return nil.
|
283
435
|
def consume_boundary
|
284
|
-
|
285
|
-
|
286
|
-
|
287
|
-
|
288
|
-
|
289
|
-
return if @sbuf.eos?
|
436
|
+
if read_buffer = @sbuf.scan_until(@body_regex)
|
437
|
+
read_buffer.end_with?(EOL) ? :BOUNDARY : :END_BOUNDARY
|
438
|
+
else
|
439
|
+
@sbuf.terminate
|
440
|
+
nil
|
290
441
|
end
|
291
442
|
end
|
292
443
|
|
293
|
-
def
|
294
|
-
filename = nil
|
295
|
-
case head
|
296
|
-
when RFC2183
|
297
|
-
params = Hash[*head.scan(DISPPARM).flat_map(&:compact)]
|
298
|
-
|
299
|
-
if filename = params['filename']
|
300
|
-
filename = $1 if filename =~ /^"(.*)"$/
|
301
|
-
elsif filename = params['filename*']
|
302
|
-
encoding, _, filename = filename.split("'", 3)
|
303
|
-
end
|
304
|
-
when BROKEN
|
305
|
-
filename = $1
|
306
|
-
filename = $1 if filename =~ /^"(.*)"$/
|
307
|
-
end
|
308
|
-
|
309
|
-
return unless filename
|
310
|
-
|
444
|
+
def normalize_filename(filename)
|
311
445
|
if filename.scan(/%.?.?/).all? { |s| /%[0-9a-fA-F]{2}/.match?(s) }
|
312
446
|
filename = Utils.unescape_path(filename)
|
313
447
|
end
|
314
448
|
|
315
449
|
filename.scrub!
|
316
450
|
|
317
|
-
|
318
|
-
filename = filename.gsub(/\\(.)/, '\1')
|
319
|
-
end
|
320
|
-
|
321
|
-
if encoding
|
322
|
-
filename.force_encoding ::Encoding.find(encoding)
|
323
|
-
end
|
324
|
-
|
325
|
-
filename
|
451
|
+
filename.split(/[\/\\]/).last || String.new
|
326
452
|
end
|
327
453
|
|
328
454
|
CHARSET = "charset"
|
455
|
+
deprecate_constant :CHARSET
|
329
456
|
|
330
457
|
def tag_multipart_encoding(filename, content_type, name, body)
|
331
458
|
name = name.to_s
|
@@ -346,7 +473,9 @@ module Rack
|
|
346
473
|
k.strip!
|
347
474
|
v.strip!
|
348
475
|
v = v[1..-2] if v.start_with?('"') && v.end_with?('"')
|
349
|
-
|
476
|
+
if k == "charset"
|
477
|
+
encoding = find_encoding(v)
|
478
|
+
end
|
350
479
|
end
|
351
480
|
end
|
352
481
|
end
|
@@ -355,9 +484,18 @@ module Rack
|
|
355
484
|
body.force_encoding(encoding)
|
356
485
|
end
|
357
486
|
|
487
|
+
# Return the related Encoding object. However, because
|
488
|
+
# enc is submitted by the user, it may be invalid, so
|
489
|
+
# use a binary encoding in that case.
|
490
|
+
def find_encoding(enc)
|
491
|
+
Encoding.find enc
|
492
|
+
rescue ArgumentError
|
493
|
+
Encoding::BINARY
|
494
|
+
end
|
495
|
+
|
358
496
|
def handle_empty_content!(content)
|
359
497
|
if content.nil? || content.empty?
|
360
|
-
raise
|
498
|
+
raise EmptyContentError
|
361
499
|
end
|
362
500
|
end
|
363
501
|
end
|