rack 2.1.0 → 3.1.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 +377 -16
- data/CONTRIBUTING.md +144 -0
- data/MIT-LICENSE +1 -1
- data/README.md +328 -0
- data/SPEC.rdoc +365 -0
- data/lib/rack/auth/abstract/handler.rb +3 -1
- data/lib/rack/auth/abstract/request.rb +2 -2
- data/lib/rack/auth/basic.rb +4 -7
- data/lib/rack/bad_request.rb +8 -0
- data/lib/rack/body_proxy.rb +34 -12
- data/lib/rack/builder.rb +162 -59
- data/lib/rack/cascade.rb +24 -10
- data/lib/rack/common_logger.rb +43 -28
- data/lib/rack/conditional_get.rb +30 -25
- data/lib/rack/constants.rb +66 -0
- data/lib/rack/content_length.rb +10 -16
- data/lib/rack/content_type.rb +9 -7
- data/lib/rack/deflater.rb +78 -50
- data/lib/rack/directory.rb +86 -63
- data/lib/rack/etag.rb +14 -22
- data/lib/rack/events.rb +18 -17
- data/lib/rack/files.rb +99 -61
- data/lib/rack/head.rb +8 -9
- data/lib/rack/headers.rb +238 -0
- data/lib/rack/lint.rb +868 -642
- data/lib/rack/lock.rb +2 -6
- data/lib/rack/logger.rb +3 -0
- data/lib/rack/media_type.rb +9 -4
- data/lib/rack/method_override.rb +6 -2
- data/lib/rack/mime.rb +14 -5
- data/lib/rack/mock.rb +1 -253
- data/lib/rack/mock_request.rb +171 -0
- data/lib/rack/mock_response.rb +124 -0
- data/lib/rack/multipart/generator.rb +15 -8
- data/lib/rack/multipart/parser.rb +238 -107
- data/lib/rack/multipart/uploaded_file.rb +17 -7
- data/lib/rack/multipart.rb +54 -42
- data/lib/rack/null_logger.rb +9 -0
- data/lib/rack/query_parser.rb +87 -105
- data/lib/rack/recursive.rb +3 -1
- data/lib/rack/reloader.rb +0 -4
- data/lib/rack/request.rb +366 -135
- data/lib/rack/response.rb +186 -68
- data/lib/rack/rewindable_input.rb +24 -6
- data/lib/rack/runtime.rb +8 -7
- data/lib/rack/sendfile.rb +29 -27
- data/lib/rack/show_exceptions.rb +27 -12
- data/lib/rack/show_status.rb +21 -13
- data/lib/rack/static.rb +19 -12
- data/lib/rack/tempfile_reaper.rb +14 -5
- data/lib/rack/urlmap.rb +5 -6
- data/lib/rack/utils.rb +274 -260
- data/lib/rack/version.rb +21 -0
- data/lib/rack.rb +18 -103
- metadata +25 -52
- data/README.rdoc +0 -262
- data/Rakefile +0 -123
- data/SPEC +0 -263
- 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 -92
- data/lib/rack/core_ext/regexp.rb +0 -14
- data/lib/rack/file.rb +0 -8
- data/lib/rack/handler/cgi.rb +0 -62
- data/lib/rack/handler/fastcgi.rb +0 -102
- data/lib/rack/handler/lsws.rb +0 -63
- data/lib/rack/handler/scgi.rb +0 -73
- data/lib/rack/handler/thin.rb +0 -38
- data/lib/rack/handler/webrick.rb +0 -122
- data/lib/rack/handler.rb +0 -104
- data/lib/rack/lobster.rb +0 -72
- data/lib/rack/server.rb +0 -467
- data/lib/rack/session/abstract/id.rb +0 -528
- data/lib/rack/session/cookie.rb +0 -205
- data/lib/rack/session/memcache.rb +0 -10
- data/lib/rack/session/pool.rb +0 -85
- data/rack.gemspec +0 -44
@@ -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
|
@@ -17,9 +19,13 @@ module Rack
|
|
17
19
|
|
18
20
|
flattened_params.map do |name, file|
|
19
21
|
if file.respond_to?(:original_filename)
|
20
|
-
|
21
|
-
|
22
|
-
|
22
|
+
if file.path
|
23
|
+
::File.open(file.path, 'rb') do |f|
|
24
|
+
f.set_encoding(Encoding::BINARY)
|
25
|
+
content_for_tempfile(f, file, name)
|
26
|
+
end
|
27
|
+
else
|
28
|
+
content_for_tempfile(file, file, name)
|
23
29
|
end
|
24
30
|
else
|
25
31
|
content_for_other(file, name)
|
@@ -69,12 +75,13 @@ module Rack
|
|
69
75
|
end
|
70
76
|
|
71
77
|
def content_for_tempfile(io, file, name)
|
78
|
+
length = ::File.stat(file.path).size if file.path
|
79
|
+
filename = "; filename=\"#{Utils.escape_path(file.original_filename)}\""
|
72
80
|
<<-EOF
|
73
81
|
--#{MULTIPART_BOUNDARY}\r
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
\r
|
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
|
78
85
|
#{io.read}\r
|
79
86
|
EOF
|
80
87
|
end
|
@@ -82,7 +89,7 @@ EOF
|
|
82
89
|
def content_for_other(file, name)
|
83
90
|
<<-EOF
|
84
91
|
--#{MULTIPART_BOUNDARY}\r
|
85
|
-
|
92
|
+
content-disposition: form-data; name="#{name}"\r
|
86
93
|
\r
|
87
94
|
#{file}\r
|
88
95
|
EOF
|
@@ -1,23 +1,49 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require 'rack/utils'
|
4
3
|
require 'strscan'
|
5
|
-
|
4
|
+
|
5
|
+
require_relative '../utils'
|
6
|
+
require_relative '../bad_request'
|
6
7
|
|
7
8
|
module Rack
|
8
9
|
module Multipart
|
9
|
-
class MultipartPartLimitError < Errno::EMFILE
|
10
|
+
class MultipartPartLimitError < Errno::EMFILE
|
11
|
+
include BadRequest
|
12
|
+
end
|
10
13
|
|
11
|
-
class
|
12
|
-
|
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
|
13
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
|
14
40
|
BUFSIZE = 1_048_576
|
15
41
|
TEXT_PLAIN = "text/plain"
|
16
42
|
TEMPFILE_FACTORY = lambda { |filename, content_type|
|
17
|
-
|
18
|
-
}
|
43
|
+
extension = ::File.extname(filename.gsub("\0", '%00'))[0, 129]
|
19
44
|
|
20
|
-
|
45
|
+
Tempfile.new(["RackMultipart", extension])
|
46
|
+
}
|
21
47
|
|
22
48
|
class BoundedIO # :nodoc:
|
23
49
|
def initialize(io, content_length)
|
@@ -40,16 +66,12 @@ module Rack
|
|
40
66
|
if str
|
41
67
|
@cursor += str.bytesize
|
42
68
|
else
|
43
|
-
# Raise an error for mismatching
|
69
|
+
# Raise an error for mismatching content-length and actual contents
|
44
70
|
raise EOFError, "bad content body"
|
45
71
|
end
|
46
72
|
|
47
73
|
str
|
48
74
|
end
|
49
|
-
|
50
|
-
def rewind
|
51
|
-
@io.rewind
|
52
|
-
end
|
53
75
|
end
|
54
76
|
|
55
77
|
MultipartInfo = Struct.new :params, :tmp_files
|
@@ -68,18 +90,17 @@ module Rack
|
|
68
90
|
boundary = parse_boundary content_type
|
69
91
|
return EMPTY unless boundary
|
70
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
|
+
|
71
99
|
io = BoundedIO.new(io, content_length) if content_length
|
72
|
-
outbuf = String.new
|
73
100
|
|
74
101
|
parser = new(boundary, tmpfile, bufsize, qp)
|
75
|
-
parser.
|
102
|
+
parser.parse(io)
|
76
103
|
|
77
|
-
loop do
|
78
|
-
break if parser.state == :DONE
|
79
|
-
parser.on_read io.read(bufsize, outbuf)
|
80
|
-
end
|
81
|
-
|
82
|
-
io.rewind
|
83
104
|
parser.result
|
84
105
|
end
|
85
106
|
|
@@ -101,12 +122,6 @@ module Rack
|
|
101
122
|
|
102
123
|
data = { filename: fn, type: content_type,
|
103
124
|
name: name, tempfile: body, head: head }
|
104
|
-
elsif !filename && content_type && body.is_a?(IO)
|
105
|
-
body.rewind
|
106
|
-
|
107
|
-
# Generic multipart cases, not coming from a form
|
108
|
-
data = { type: content_type,
|
109
|
-
name: name, tempfile: body, head: head }
|
110
125
|
end
|
111
126
|
|
112
127
|
yield data
|
@@ -125,7 +140,7 @@ module Rack
|
|
125
140
|
|
126
141
|
include Enumerable
|
127
142
|
|
128
|
-
def initialize
|
143
|
+
def initialize(tempfile)
|
129
144
|
@tempfile = tempfile
|
130
145
|
@mime_parts = []
|
131
146
|
@open_files = 0
|
@@ -135,7 +150,7 @@ module Rack
|
|
135
150
|
@mime_parts.each { |part| yield part }
|
136
151
|
end
|
137
152
|
|
138
|
-
def on_mime_head
|
153
|
+
def on_mime_head(mime_index, head, filename, content_type, name)
|
139
154
|
if filename
|
140
155
|
body = @tempfile.call(filename, content_type)
|
141
156
|
body.binmode if body.respond_to?(:binmode)
|
@@ -148,25 +163,35 @@ module Rack
|
|
148
163
|
|
149
164
|
@mime_parts[mime_index] = klass.new(body, head, filename, content_type, name)
|
150
165
|
|
151
|
-
|
166
|
+
check_part_limits
|
152
167
|
end
|
153
168
|
|
154
|
-
def on_mime_body
|
169
|
+
def on_mime_body(mime_index, content)
|
155
170
|
@mime_parts[mime_index].body << content
|
156
171
|
end
|
157
172
|
|
158
|
-
def on_mime_finish
|
173
|
+
def on_mime_finish(mime_index)
|
159
174
|
end
|
160
175
|
|
161
176
|
private
|
162
177
|
|
163
|
-
def
|
164
|
-
|
165
|
-
|
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
|
166
184
|
@mime_parts.each(&:close)
|
167
185
|
raise MultipartPartLimitError, 'Maximum file multiparts in content reached'
|
168
186
|
end
|
169
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
|
170
195
|
end
|
171
196
|
end
|
172
197
|
|
@@ -175,32 +200,48 @@ module Rack
|
|
175
200
|
def initialize(boundary, tempfile, bufsize, query_parser)
|
176
201
|
@query_parser = query_parser
|
177
202
|
@params = query_parser.make_params
|
178
|
-
@boundary = "--#{boundary}"
|
179
203
|
@bufsize = bufsize
|
180
204
|
|
181
|
-
@full_boundary = @boundary
|
182
|
-
@end_boundary = @boundary + '--'
|
183
205
|
@state = :FAST_FORWARD
|
184
206
|
@mime_index = 0
|
185
207
|
@collector = Collector.new tempfile
|
186
208
|
|
187
209
|
@sbuf = StringScanner.new("".dup)
|
188
|
-
@body_regex = /(
|
189
|
-
@
|
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)
|
190
214
|
@head_regex = /(.*?#{EOL})#{EOL}/m
|
191
215
|
end
|
192
216
|
|
193
|
-
def
|
194
|
-
|
195
|
-
|
196
|
-
|
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
|
197
238
|
end
|
198
239
|
|
199
240
|
def result
|
200
241
|
@collector.each do |part|
|
201
242
|
part.get_data do |data|
|
202
243
|
tag_multipart_encoding(part.filename, part.content_type, part.name, data)
|
203
|
-
@query_parser.normalize_params(@params, part.name, data
|
244
|
+
@query_parser.normalize_params(@params, part.name, data)
|
204
245
|
end
|
205
246
|
end
|
206
247
|
MultipartInfo.new @params.to_params_hash, @collector.find_all(&:file?).map(&:body)
|
@@ -208,29 +249,45 @@ module Rack
|
|
208
249
|
|
209
250
|
private
|
210
251
|
|
211
|
-
def
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
break if handle_fast_forward == :want_read
|
216
|
-
when :CONSUME_TOKEN
|
217
|
-
break if handle_consume_token == :want_read
|
218
|
-
when :MIME_HEAD
|
219
|
-
break if handle_mime_head == :want_read
|
220
|
-
when :MIME_BODY
|
221
|
-
break if handle_mime_body == :want_read
|
222
|
-
when :DONE
|
223
|
-
break
|
224
|
-
end
|
225
|
-
end
|
252
|
+
def dequote(str) # From WEBrick::HTTPUtils
|
253
|
+
ret = (/\A"(.*)"\Z/ =~ str) ? $1 : str.dup
|
254
|
+
ret.gsub!(/\\(.)/, "\\1")
|
255
|
+
ret
|
226
256
|
end
|
227
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.
|
228
271
|
def handle_fast_forward
|
229
|
-
|
230
|
-
|
231
|
-
|
232
|
-
|
233
|
-
|
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
|
234
291
|
end
|
235
292
|
end
|
236
293
|
|
@@ -244,17 +301,102 @@ module Rack
|
|
244
301
|
end
|
245
302
|
end
|
246
303
|
|
304
|
+
CONTENT_DISPOSITION_MAX_PARAMS = 16
|
305
|
+
CONTENT_DISPOSITION_MAX_BYTES = 1536
|
247
306
|
def handle_mime_head
|
248
307
|
if @sbuf.scan_until(@head_regex)
|
249
308
|
head = @sbuf[1]
|
250
309
|
content_type = head[MULTIPART_CONTENT_TYPE, 1]
|
251
|
-
if
|
252
|
-
|
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
|
253
388
|
else
|
254
389
|
name = head[MULTIPART_CONTENT_ID, 1]
|
255
390
|
end
|
256
391
|
|
257
|
-
|
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
|
258
400
|
|
259
401
|
if name.nil? || name.empty?
|
260
402
|
name = filename || "#{content_type || TEXT_PLAIN}[]".dup
|
@@ -268,8 +410,8 @@ module Rack
|
|
268
410
|
end
|
269
411
|
|
270
412
|
def handle_mime_body
|
271
|
-
if @sbuf.check_until(@body_regex) # check but do not advance the pointer yet
|
272
|
-
body = @
|
413
|
+
if (body_with_boundary = @sbuf.check_until(@body_regex)) # check but do not advance the pointer yet
|
414
|
+
body = body_with_boundary.sub(@body_regex_at_end, '') # remove the boundary from the string
|
273
415
|
@collector.on_mime_body @mime_index, body
|
274
416
|
@sbuf.pos += body.length + 2 # skip \r\n after the content
|
275
417
|
@state = :CONSUME_TOKEN
|
@@ -286,53 +428,31 @@ module Rack
|
|
286
428
|
end
|
287
429
|
end
|
288
430
|
|
289
|
-
|
290
|
-
|
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.
|
291
435
|
def consume_boundary
|
292
|
-
|
293
|
-
|
294
|
-
|
295
|
-
|
296
|
-
|
297
|
-
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
|
298
441
|
end
|
299
442
|
end
|
300
443
|
|
301
|
-
def
|
302
|
-
filename = nil
|
303
|
-
case head
|
304
|
-
when RFC2183
|
305
|
-
params = Hash[*head.scan(DISPPARM).flat_map(&:compact)]
|
306
|
-
|
307
|
-
if filename = params['filename']
|
308
|
-
filename = $1 if filename =~ /^"(.*)"$/
|
309
|
-
elsif filename = params['filename*']
|
310
|
-
encoding, _, filename = filename.split("'", 3)
|
311
|
-
end
|
312
|
-
when BROKEN_QUOTED, BROKEN_UNQUOTED
|
313
|
-
filename = $1
|
314
|
-
end
|
315
|
-
|
316
|
-
return unless filename
|
317
|
-
|
444
|
+
def normalize_filename(filename)
|
318
445
|
if filename.scan(/%.?.?/).all? { |s| /%[0-9a-fA-F]{2}/.match?(s) }
|
319
446
|
filename = Utils.unescape_path(filename)
|
320
447
|
end
|
321
448
|
|
322
449
|
filename.scrub!
|
323
450
|
|
324
|
-
|
325
|
-
filename = filename.gsub(/\\(.)/, '\1')
|
326
|
-
end
|
327
|
-
|
328
|
-
if encoding
|
329
|
-
filename.force_encoding ::Encoding.find(encoding)
|
330
|
-
end
|
331
|
-
|
332
|
-
filename
|
451
|
+
filename.split(/[\/\\]/).last || String.new
|
333
452
|
end
|
334
453
|
|
335
454
|
CHARSET = "charset"
|
455
|
+
deprecate_constant :CHARSET
|
336
456
|
|
337
457
|
def tag_multipart_encoding(filename, content_type, name, body)
|
338
458
|
name = name.to_s
|
@@ -347,13 +467,15 @@ module Rack
|
|
347
467
|
type_subtype = list.first
|
348
468
|
type_subtype.strip!
|
349
469
|
if TEXT_PLAIN == type_subtype
|
350
|
-
rest
|
470
|
+
rest = list.drop 1
|
351
471
|
rest.each do |param|
|
352
472
|
k, v = param.split('=', 2)
|
353
473
|
k.strip!
|
354
474
|
v.strip!
|
355
475
|
v = v[1..-2] if v.start_with?('"') && v.end_with?('"')
|
356
|
-
|
476
|
+
if k == "charset"
|
477
|
+
encoding = find_encoding(v)
|
478
|
+
end
|
357
479
|
end
|
358
480
|
end
|
359
481
|
end
|
@@ -362,9 +484,18 @@ module Rack
|
|
362
484
|
body.force_encoding(encoding)
|
363
485
|
end
|
364
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
|
+
|
365
496
|
def handle_empty_content!(content)
|
366
497
|
if content.nil? || content.empty?
|
367
|
-
raise
|
498
|
+
raise EmptyContentError
|
368
499
|
end
|
369
500
|
end
|
370
501
|
end
|