rack 2.0.9.3 → 3.0.0
Sign up to get free protection for your applications and to get access to all the features.
Potentially problematic release.
This version of rack might be problematic. Click here for more details.
- checksums.yaml +4 -4
- data/CHANGELOG.md +808 -0
- data/CONTRIBUTING.md +142 -0
- data/{COPYING → MIT-LICENSE} +4 -2
- data/README.md +293 -0
- data/SPEC.rdoc +340 -0
- data/lib/rack/auth/abstract/handler.rb +6 -2
- data/lib/rack/auth/abstract/request.rb +4 -2
- data/lib/rack/auth/basic.rb +7 -4
- data/lib/rack/auth/digest/md5.rb +1 -129
- data/lib/rack/auth/digest/nonce.rb +1 -51
- data/lib/rack/auth/digest/params.rb +1 -52
- data/lib/rack/auth/digest/request.rb +1 -41
- data/lib/rack/auth/digest.rb +256 -0
- data/lib/rack/body_proxy.rb +18 -15
- data/lib/rack/builder.rb +151 -40
- data/lib/rack/cascade.rb +30 -12
- data/lib/rack/chunked.rb +74 -23
- data/lib/rack/common_logger.rb +49 -36
- data/lib/rack/conditional_get.rb +33 -26
- data/lib/rack/config.rb +2 -0
- data/lib/rack/constants.rb +63 -0
- data/lib/rack/content_length.rb +13 -16
- data/lib/rack/content_type.rb +12 -8
- data/lib/rack/deflater.rb +84 -45
- data/lib/rack/directory.rb +90 -64
- data/lib/rack/etag.rb +17 -23
- data/lib/rack/events.rb +23 -20
- data/lib/rack/file.rb +5 -172
- data/lib/rack/files.rb +216 -0
- data/lib/rack/head.rb +10 -9
- data/lib/rack/headers.rb +154 -0
- data/lib/rack/lint.rb +786 -645
- data/lib/rack/lock.rb +4 -6
- data/lib/rack/logger.rb +4 -0
- data/lib/rack/media_type.rb +10 -5
- data/lib/rack/method_override.rb +8 -2
- data/lib/rack/mime.rb +17 -1
- data/lib/rack/mock.rb +2 -195
- data/lib/rack/mock_request.rb +166 -0
- data/lib/rack/mock_response.rb +126 -0
- data/lib/rack/multipart/generator.rb +21 -15
- data/lib/rack/multipart/parser.rb +161 -118
- data/lib/rack/multipart/uploaded_file.rb +19 -7
- data/lib/rack/multipart.rb +23 -41
- data/lib/rack/null_logger.rb +11 -0
- data/lib/rack/query_parser.rb +126 -65
- data/lib/rack/recursive.rb +9 -5
- data/lib/rack/reloader.rb +6 -4
- data/lib/rack/request.rb +331 -74
- data/lib/rack/response.rb +223 -70
- data/lib/rack/rewindable_input.rb +28 -8
- data/lib/rack/runtime.rb +11 -8
- data/lib/rack/sendfile.rb +42 -33
- data/lib/rack/show_exceptions.rb +35 -18
- data/lib/rack/show_status.rb +25 -15
- data/lib/rack/static.rb +30 -18
- data/lib/rack/tempfile_reaper.rb +16 -5
- data/lib/rack/urlmap.rb +14 -6
- data/lib/rack/utils.rb +268 -260
- data/lib/rack/version.rb +34 -0
- data/lib/rack.rb +15 -92
- metadata +44 -207
- data/HISTORY.md +0 -520
- data/README.rdoc +0 -316
- data/Rakefile +0 -116
- data/SPEC +0 -263
- data/bin/rackup +0 -4
- 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 -4
- data/example/protectedlobster.rb +0 -14
- data/example/protectedlobster.ru +0 -8
- data/lib/rack/handler/cgi.rb +0 -60
- data/lib/rack/handler/fastcgi.rb +0 -100
- data/lib/rack/handler/lsws.rb +0 -61
- data/lib/rack/handler/scgi.rb +0 -70
- data/lib/rack/handler/thin.rb +0 -36
- data/lib/rack/handler/webrick.rb +0 -120
- data/lib/rack/handler.rb +0 -99
- data/lib/rack/lobster.rb +0 -70
- data/lib/rack/server.rb +0 -395
- data/lib/rack/session/abstract/id.rb +0 -510
- data/lib/rack/session/cookie.rb +0 -204
- data/lib/rack/session/memcache.rb +0 -99
- data/lib/rack/session/pool.rb +0 -83
- data/rack.gemspec +0 -34
- data/test/builder/an_underscore_app.rb +0 -5
- data/test/builder/anything.rb +0 -5
- data/test/builder/comment.ru +0 -4
- data/test/builder/end.ru +0 -5
- data/test/builder/line.ru +0 -1
- data/test/builder/options.ru +0 -2
- data/test/cgi/assets/folder/test.js +0 -1
- data/test/cgi/assets/fonts/font.eot +0 -1
- data/test/cgi/assets/images/image.png +0 -1
- data/test/cgi/assets/index.html +0 -1
- data/test/cgi/assets/javascripts/app.js +0 -1
- data/test/cgi/assets/stylesheets/app.css +0 -1
- data/test/cgi/lighttpd.conf +0 -26
- data/test/cgi/rackup_stub.rb +0 -6
- data/test/cgi/sample_rackup.ru +0 -5
- data/test/cgi/test +0 -9
- data/test/cgi/test+directory/test+file +0 -1
- data/test/cgi/test.fcgi +0 -9
- data/test/cgi/test.gz +0 -0
- data/test/cgi/test.ru +0 -5
- data/test/gemloader.rb +0 -10
- data/test/helper.rb +0 -34
- data/test/multipart/bad_robots +0 -259
- data/test/multipart/binary +0 -0
- data/test/multipart/content_type_and_no_filename +0 -6
- data/test/multipart/empty +0 -10
- data/test/multipart/fail_16384_nofile +0 -814
- data/test/multipart/file1.txt +0 -1
- data/test/multipart/filename_and_modification_param +0 -7
- data/test/multipart/filename_and_no_name +0 -6
- data/test/multipart/filename_with_encoded_words +0 -7
- data/test/multipart/filename_with_escaped_quotes +0 -6
- data/test/multipart/filename_with_escaped_quotes_and_modification_param +0 -7
- data/test/multipart/filename_with_null_byte +0 -7
- data/test/multipart/filename_with_percent_escaped_quotes +0 -6
- data/test/multipart/filename_with_single_quote +0 -7
- data/test/multipart/filename_with_unescaped_percentages +0 -6
- data/test/multipart/filename_with_unescaped_percentages2 +0 -6
- data/test/multipart/filename_with_unescaped_percentages3 +0 -6
- data/test/multipart/filename_with_unescaped_quotes +0 -6
- data/test/multipart/ie +0 -6
- data/test/multipart/invalid_character +0 -6
- data/test/multipart/mixed_files +0 -21
- data/test/multipart/nested +0 -10
- data/test/multipart/none +0 -9
- data/test/multipart/quoted +0 -15
- data/test/multipart/rack-logo.png +0 -0
- data/test/multipart/semicolon +0 -6
- data/test/multipart/text +0 -15
- data/test/multipart/three_files_three_fields +0 -31
- data/test/multipart/unity3d_wwwform +0 -11
- data/test/multipart/webkit +0 -32
- data/test/rackup/config.ru +0 -31
- data/test/registering_handler/rack/handler/registering_myself.rb +0 -8
- data/test/spec_auth_basic.rb +0 -89
- data/test/spec_auth_digest.rb +0 -260
- data/test/spec_body_proxy.rb +0 -85
- data/test/spec_builder.rb +0 -233
- data/test/spec_cascade.rb +0 -63
- data/test/spec_cgi.rb +0 -84
- data/test/spec_chunked.rb +0 -103
- data/test/spec_common_logger.rb +0 -107
- data/test/spec_conditional_get.rb +0 -103
- data/test/spec_config.rb +0 -23
- data/test/spec_content_length.rb +0 -86
- data/test/spec_content_type.rb +0 -46
- data/test/spec_deflater.rb +0 -375
- data/test/spec_directory.rb +0 -148
- data/test/spec_etag.rb +0 -108
- data/test/spec_events.rb +0 -133
- data/test/spec_fastcgi.rb +0 -85
- data/test/spec_file.rb +0 -264
- data/test/spec_handler.rb +0 -57
- data/test/spec_head.rb +0 -46
- data/test/spec_lint.rb +0 -520
- data/test/spec_lobster.rb +0 -59
- data/test/spec_lock.rb +0 -204
- data/test/spec_logger.rb +0 -24
- data/test/spec_media_type.rb +0 -42
- data/test/spec_method_override.rb +0 -110
- data/test/spec_mime.rb +0 -51
- data/test/spec_mock.rb +0 -359
- data/test/spec_multipart.rb +0 -721
- data/test/spec_null_logger.rb +0 -21
- data/test/spec_recursive.rb +0 -75
- data/test/spec_request.rb +0 -1423
- data/test/spec_response.rb +0 -528
- data/test/spec_rewindable_input.rb +0 -128
- data/test/spec_runtime.rb +0 -50
- data/test/spec_sendfile.rb +0 -125
- data/test/spec_server.rb +0 -193
- data/test/spec_session_abstract_id.rb +0 -31
- data/test/spec_session_abstract_session_hash.rb +0 -45
- data/test/spec_session_cookie.rb +0 -442
- data/test/spec_session_memcache.rb +0 -357
- data/test/spec_session_persisted_secure_secure_session_hash.rb +0 -73
- data/test/spec_session_pool.rb +0 -247
- data/test/spec_show_exceptions.rb +0 -93
- data/test/spec_show_status.rb +0 -104
- data/test/spec_static.rb +0 -184
- data/test/spec_tempfile_reaper.rb +0 -64
- data/test/spec_thin.rb +0 -96
- data/test/spec_urlmap.rb +0 -237
- data/test/spec_utils.rb +0 -742
- data/test/spec_version.rb +0 -11
- data/test/spec_webrick.rb +0 -206
- data/test/static/another/index.html +0 -1
- data/test/static/foo.html +0 -1
- data/test/static/index.html +0 -1
- data/test/testrequest.rb +0 -78
- data/test/unregistered_handler/rack/handler/unregistered.rb +0 -7
- data/test/unregistered_handler/rack/handler/unregistered_long_one.rb +0 -7
@@ -1,3 +1,7 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'uploaded_file'
|
4
|
+
|
1
5
|
module Rack
|
2
6
|
module Multipart
|
3
7
|
class Generator
|
@@ -15,9 +19,13 @@ module Rack
|
|
15
19
|
|
16
20
|
flattened_params.map do |name, file|
|
17
21
|
if file.respond_to?(:original_filename)
|
18
|
-
|
19
|
-
|
20
|
-
|
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)
|
21
29
|
end
|
22
30
|
else
|
23
31
|
content_for_other(file, name)
|
@@ -27,21 +35,18 @@ module Rack
|
|
27
35
|
|
28
36
|
private
|
29
37
|
def multipart?
|
30
|
-
multipart = false
|
31
|
-
|
32
38
|
query = lambda { |value|
|
33
39
|
case value
|
34
40
|
when Array
|
35
|
-
value.
|
41
|
+
value.any?(&query)
|
36
42
|
when Hash
|
37
|
-
value.values.
|
43
|
+
value.values.any?(&query)
|
38
44
|
when Rack::Multipart::UploadedFile
|
39
|
-
|
45
|
+
true
|
40
46
|
end
|
41
47
|
}
|
42
|
-
@params.values.each(&query)
|
43
48
|
|
44
|
-
|
49
|
+
@params.values.any?(&query)
|
45
50
|
end
|
46
51
|
|
47
52
|
def flattened_params
|
@@ -70,12 +75,13 @@ module Rack
|
|
70
75
|
end
|
71
76
|
|
72
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)}\""
|
73
80
|
<<-EOF
|
74
81
|
--#{MULTIPART_BOUNDARY}\r
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
\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
|
79
85
|
#{io.read}\r
|
80
86
|
EOF
|
81
87
|
end
|
@@ -83,7 +89,7 @@ EOF
|
|
83
89
|
def content_for_other(file, name)
|
84
90
|
<<-EOF
|
85
91
|
--#{MULTIPART_BOUNDARY}\r
|
86
|
-
|
92
|
+
content-disposition: form-data; name="#{name}"\r
|
87
93
|
\r
|
88
94
|
#{file}\r
|
89
95
|
EOF
|
@@ -1,15 +1,51 @@
|
|
1
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'strscan'
|
4
|
+
|
5
|
+
require_relative '../utils'
|
2
6
|
|
3
7
|
module Rack
|
4
8
|
module Multipart
|
5
9
|
class MultipartPartLimitError < Errno::EMFILE; end
|
6
|
-
|
10
|
+
|
11
|
+
# Use specific error class when parsing multipart request
|
12
|
+
# that ends early.
|
13
|
+
class EmptyContentError < ::EOFError; end
|
14
|
+
|
15
|
+
# Base class for multipart exceptions that do not subclass from
|
16
|
+
# other exception classes for backwards compatibility.
|
17
|
+
class Error < StandardError; end
|
18
|
+
|
19
|
+
EOL = "\r\n"
|
20
|
+
MULTIPART = %r|\Amultipart/.*boundary=\"?([^\";,]+)\"?|ni
|
21
|
+
TOKEN = /[^\s()<>,;:\\"\/\[\]?=]+/
|
22
|
+
CONDISP = /Content-Disposition:\s*#{TOKEN}\s*/i
|
23
|
+
VALUE = /"(?:\\"|[^"])*"|#{TOKEN}/
|
24
|
+
BROKEN = /^#{CONDISP}.*;\s*filename=(#{VALUE})/i
|
25
|
+
MULTIPART_CONTENT_TYPE = /Content-Type: (.*)#{EOL}/ni
|
26
|
+
MULTIPART_CONTENT_DISPOSITION = /Content-Disposition:.*;\s*name=(#{VALUE})/ni
|
27
|
+
MULTIPART_CONTENT_ID = /Content-ID:\s*([^#{EOL}]*)/ni
|
28
|
+
# Updated definitions from RFC 2231
|
29
|
+
ATTRIBUTE_CHAR = %r{[^ \t\v\n\r)(><@,;:\\"/\[\]?='*%]}
|
30
|
+
ATTRIBUTE = /#{ATTRIBUTE_CHAR}+/
|
31
|
+
SECTION = /\*[0-9]+/
|
32
|
+
REGULAR_PARAMETER_NAME = /#{ATTRIBUTE}#{SECTION}?/
|
33
|
+
REGULAR_PARAMETER = /(#{REGULAR_PARAMETER_NAME})=(#{VALUE})/
|
34
|
+
EXTENDED_OTHER_NAME = /#{ATTRIBUTE}\*[1-9][0-9]*\*/
|
35
|
+
EXTENDED_OTHER_VALUE = /%[0-9a-fA-F]{2}|#{ATTRIBUTE_CHAR}/
|
36
|
+
EXTENDED_OTHER_PARAMETER = /(#{EXTENDED_OTHER_NAME})=(#{EXTENDED_OTHER_VALUE}*)/
|
37
|
+
EXTENDED_INITIAL_NAME = /#{ATTRIBUTE}(?:\*0)?\*/
|
38
|
+
EXTENDED_INITIAL_VALUE = /[a-zA-Z0-9\-]*'[a-zA-Z0-9\-]*'#{EXTENDED_OTHER_VALUE}*/
|
39
|
+
EXTENDED_INITIAL_PARAMETER = /(#{EXTENDED_INITIAL_NAME})=(#{EXTENDED_INITIAL_VALUE})/
|
40
|
+
EXTENDED_PARAMETER = /#{EXTENDED_INITIAL_PARAMETER}|#{EXTENDED_OTHER_PARAMETER}/
|
41
|
+
DISPPARM = /;\s*(?:#{REGULAR_PARAMETER}|#{EXTENDED_PARAMETER})\s*/
|
42
|
+
RFC2183 = /^#{CONDISP}(#{DISPPARM})+$/i
|
7
43
|
|
8
44
|
class Parser
|
9
|
-
BUFSIZE =
|
45
|
+
BUFSIZE = 1_048_576
|
10
46
|
TEXT_PLAIN = "text/plain"
|
11
47
|
TEMPFILE_FACTORY = lambda { |filename, content_type|
|
12
|
-
Tempfile.new(["RackMultipart", ::File.extname(filename.gsub("\0"
|
48
|
+
Tempfile.new(["RackMultipart", ::File.extname(filename.gsub("\0", '%00'))])
|
13
49
|
}
|
14
50
|
|
15
51
|
class BoundedIO # :nodoc:
|
@@ -19,30 +55,26 @@ module Rack
|
|
19
55
|
@cursor = 0
|
20
56
|
end
|
21
57
|
|
22
|
-
def read(size)
|
58
|
+
def read(size, outbuf = nil)
|
23
59
|
return if @cursor >= @content_length
|
24
60
|
|
25
61
|
left = @content_length - @cursor
|
26
62
|
|
27
63
|
str = if left < size
|
28
|
-
@io.read left
|
64
|
+
@io.read left, outbuf
|
29
65
|
else
|
30
|
-
@io.read size
|
66
|
+
@io.read size, outbuf
|
31
67
|
end
|
32
68
|
|
33
69
|
if str
|
34
70
|
@cursor += str.bytesize
|
35
71
|
else
|
36
|
-
# Raise an error for mismatching
|
72
|
+
# Raise an error for mismatching content-length and actual contents
|
37
73
|
raise EOFError, "bad content body"
|
38
74
|
end
|
39
75
|
|
40
76
|
str
|
41
77
|
end
|
42
|
-
|
43
|
-
def rewind
|
44
|
-
@io.rewind
|
45
|
-
end
|
46
78
|
end
|
47
79
|
|
48
80
|
MultipartInfo = Struct.new :params, :tmp_files
|
@@ -61,17 +93,17 @@ module Rack
|
|
61
93
|
boundary = parse_boundary content_type
|
62
94
|
return EMPTY unless boundary
|
63
95
|
|
96
|
+
if boundary.length > 70
|
97
|
+
# RFC 1521 Section 7.2.1 imposes a 70 character maximum for the boundary.
|
98
|
+
# Most clients use no more than 55 characters.
|
99
|
+
raise Error, "multipart boundary size too large (#{boundary.length} characters)"
|
100
|
+
end
|
101
|
+
|
64
102
|
io = BoundedIO.new(io, content_length) if content_length
|
65
103
|
|
66
104
|
parser = new(boundary, tmpfile, bufsize, qp)
|
67
|
-
parser.
|
68
|
-
|
69
|
-
loop do
|
70
|
-
break if parser.state == :DONE
|
71
|
-
parser.on_read io.read(bufsize)
|
72
|
-
end
|
105
|
+
parser.parse(io)
|
73
106
|
|
74
|
-
io.rewind
|
75
107
|
parser.result
|
76
108
|
end
|
77
109
|
|
@@ -91,14 +123,8 @@ module Rack
|
|
91
123
|
# those which give the lone filename.
|
92
124
|
fn = filename.split(/[\/\\]/).last
|
93
125
|
|
94
|
-
data = {:
|
95
|
-
:
|
96
|
-
elsif !filename && content_type && body.is_a?(IO)
|
97
|
-
body.rewind
|
98
|
-
|
99
|
-
# Generic multipart cases, not coming from a form
|
100
|
-
data = {:type => content_type,
|
101
|
-
:name => name, :tempfile => body, :head => head}
|
126
|
+
data = { filename: fn, type: content_type,
|
127
|
+
name: name, tempfile: body, head: head }
|
102
128
|
end
|
103
129
|
|
104
130
|
yield data
|
@@ -117,7 +143,7 @@ module Rack
|
|
117
143
|
|
118
144
|
include Enumerable
|
119
145
|
|
120
|
-
def initialize
|
146
|
+
def initialize(tempfile)
|
121
147
|
@tempfile = tempfile
|
122
148
|
@mime_parts = []
|
123
149
|
@open_files = 0
|
@@ -127,7 +153,7 @@ module Rack
|
|
127
153
|
@mime_parts.each { |part| yield part }
|
128
154
|
end
|
129
155
|
|
130
|
-
def on_mime_head
|
156
|
+
def on_mime_head(mime_index, head, filename, content_type, name)
|
131
157
|
if filename
|
132
158
|
body = @tempfile.call(filename, content_type)
|
133
159
|
body.binmode if body.respond_to?(:binmode)
|
@@ -140,121 +166,131 @@ module Rack
|
|
140
166
|
|
141
167
|
@mime_parts[mime_index] = klass.new(body, head, filename, content_type, name)
|
142
168
|
|
143
|
-
|
169
|
+
check_open_files
|
144
170
|
end
|
145
171
|
|
146
|
-
def on_mime_body
|
172
|
+
def on_mime_body(mime_index, content)
|
147
173
|
@mime_parts[mime_index].body << content
|
148
174
|
end
|
149
175
|
|
150
|
-
def on_mime_finish
|
176
|
+
def on_mime_finish(mime_index)
|
151
177
|
end
|
152
178
|
|
153
179
|
private
|
154
180
|
|
155
|
-
def
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
if file_limit && file_limit > 0
|
160
|
-
if @open_files >= file_limit
|
181
|
+
def check_open_files
|
182
|
+
if Utils.multipart_part_limit > 0
|
183
|
+
if @open_files >= Utils.multipart_part_limit
|
161
184
|
@mime_parts.each(&:close)
|
162
185
|
raise MultipartPartLimitError, 'Maximum file multiparts in content reached'
|
163
186
|
end
|
164
187
|
end
|
165
|
-
|
166
|
-
if part_limit && part_limit > 0
|
167
|
-
if @mime_parts.size >= part_limit
|
168
|
-
@mime_parts.each(&:close)
|
169
|
-
raise MultipartTotalPartLimitError, 'Maximum total multiparts in content reached'
|
170
|
-
end
|
171
|
-
end
|
172
188
|
end
|
173
189
|
end
|
174
190
|
|
175
191
|
attr_reader :state
|
176
192
|
|
177
193
|
def initialize(boundary, tempfile, bufsize, query_parser)
|
178
|
-
@buf = String.new
|
179
|
-
|
180
194
|
@query_parser = query_parser
|
181
195
|
@params = query_parser.make_params
|
182
|
-
@boundary = "--#{boundary}"
|
183
196
|
@bufsize = bufsize
|
184
197
|
|
185
|
-
@rx = /(?:#{EOL})?#{Regexp.quote(@boundary)}(#{EOL}|--)/n
|
186
|
-
@rx_max_size = EOL.size + @boundary.bytesize + [EOL.size, '--'.size].max
|
187
|
-
@full_boundary = @boundary
|
188
|
-
@end_boundary = @boundary + '--'
|
189
198
|
@state = :FAST_FORWARD
|
190
199
|
@mime_index = 0
|
191
200
|
@collector = Collector.new tempfile
|
201
|
+
|
202
|
+
@sbuf = StringScanner.new("".dup)
|
203
|
+
@body_regex = /(?:#{EOL}|\A)--#{Regexp.quote(boundary)}(?:#{EOL}|--)/m
|
204
|
+
@rx_max_size = boundary.bytesize + 6 # (\r\n-- at start, either \r\n or -- at finish)
|
205
|
+
@head_regex = /(.*?#{EOL})#{EOL}/m
|
192
206
|
end
|
193
207
|
|
194
|
-
def
|
195
|
-
|
196
|
-
|
197
|
-
|
208
|
+
def parse(io)
|
209
|
+
outbuf = String.new
|
210
|
+
read_data(io, outbuf)
|
211
|
+
|
212
|
+
loop do
|
213
|
+
status =
|
214
|
+
case @state
|
215
|
+
when :FAST_FORWARD
|
216
|
+
handle_fast_forward
|
217
|
+
when :CONSUME_TOKEN
|
218
|
+
handle_consume_token
|
219
|
+
when :MIME_HEAD
|
220
|
+
handle_mime_head
|
221
|
+
when :MIME_BODY
|
222
|
+
handle_mime_body
|
223
|
+
else # when :DONE
|
224
|
+
return
|
225
|
+
end
|
226
|
+
|
227
|
+
read_data(io, outbuf) if status == :want_read
|
228
|
+
end
|
198
229
|
end
|
199
230
|
|
200
231
|
def result
|
201
232
|
@collector.each do |part|
|
202
233
|
part.get_data do |data|
|
203
234
|
tag_multipart_encoding(part.filename, part.content_type, part.name, data)
|
204
|
-
@query_parser.normalize_params(@params, part.name, data
|
235
|
+
@query_parser.normalize_params(@params, part.name, data)
|
205
236
|
end
|
206
237
|
end
|
207
|
-
|
208
238
|
MultipartInfo.new @params.to_params_hash, @collector.find_all(&:file?).map(&:body)
|
209
239
|
end
|
210
240
|
|
211
241
|
private
|
212
242
|
|
213
|
-
def
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
break if handle_fast_forward == :want_read
|
218
|
-
when :CONSUME_TOKEN
|
219
|
-
break if handle_consume_token == :want_read
|
220
|
-
when :MIME_HEAD
|
221
|
-
break if handle_mime_head == :want_read
|
222
|
-
when :MIME_BODY
|
223
|
-
break if handle_mime_body == :want_read
|
224
|
-
when :DONE
|
225
|
-
break
|
226
|
-
end
|
227
|
-
end
|
243
|
+
def dequote(str) # From WEBrick::HTTPUtils
|
244
|
+
ret = (/\A"(.*)"\Z/ =~ str) ? $1 : str.dup
|
245
|
+
ret.gsub!(/\\(.)/, "\\1")
|
246
|
+
ret
|
228
247
|
end
|
229
248
|
|
249
|
+
def read_data(io, outbuf)
|
250
|
+
content = io.read(@bufsize, outbuf)
|
251
|
+
handle_empty_content!(content)
|
252
|
+
@sbuf.concat(content)
|
253
|
+
end
|
254
|
+
|
255
|
+
# This handles the initial parser state. We read until we find the starting
|
256
|
+
# boundary, then we can transition to the next state. If we find the ending
|
257
|
+
# boundary, this is an invalid multipart upload, but keep scanning for opening
|
258
|
+
# boundary in that case. If no boundary found, we need to keep reading data
|
259
|
+
# and retry. It's highly unlikely the initial read will not consume the
|
260
|
+
# boundary. The client would have to deliberately craft a response
|
261
|
+
# with the opening boundary beyond the buffer size for that to happen.
|
230
262
|
def handle_fast_forward
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
|
263
|
+
while true
|
264
|
+
case consume_boundary
|
265
|
+
when :BOUNDARY
|
266
|
+
# found opening boundary, transition to next state
|
267
|
+
@state = :MIME_HEAD
|
268
|
+
return
|
269
|
+
when :END_BOUNDARY
|
270
|
+
# invalid multipart upload, but retry for opening boundary
|
271
|
+
else
|
272
|
+
# no boundary found, keep reading data
|
273
|
+
return :want_read
|
274
|
+
end
|
236
275
|
end
|
237
276
|
end
|
238
277
|
|
239
278
|
def handle_consume_token
|
240
279
|
tok = consume_boundary
|
241
280
|
# break if we're at the end of a buffer, but not if it is the end of a field
|
242
|
-
if tok == :END_BOUNDARY || (@
|
243
|
-
|
281
|
+
@state = if tok == :END_BOUNDARY || (@sbuf.eos? && tok != :BOUNDARY)
|
282
|
+
:DONE
|
244
283
|
else
|
245
|
-
|
284
|
+
:MIME_HEAD
|
246
285
|
end
|
247
286
|
end
|
248
287
|
|
249
288
|
def handle_mime_head
|
250
|
-
if @
|
251
|
-
|
252
|
-
head = @buf.slice!(0, i+2) # First \r\n
|
253
|
-
@buf.slice!(0, 2) # Second \r\n
|
254
|
-
|
289
|
+
if @sbuf.scan_until(@head_regex)
|
290
|
+
head = @sbuf[1]
|
255
291
|
content_type = head[MULTIPART_CONTENT_TYPE, 1]
|
256
292
|
if name = head[MULTIPART_CONTENT_DISPOSITION, 1]
|
257
|
-
name =
|
293
|
+
name = dequote(name)
|
258
294
|
else
|
259
295
|
name = head[MULTIPART_CONTENT_ID, 1]
|
260
296
|
end
|
@@ -262,7 +298,7 @@ module Rack
|
|
262
298
|
filename = get_filename(head)
|
263
299
|
|
264
300
|
if name.nil? || name.empty?
|
265
|
-
name = filename || "#{content_type || TEXT_PLAIN}[]"
|
301
|
+
name = filename || "#{content_type || TEXT_PLAIN}[]".dup
|
266
302
|
end
|
267
303
|
|
268
304
|
@collector.on_mime_head @mime_index, head, filename, content_type, name
|
@@ -273,33 +309,34 @@ module Rack
|
|
273
309
|
end
|
274
310
|
|
275
311
|
def handle_mime_body
|
276
|
-
if
|
277
|
-
#
|
278
|
-
@collector.on_mime_body @mime_index,
|
279
|
-
@
|
312
|
+
if (body_with_boundary = @sbuf.check_until(@body_regex)) # check but do not advance the pointer yet
|
313
|
+
body = body_with_boundary.sub(/#{@body_regex}\z/m, '') # remove the boundary from the string
|
314
|
+
@collector.on_mime_body @mime_index, body
|
315
|
+
@sbuf.pos += body.length + 2 # skip \r\n after the content
|
280
316
|
@state = :CONSUME_TOKEN
|
281
317
|
@mime_index += 1
|
282
318
|
else
|
283
|
-
# Save
|
284
|
-
if @rx_max_size < @
|
285
|
-
|
319
|
+
# Save what we have so far
|
320
|
+
if @rx_max_size < @sbuf.rest_size
|
321
|
+
delta = @sbuf.rest_size - @rx_max_size
|
322
|
+
@collector.on_mime_body @mime_index, @sbuf.peek(delta)
|
323
|
+
@sbuf.pos += delta
|
324
|
+
@sbuf.string = @sbuf.rest
|
286
325
|
end
|
287
326
|
:want_read
|
288
327
|
end
|
289
328
|
end
|
290
329
|
|
291
|
-
|
292
|
-
|
293
|
-
|
294
|
-
|
330
|
+
# Scan until the we find the start or end of the boundary.
|
331
|
+
# If we find it, return the appropriate symbol for the start or
|
332
|
+
# end of the boundary. If we don't find the start or end of the
|
333
|
+
# boundary, clear the buffer and return nil.
|
295
334
|
def consume_boundary
|
296
|
-
|
297
|
-
read_buffer
|
298
|
-
|
299
|
-
|
300
|
-
|
301
|
-
end
|
302
|
-
return if @buf.empty?
|
335
|
+
if read_buffer = @sbuf.scan_until(@body_regex)
|
336
|
+
read_buffer.end_with?(EOL) ? :BOUNDARY : :END_BOUNDARY
|
337
|
+
else
|
338
|
+
@sbuf.terminate
|
339
|
+
nil
|
303
340
|
end
|
304
341
|
end
|
305
342
|
|
@@ -309,10 +346,10 @@ module Rack
|
|
309
346
|
when RFC2183
|
310
347
|
params = Hash[*head.scan(DISPPARM).flat_map(&:compact)]
|
311
348
|
|
312
|
-
if filename = params['filename']
|
313
|
-
filename = $1 if filename =~ /^"(.*)"$/
|
314
|
-
elsif filename = params['filename*']
|
349
|
+
if filename = params['filename*']
|
315
350
|
encoding, _, filename = filename.split("'", 3)
|
351
|
+
elsif filename = params['filename']
|
352
|
+
filename = $1 if filename =~ /^"(.*)"$/
|
316
353
|
end
|
317
354
|
when BROKEN
|
318
355
|
filename = $1
|
@@ -321,8 +358,8 @@ module Rack
|
|
321
358
|
|
322
359
|
return unless filename
|
323
360
|
|
324
|
-
if filename.scan(/%.?.?/).all? { |s|
|
325
|
-
filename = Utils.
|
361
|
+
if filename.scan(/%.?.?/).all? { |s| /%[0-9a-fA-F]{2}/.match?(s) }
|
362
|
+
filename = Utils.unescape_path(filename)
|
326
363
|
end
|
327
364
|
|
328
365
|
filename.scrub!
|
@@ -338,7 +375,8 @@ module Rack
|
|
338
375
|
filename
|
339
376
|
end
|
340
377
|
|
341
|
-
CHARSET
|
378
|
+
CHARSET = "charset"
|
379
|
+
deprecate_constant :CHARSET
|
342
380
|
|
343
381
|
def tag_multipart_encoding(filename, content_type, name, body)
|
344
382
|
name = name.to_s
|
@@ -353,13 +391,19 @@ module Rack
|
|
353
391
|
type_subtype = list.first
|
354
392
|
type_subtype.strip!
|
355
393
|
if TEXT_PLAIN == type_subtype
|
356
|
-
rest
|
394
|
+
rest = list.drop 1
|
357
395
|
rest.each do |param|
|
358
|
-
k,v = param.split('=', 2)
|
396
|
+
k, v = param.split('=', 2)
|
359
397
|
k.strip!
|
360
398
|
v.strip!
|
361
|
-
v = v[1..-2] if v
|
362
|
-
|
399
|
+
v = v[1..-2] if v.start_with?('"') && v.end_with?('"')
|
400
|
+
if k == "charset"
|
401
|
+
encoding = begin
|
402
|
+
Encoding.find v
|
403
|
+
rescue ArgumentError
|
404
|
+
Encoding::BINARY
|
405
|
+
end
|
406
|
+
end
|
363
407
|
end
|
364
408
|
end
|
365
409
|
end
|
@@ -368,10 +412,9 @@ module Rack
|
|
368
412
|
body.force_encoding(encoding)
|
369
413
|
end
|
370
414
|
|
371
|
-
|
372
415
|
def handle_empty_content!(content)
|
373
416
|
if content.nil? || content.empty?
|
374
|
-
raise
|
417
|
+
raise EmptyContentError
|
375
418
|
end
|
376
419
|
end
|
377
420
|
end
|
@@ -1,23 +1,35 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'tempfile'
|
4
|
+
require 'fileutils'
|
5
|
+
|
1
6
|
module Rack
|
2
7
|
module Multipart
|
3
8
|
class UploadedFile
|
9
|
+
|
4
10
|
# The filename, *not* including the path, of the "uploaded" file
|
5
11
|
attr_reader :original_filename
|
6
12
|
|
7
13
|
# The content type of the "uploaded" file
|
8
14
|
attr_accessor :content_type
|
9
15
|
|
10
|
-
def initialize(
|
11
|
-
|
16
|
+
def initialize(filepath = nil, ct = "text/plain", bin = false,
|
17
|
+
path: filepath, content_type: ct, binary: bin, filename: nil, io: nil)
|
18
|
+
if io
|
19
|
+
@tempfile = io
|
20
|
+
@original_filename = filename
|
21
|
+
else
|
22
|
+
raise "#{path} file does not exist" unless ::File.exist?(path)
|
23
|
+
@original_filename = filename || ::File.basename(path)
|
24
|
+
@tempfile = Tempfile.new([@original_filename, ::File.extname(path)], encoding: Encoding::BINARY)
|
25
|
+
@tempfile.binmode if binary
|
26
|
+
FileUtils.copy_file(path, @tempfile.path)
|
27
|
+
end
|
12
28
|
@content_type = content_type
|
13
|
-
@original_filename = ::File.basename(path)
|
14
|
-
@tempfile = Tempfile.new([@original_filename, ::File.extname(path)], encoding: Encoding::BINARY)
|
15
|
-
@tempfile.binmode if binary
|
16
|
-
FileUtils.copy_file(path, @tempfile.path)
|
17
29
|
end
|
18
30
|
|
19
31
|
def path
|
20
|
-
@tempfile.path
|
32
|
+
@tempfile.path if @tempfile.respond_to?(:path)
|
21
33
|
end
|
22
34
|
alias_method :local_path, :path
|
23
35
|
|
data/lib/rack/multipart.rb
CHANGED
@@ -1,62 +1,44 @@
|
|
1
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'constants'
|
4
|
+
require_relative 'utils'
|
5
|
+
|
6
|
+
require_relative 'multipart/parser'
|
7
|
+
require_relative 'multipart/generator'
|
2
8
|
|
3
9
|
module Rack
|
4
10
|
# A multipart form data parser, adapted from IOWA.
|
5
11
|
#
|
6
12
|
# Usually, Rack::Request#POST takes care of calling this.
|
7
13
|
module Multipart
|
8
|
-
autoload :UploadedFile, 'rack/multipart/uploaded_file'
|
9
|
-
autoload :Generator, 'rack/multipart/generator'
|
10
|
-
|
11
|
-
EOL = "\r\n"
|
12
14
|
MULTIPART_BOUNDARY = "AaB03x"
|
13
|
-
MULTIPART = %r|\Amultipart/.*boundary=\"?([^\";,]+)\"?|ni
|
14
|
-
TOKEN = /[^\s()<>,;:\\"\/\[\]?=]+/
|
15
|
-
CONDISP = /Content-Disposition:\s*#{TOKEN}\s*/i
|
16
|
-
VALUE = /"(?:\\"|[^"])*"|#{TOKEN}/
|
17
|
-
BROKEN = /^#{CONDISP}.*;\s*filename=(#{VALUE})/i
|
18
|
-
MULTIPART_CONTENT_TYPE = /Content-Type: (.*)#{EOL}/ni
|
19
|
-
MULTIPART_CONTENT_DISPOSITION = /Content-Disposition:[^:]*;\s+name=(#{VALUE})/ni
|
20
|
-
MULTIPART_CONTENT_ID = /Content-ID:\s*([^#{EOL}]*)/ni
|
21
|
-
# Updated definitions from RFC 2231
|
22
|
-
ATTRIBUTE_CHAR = %r{[^ \x00-\x1f\x7f)(><@,;:\\"/\[\]?='*%]}
|
23
|
-
ATTRIBUTE = /#{ATTRIBUTE_CHAR}+/
|
24
|
-
SECTION = /\*[0-9]+/
|
25
|
-
REGULAR_PARAMETER_NAME = /#{ATTRIBUTE}#{SECTION}?/
|
26
|
-
REGULAR_PARAMETER = /(#{REGULAR_PARAMETER_NAME})=(#{VALUE})/
|
27
|
-
EXTENDED_OTHER_NAME = /#{ATTRIBUTE}\*[1-9][0-9]*\*/
|
28
|
-
EXTENDED_OTHER_VALUE = /%[0-9a-fA-F]{2}|#{ATTRIBUTE_CHAR}/
|
29
|
-
EXTENDED_OTHER_PARAMETER = /(#{EXTENDED_OTHER_NAME})=(#{EXTENDED_OTHER_VALUE}*)/
|
30
|
-
EXTENDED_INITIAL_NAME = /#{ATTRIBUTE}(?:\*0)?\*/
|
31
|
-
EXTENDED_INITIAL_VALUE = /[a-zA-Z0-9\-]*'[a-zA-Z0-9\-]*'#{EXTENDED_OTHER_VALUE}*/
|
32
|
-
EXTENDED_INITIAL_PARAMETER = /(#{EXTENDED_INITIAL_NAME})=(#{EXTENDED_INITIAL_VALUE})/
|
33
|
-
EXTENDED_PARAMETER = /#{EXTENDED_INITIAL_PARAMETER}|#{EXTENDED_OTHER_PARAMETER}/
|
34
|
-
DISPPARM = /;\s*(?:#{REGULAR_PARAMETER}|#{EXTENDED_PARAMETER})\s*/
|
35
|
-
RFC2183 = /^#{CONDISP}(#{DISPPARM})+$/i
|
36
15
|
|
37
16
|
class << self
|
38
17
|
def parse_multipart(env, params = Rack::Utils.default_query_parser)
|
39
|
-
|
40
|
-
|
18
|
+
io = env[RACK_INPUT]
|
19
|
+
|
20
|
+
if content_length = env['CONTENT_LENGTH']
|
21
|
+
content_length = content_length.to_i
|
22
|
+
end
|
41
23
|
|
42
|
-
|
43
|
-
io = req.get_header(RACK_INPUT)
|
44
|
-
io.rewind
|
45
|
-
content_length = req.content_length
|
46
|
-
content_length = content_length.to_i if content_length
|
24
|
+
content_type = env['CONTENT_TYPE']
|
47
25
|
|
48
|
-
tempfile =
|
49
|
-
bufsize =
|
26
|
+
tempfile = env[RACK_MULTIPART_TEMPFILE_FACTORY] || Parser::TEMPFILE_FACTORY
|
27
|
+
bufsize = env[RACK_MULTIPART_BUFFER_SIZE] || Parser::BUFSIZE
|
50
28
|
|
51
|
-
info = Parser.parse
|
52
|
-
|
53
|
-
|
29
|
+
info = Parser.parse(io, content_length, content_type, tempfile, bufsize, params)
|
30
|
+
env[RACK_TEMPFILES] = info.tmp_files
|
31
|
+
|
32
|
+
return info.params
|
33
|
+
end
|
34
|
+
|
35
|
+
def extract_multipart(request, params = Rack::Utils.default_query_parser)
|
36
|
+
parse_multipart(request.env)
|
54
37
|
end
|
55
38
|
|
56
39
|
def build_multipart(params, first = true)
|
57
40
|
Generator.new(params, first).dump
|
58
41
|
end
|
59
42
|
end
|
60
|
-
|
61
43
|
end
|
62
44
|
end
|