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