rack 1.6.13 → 2.1.4.3
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 +92 -0
- data/{COPYING → MIT-LICENSE} +4 -2
- data/README.rdoc +105 -141
- data/Rakefile +27 -28
- data/SPEC +6 -7
- data/bin/rackup +1 -0
- data/contrib/rack_logo.svg +164 -111
- data/example/lobster.ru +2 -0
- data/example/protectedlobster.rb +4 -2
- data/example/protectedlobster.ru +3 -1
- data/lib/rack/auth/abstract/handler.rb +3 -1
- data/lib/rack/auth/abstract/request.rb +7 -1
- data/lib/rack/auth/basic.rb +4 -1
- data/lib/rack/auth/digest/md5.rb +9 -7
- data/lib/rack/auth/digest/nonce.rb +6 -3
- data/lib/rack/auth/digest/params.rb +5 -4
- data/lib/rack/auth/digest/request.rb +3 -1
- data/lib/rack/body_proxy.rb +11 -9
- data/lib/rack/builder.rb +42 -18
- data/lib/rack/cascade.rb +6 -5
- data/lib/rack/chunked.rb +33 -10
- data/lib/rack/{commonlogger.rb → common_logger.rb} +14 -10
- data/lib/rack/{conditionalget.rb → conditional_get.rb} +3 -1
- data/lib/rack/config.rb +2 -0
- data/lib/rack/content_length.rb +5 -3
- data/lib/rack/content_type.rb +3 -1
- data/lib/rack/core_ext/regexp.rb +14 -0
- data/lib/rack/deflater.rb +33 -53
- data/lib/rack/directory.rb +75 -60
- data/lib/rack/etag.rb +8 -5
- data/lib/rack/events.rb +156 -0
- data/lib/rack/file.rb +4 -149
- data/lib/rack/files.rb +178 -0
- data/lib/rack/handler/cgi.rb +18 -17
- data/lib/rack/handler/fastcgi.rb +17 -16
- data/lib/rack/handler/lsws.rb +14 -12
- data/lib/rack/handler/scgi.rb +22 -19
- data/lib/rack/handler/thin.rb +6 -1
- data/lib/rack/handler/webrick.rb +28 -28
- data/lib/rack/handler.rb +9 -26
- data/lib/rack/head.rb +17 -17
- data/lib/rack/lint.rb +55 -52
- data/lib/rack/lobster.rb +8 -6
- data/lib/rack/lock.rb +17 -10
- data/lib/rack/logger.rb +4 -2
- data/lib/rack/media_type.rb +43 -0
- data/lib/rack/{methodoverride.rb → method_override.rb} +10 -8
- data/lib/rack/mime.rb +27 -6
- data/lib/rack/mock.rb +101 -60
- data/lib/rack/multipart/generator.rb +11 -12
- data/lib/rack/multipart/parser.rb +292 -161
- data/lib/rack/multipart/uploaded_file.rb +3 -2
- data/lib/rack/multipart.rb +38 -8
- data/lib/rack/{nulllogger.rb → null_logger.rb} +3 -1
- data/lib/rack/query_parser.rb +218 -0
- data/lib/rack/recursive.rb +11 -9
- data/lib/rack/reloader.rb +10 -4
- data/lib/rack/request.rb +447 -305
- data/lib/rack/response.rb +196 -83
- data/lib/rack/rewindable_input.rb +5 -14
- data/lib/rack/runtime.rb +12 -18
- data/lib/rack/sendfile.rb +19 -14
- data/lib/rack/server.rb +118 -41
- data/lib/rack/session/abstract/id.rb +139 -94
- data/lib/rack/session/cookie.rb +34 -26
- data/lib/rack/session/memcache.rb +4 -93
- data/lib/rack/session/pool.rb +12 -10
- data/lib/rack/show_exceptions.rb +392 -0
- data/lib/rack/{showstatus.rb → show_status.rb} +7 -5
- data/lib/rack/static.rb +41 -11
- data/lib/rack/tempfile_reaper.rb +4 -2
- data/lib/rack/urlmap.rb +25 -15
- data/lib/rack/utils.rb +203 -277
- data/lib/rack.rb +76 -24
- data/rack.gemspec +25 -14
- metadata +62 -183
- data/HISTORY.md +0 -375
- data/KNOWN-ISSUES +0 -44
- data/lib/rack/backports/uri/common_18.rb +0 -56
- data/lib/rack/backports/uri/common_192.rb +0 -52
- data/lib/rack/backports/uri/common_193.rb +0 -29
- data/lib/rack/handler/evented_mongrel.rb +0 -8
- data/lib/rack/handler/mongrel.rb +0 -106
- data/lib/rack/handler/swiftiplied_mongrel.rb +0 -8
- data/lib/rack/showexceptions.rb +0 -387
- data/lib/rack/utils/okjson.rb +0 -600
- 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 -8
- data/test/cgi/test.ru +0 -5
- data/test/gemloader.rb +0 -10
- 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_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_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/semicolon +0 -6
- data/test/multipart/text +0 -15
- data/test/multipart/three_files_three_fields +0 -31
- 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 -81
- data/test/spec_auth_digest.rb +0 -259
- data/test/spec_body_proxy.rb +0 -85
- data/test/spec_builder.rb +0 -223
- data/test/spec_cascade.rb +0 -61
- data/test/spec_cgi.rb +0 -102
- data/test/spec_chunked.rb +0 -101
- data/test/spec_commonlogger.rb +0 -93
- data/test/spec_conditionalget.rb +0 -102
- data/test/spec_config.rb +0 -22
- data/test/spec_content_length.rb +0 -85
- data/test/spec_content_type.rb +0 -45
- data/test/spec_deflater.rb +0 -339
- data/test/spec_directory.rb +0 -88
- data/test/spec_etag.rb +0 -107
- data/test/spec_fastcgi.rb +0 -107
- data/test/spec_file.rb +0 -221
- data/test/spec_handler.rb +0 -72
- data/test/spec_head.rb +0 -45
- data/test/spec_lint.rb +0 -550
- data/test/spec_lobster.rb +0 -58
- data/test/spec_lock.rb +0 -164
- data/test/spec_logger.rb +0 -23
- data/test/spec_methodoverride.rb +0 -111
- data/test/spec_mime.rb +0 -51
- data/test/spec_mock.rb +0 -297
- data/test/spec_mongrel.rb +0 -182
- data/test/spec_multipart.rb +0 -600
- data/test/spec_nulllogger.rb +0 -20
- data/test/spec_recursive.rb +0 -72
- data/test/spec_request.rb +0 -1232
- data/test/spec_response.rb +0 -407
- data/test/spec_rewindable_input.rb +0 -118
- data/test/spec_runtime.rb +0 -49
- data/test/spec_sendfile.rb +0 -130
- data/test/spec_server.rb +0 -167
- data/test/spec_session_abstract_id.rb +0 -53
- data/test/spec_session_cookie.rb +0 -410
- data/test/spec_session_memcache.rb +0 -358
- data/test/spec_session_persisted_secure_secure_session_hash.rb +0 -73
- data/test/spec_session_pool.rb +0 -246
- data/test/spec_showexceptions.rb +0 -98
- data/test/spec_showstatus.rb +0 -103
- data/test/spec_static.rb +0 -145
- data/test/spec_tempfile_reaper.rb +0 -63
- data/test/spec_thin.rb +0 -91
- data/test/spec_urlmap.rb +0 -236
- data/test/spec_utils.rb +0 -647
- data/test/spec_version.rb +0 -17
- data/test/spec_webrick.rb +0 -184
- data/test/static/another/index.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,252 +1,383 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
require 'rack/utils'
|
|
4
|
+
require 'strscan'
|
|
5
|
+
require 'rack/core_ext/regexp'
|
|
2
6
|
|
|
3
7
|
module Rack
|
|
4
8
|
module Multipart
|
|
5
9
|
class MultipartPartLimitError < Errno::EMFILE; end
|
|
10
|
+
class MultipartTotalPartLimitError < StandardError; end
|
|
6
11
|
|
|
7
12
|
class Parser
|
|
8
|
-
|
|
9
|
-
DUMMY = Struct.new(:parse).new
|
|
13
|
+
using ::Rack::RegexpExtensions
|
|
10
14
|
|
|
11
|
-
|
|
12
|
-
|
|
15
|
+
BUFSIZE = 1_048_576
|
|
16
|
+
TEXT_PLAIN = "text/plain"
|
|
17
|
+
TEMPFILE_FACTORY = lambda { |filename, content_type|
|
|
18
|
+
Tempfile.new(["RackMultipart", ::File.extname(filename.gsub("\0", '%00'))])
|
|
19
|
+
}
|
|
13
20
|
|
|
14
|
-
|
|
15
|
-
io.rewind
|
|
21
|
+
BOUNDARY_REGEX = /\A([^\n]*(?:\n|\Z))/
|
|
16
22
|
|
|
17
|
-
|
|
18
|
-
|
|
23
|
+
class BoundedIO # :nodoc:
|
|
24
|
+
def initialize(io, content_length)
|
|
25
|
+
@io = io
|
|
26
|
+
@content_length = content_length
|
|
27
|
+
@cursor = 0
|
|
28
|
+
end
|
|
19
29
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
bufsize = env['rack.multipart.buffer_size'] || BUFSIZE
|
|
30
|
+
def read(size, outbuf = nil)
|
|
31
|
+
return if @cursor >= @content_length
|
|
23
32
|
|
|
24
|
-
|
|
25
|
-
end
|
|
33
|
+
left = @content_length - @cursor
|
|
26
34
|
|
|
27
|
-
|
|
28
|
-
|
|
35
|
+
str = if left < size
|
|
36
|
+
@io.read left, outbuf
|
|
37
|
+
else
|
|
38
|
+
@io.read size, outbuf
|
|
39
|
+
end
|
|
29
40
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
41
|
+
if str
|
|
42
|
+
@cursor += str.bytesize
|
|
43
|
+
else
|
|
44
|
+
# Raise an error for mismatching Content-Length and actual contents
|
|
45
|
+
raise EOFError, "bad content body"
|
|
46
|
+
end
|
|
33
47
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
@io = io
|
|
37
|
-
@content_length = content_length
|
|
38
|
-
@boundary_size = Utils.bytesize(@boundary) + EOL.size
|
|
39
|
-
@env = env
|
|
40
|
-
@tempfile = tempfile
|
|
41
|
-
@bufsize = bufsize
|
|
48
|
+
str
|
|
49
|
+
end
|
|
42
50
|
|
|
43
|
-
|
|
44
|
-
@
|
|
51
|
+
def rewind
|
|
52
|
+
@io.rewind
|
|
45
53
|
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
MultipartInfo = Struct.new :params, :tmp_files
|
|
57
|
+
EMPTY = MultipartInfo.new(nil, [])
|
|
46
58
|
|
|
47
|
-
|
|
48
|
-
|
|
59
|
+
def self.parse_boundary(content_type)
|
|
60
|
+
return unless content_type
|
|
61
|
+
data = content_type.match(MULTIPART)
|
|
62
|
+
return unless data
|
|
63
|
+
data[1]
|
|
49
64
|
end
|
|
50
65
|
|
|
51
|
-
def parse
|
|
52
|
-
|
|
66
|
+
def self.parse(io, content_length, content_type, tmpfile, bufsize, qp)
|
|
67
|
+
return EMPTY if 0 == content_length
|
|
68
|
+
|
|
69
|
+
boundary = parse_boundary content_type
|
|
70
|
+
return EMPTY unless boundary
|
|
71
|
+
|
|
72
|
+
io = BoundedIO.new(io, content_length) if content_length
|
|
73
|
+
outbuf = String.new
|
|
74
|
+
|
|
75
|
+
parser = new(boundary, tmpfile, bufsize, qp)
|
|
76
|
+
parser.on_read io.read(bufsize, outbuf)
|
|
53
77
|
|
|
54
|
-
opened_files = 0
|
|
55
78
|
loop do
|
|
79
|
+
break if parser.state == :DONE
|
|
80
|
+
parser.on_read io.read(bufsize, outbuf)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
io.rewind
|
|
84
|
+
parser.result
|
|
85
|
+
end
|
|
56
86
|
|
|
57
|
-
|
|
58
|
-
|
|
87
|
+
class Collector
|
|
88
|
+
class MimePart < Struct.new(:body, :head, :filename, :content_type, :name)
|
|
89
|
+
def get_data
|
|
90
|
+
data = body
|
|
91
|
+
if filename == ""
|
|
92
|
+
# filename is blank which means no file has been selected
|
|
93
|
+
return
|
|
94
|
+
elsif filename
|
|
95
|
+
body.rewind if body.respond_to?(:rewind)
|
|
96
|
+
|
|
97
|
+
# Take the basename of the upload's original filename.
|
|
98
|
+
# This handles the full Windows paths given by Internet Explorer
|
|
99
|
+
# (and perhaps other broken user agents) without affecting
|
|
100
|
+
# those which give the lone filename.
|
|
101
|
+
fn = filename.split(/[\/\\]/).last
|
|
102
|
+
|
|
103
|
+
data = { filename: fn, type: content_type,
|
|
104
|
+
name: name, tempfile: body, head: head }
|
|
105
|
+
elsif !filename && content_type && body.is_a?(IO)
|
|
106
|
+
body.rewind
|
|
107
|
+
|
|
108
|
+
# Generic multipart cases, not coming from a form
|
|
109
|
+
data = { type: content_type,
|
|
110
|
+
name: name, tempfile: body, head: head }
|
|
111
|
+
end
|
|
59
112
|
|
|
60
|
-
|
|
61
|
-
opened_files += 1 if filename
|
|
62
|
-
raise MultipartPartLimitError, 'Maximum file multiparts in content reached' if opened_files >= Utils.multipart_part_limit
|
|
113
|
+
yield data
|
|
63
114
|
end
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
class BufferPart < MimePart
|
|
118
|
+
def file?; false; end
|
|
119
|
+
def close; end
|
|
120
|
+
end
|
|
64
121
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
122
|
+
class TempfilePart < MimePart
|
|
123
|
+
def file?; true; end
|
|
124
|
+
def close; body.close; end
|
|
125
|
+
end
|
|
69
126
|
|
|
70
|
-
|
|
71
|
-
end
|
|
127
|
+
include Enumerable
|
|
72
128
|
|
|
73
|
-
|
|
74
|
-
|
|
129
|
+
def initialize tempfile
|
|
130
|
+
@tempfile = tempfile
|
|
131
|
+
@mime_parts = []
|
|
132
|
+
@open_files = 0
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def each
|
|
136
|
+
@mime_parts.each { |part| yield part }
|
|
137
|
+
end
|
|
75
138
|
|
|
76
|
-
|
|
139
|
+
def on_mime_head mime_index, head, filename, content_type, name
|
|
140
|
+
if filename
|
|
141
|
+
body = @tempfile.call(filename, content_type)
|
|
142
|
+
body.binmode if body.respond_to?(:binmode)
|
|
143
|
+
klass = TempfilePart
|
|
144
|
+
@open_files += 1
|
|
145
|
+
else
|
|
146
|
+
body = String.new
|
|
147
|
+
klass = BufferPart
|
|
77
148
|
end
|
|
78
149
|
|
|
79
|
-
|
|
80
|
-
break if (@buf.empty? && $1 != EOL) || @content_length == -1
|
|
81
|
-
end
|
|
150
|
+
@mime_parts[mime_index] = klass.new(body, head, filename, content_type, name)
|
|
82
151
|
|
|
83
|
-
|
|
152
|
+
check_part_limits
|
|
153
|
+
end
|
|
84
154
|
|
|
85
|
-
|
|
86
|
-
|
|
155
|
+
def on_mime_body mime_index, content
|
|
156
|
+
@mime_parts[mime_index].body << content
|
|
157
|
+
end
|
|
87
158
|
|
|
88
|
-
|
|
89
|
-
|
|
159
|
+
def on_mime_finish mime_index
|
|
160
|
+
end
|
|
90
161
|
|
|
91
|
-
|
|
162
|
+
private
|
|
92
163
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
raise EOFError, "bad content body" unless content
|
|
97
|
-
@buf << content
|
|
164
|
+
def check_part_limits
|
|
165
|
+
file_limit = Utils.multipart_file_limit
|
|
166
|
+
part_limit = Utils.multipart_total_part_limit
|
|
98
167
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
168
|
+
if file_limit && file_limit > 0
|
|
169
|
+
if @open_files >= file_limit
|
|
170
|
+
@mime_parts.each(&:close)
|
|
171
|
+
raise MultipartPartLimitError, 'Maximum file multiparts in content reached'
|
|
172
|
+
end
|
|
102
173
|
end
|
|
103
174
|
|
|
104
|
-
|
|
175
|
+
if part_limit && part_limit > 0
|
|
176
|
+
if @mime_parts.size >= part_limit
|
|
177
|
+
@mime_parts.each(&:close)
|
|
178
|
+
raise MultipartTotalPartLimitError, 'Maximum total multiparts in content reached'
|
|
179
|
+
end
|
|
180
|
+
end
|
|
105
181
|
end
|
|
106
182
|
end
|
|
107
183
|
|
|
108
|
-
|
|
109
|
-
head = nil
|
|
110
|
-
body = ''
|
|
184
|
+
attr_reader :state
|
|
111
185
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
186
|
+
def initialize(boundary, tempfile, bufsize, query_parser)
|
|
187
|
+
@query_parser = query_parser
|
|
188
|
+
@params = query_parser.make_params
|
|
189
|
+
@boundary = "--#{boundary}"
|
|
190
|
+
@bufsize = bufsize
|
|
115
191
|
|
|
116
|
-
|
|
192
|
+
@full_boundary = @boundary
|
|
193
|
+
@end_boundary = @boundary + '--'
|
|
194
|
+
@state = :FAST_FORWARD
|
|
195
|
+
@mime_index = 0
|
|
196
|
+
@collector = Collector.new tempfile
|
|
117
197
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
198
|
+
@sbuf = StringScanner.new("".dup)
|
|
199
|
+
@body_regex = /(?:#{EOL})?#{Regexp.quote(@boundary)}(?:#{EOL}|--)/m
|
|
200
|
+
@rx_max_size = EOL.size + @boundary.bytesize + [EOL.size, '--'.size].max
|
|
201
|
+
@head_regex = /(.*?#{EOL})#{EOL}/m
|
|
202
|
+
end
|
|
121
203
|
|
|
122
|
-
|
|
204
|
+
def on_read content
|
|
205
|
+
handle_empty_content!(content)
|
|
206
|
+
@sbuf.concat content
|
|
207
|
+
run_parser
|
|
208
|
+
end
|
|
123
209
|
|
|
124
|
-
|
|
125
|
-
|
|
210
|
+
def result
|
|
211
|
+
@collector.each do |part|
|
|
212
|
+
part.get_data do |data|
|
|
213
|
+
tag_multipart_encoding(part.filename, part.content_type, part.name, data)
|
|
214
|
+
@query_parser.normalize_params(@params, part.name, data, @query_parser.param_depth_limit)
|
|
215
|
+
end
|
|
216
|
+
end
|
|
217
|
+
MultipartInfo.new @params.to_params_hash, @collector.find_all(&:file?).map(&:body)
|
|
218
|
+
end
|
|
126
219
|
|
|
127
|
-
|
|
220
|
+
private
|
|
128
221
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
222
|
+
def run_parser
|
|
223
|
+
loop do
|
|
224
|
+
case @state
|
|
225
|
+
when :FAST_FORWARD
|
|
226
|
+
break if handle_fast_forward == :want_read
|
|
227
|
+
when :CONSUME_TOKEN
|
|
228
|
+
break if handle_consume_token == :want_read
|
|
229
|
+
when :MIME_HEAD
|
|
230
|
+
break if handle_mime_head == :want_read
|
|
231
|
+
when :MIME_BODY
|
|
232
|
+
break if handle_mime_body == :want_read
|
|
233
|
+
when :DONE
|
|
234
|
+
break
|
|
235
|
+
end
|
|
236
|
+
end
|
|
237
|
+
end
|
|
132
238
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
239
|
+
def handle_fast_forward
|
|
240
|
+
if consume_boundary
|
|
241
|
+
@state = :MIME_HEAD
|
|
242
|
+
else
|
|
243
|
+
raise EOFError, "bad content body" if @sbuf.rest_size >= @bufsize
|
|
244
|
+
:want_read
|
|
245
|
+
end
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
def handle_consume_token
|
|
249
|
+
tok = consume_boundary
|
|
250
|
+
# break if we're at the end of a buffer, but not if it is the end of a field
|
|
251
|
+
@state = if tok == :END_BOUNDARY || (@sbuf.eos? && tok != :BOUNDARY)
|
|
252
|
+
:DONE
|
|
253
|
+
else
|
|
254
|
+
:MIME_HEAD
|
|
255
|
+
end
|
|
256
|
+
end
|
|
137
257
|
|
|
138
|
-
|
|
258
|
+
def handle_mime_head
|
|
259
|
+
if @sbuf.scan_until(@head_regex)
|
|
260
|
+
head = @sbuf[1]
|
|
261
|
+
content_type = head[MULTIPART_CONTENT_TYPE, 1]
|
|
262
|
+
if name = head[MULTIPART_CONTENT_DISPOSITION, 1]
|
|
263
|
+
name = Rack::Auth::Digest::Params::dequote(name)
|
|
264
|
+
else
|
|
265
|
+
name = head[MULTIPART_CONTENT_ID, 1]
|
|
139
266
|
end
|
|
140
267
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
268
|
+
filename = get_filename(head)
|
|
269
|
+
|
|
270
|
+
if name.nil? || name.empty?
|
|
271
|
+
name = filename || "#{content_type || TEXT_PLAIN}[]".dup
|
|
144
272
|
end
|
|
145
273
|
|
|
146
|
-
|
|
147
|
-
|
|
274
|
+
@collector.on_mime_head @mime_index, head, filename, content_type, name
|
|
275
|
+
@state = :MIME_BODY
|
|
276
|
+
else
|
|
277
|
+
:want_read
|
|
278
|
+
end
|
|
279
|
+
end
|
|
148
280
|
|
|
149
|
-
|
|
150
|
-
|
|
281
|
+
def handle_mime_body
|
|
282
|
+
if (body_with_boundary = @sbuf.check_until(@body_regex)) # check but do not advance the pointer yet
|
|
283
|
+
body = body_with_boundary.sub(/#{@body_regex}\z/m, '') # remove the boundary from the string
|
|
284
|
+
@collector.on_mime_body @mime_index, body
|
|
285
|
+
@sbuf.pos += body.length + 2 # skip \r\n after the content
|
|
286
|
+
@state = :CONSUME_TOKEN
|
|
287
|
+
@mime_index += 1
|
|
288
|
+
else
|
|
289
|
+
# Save what we have so far
|
|
290
|
+
if @rx_max_size < @sbuf.rest_size
|
|
291
|
+
delta = @sbuf.rest_size - @rx_max_size
|
|
292
|
+
@collector.on_mime_body @mime_index, @sbuf.peek(delta)
|
|
293
|
+
@sbuf.pos += delta
|
|
294
|
+
@sbuf.string = @sbuf.rest
|
|
295
|
+
end
|
|
296
|
+
:want_read
|
|
151
297
|
end
|
|
298
|
+
end
|
|
152
299
|
|
|
153
|
-
|
|
300
|
+
def full_boundary; @full_boundary; end
|
|
301
|
+
|
|
302
|
+
def consume_boundary
|
|
303
|
+
while read_buffer = @sbuf.scan_until(BOUNDARY_REGEX)
|
|
304
|
+
case read_buffer.strip
|
|
305
|
+
when full_boundary then return :BOUNDARY
|
|
306
|
+
when @end_boundary then return :END_BOUNDARY
|
|
307
|
+
end
|
|
308
|
+
return if @sbuf.eos?
|
|
309
|
+
end
|
|
154
310
|
end
|
|
155
311
|
|
|
156
312
|
def get_filename(head)
|
|
157
313
|
filename = nil
|
|
158
314
|
case head
|
|
159
315
|
when RFC2183
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
316
|
+
params = Hash[*head.scan(DISPPARM).flat_map(&:compact)]
|
|
317
|
+
|
|
318
|
+
if filename = params['filename']
|
|
319
|
+
filename = $1 if filename =~ /^"(.*)"$/
|
|
320
|
+
elsif filename = params['filename*']
|
|
321
|
+
encoding, _, filename = filename.split("'", 3)
|
|
322
|
+
end
|
|
323
|
+
when BROKEN
|
|
163
324
|
filename = $1
|
|
325
|
+
filename = $1 if filename =~ /^"(.*)"$/
|
|
164
326
|
end
|
|
165
327
|
|
|
166
328
|
return unless filename
|
|
167
329
|
|
|
168
|
-
if filename.scan(/%.?.?/).all? { |s|
|
|
169
|
-
filename = Utils.
|
|
330
|
+
if filename.scan(/%.?.?/).all? { |s| /%[0-9a-fA-F]{2}/.match?(s) }
|
|
331
|
+
filename = Utils.unescape_path(filename)
|
|
170
332
|
end
|
|
171
333
|
|
|
172
|
-
|
|
334
|
+
filename.scrub!
|
|
173
335
|
|
|
174
336
|
if filename !~ /\\[^\\"]/
|
|
175
337
|
filename = filename.gsub(/\\(.)/, '\1')
|
|
176
338
|
end
|
|
177
|
-
filename
|
|
178
|
-
end
|
|
179
339
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
unless filename.valid_encoding?
|
|
183
|
-
# FIXME: this force_encoding is for Ruby 2.0 and 1.9 support.
|
|
184
|
-
# We can remove it after they are dropped
|
|
185
|
-
filename.force_encoding(Encoding::ASCII_8BIT)
|
|
186
|
-
filename.encode!(:invalid => :replace, :undef => :replace)
|
|
187
|
-
end
|
|
340
|
+
if encoding
|
|
341
|
+
filename.force_encoding ::Encoding.find(encoding)
|
|
188
342
|
end
|
|
189
343
|
|
|
190
|
-
|
|
191
|
-
|
|
344
|
+
filename
|
|
345
|
+
end
|
|
346
|
+
|
|
347
|
+
CHARSET = "charset"
|
|
192
348
|
|
|
193
|
-
|
|
194
|
-
|
|
349
|
+
def tag_multipart_encoding(filename, content_type, name, body)
|
|
350
|
+
name = name.to_s
|
|
351
|
+
encoding = Encoding::UTF_8
|
|
195
352
|
|
|
196
|
-
|
|
353
|
+
name.force_encoding(encoding)
|
|
197
354
|
|
|
198
|
-
|
|
355
|
+
return if filename
|
|
199
356
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
357
|
+
if content_type
|
|
358
|
+
list = content_type.split(';')
|
|
359
|
+
type_subtype = list.first
|
|
360
|
+
type_subtype.strip!
|
|
361
|
+
if TEXT_PLAIN == type_subtype
|
|
362
|
+
rest = list.drop 1
|
|
363
|
+
rest.each do |param|
|
|
364
|
+
k, v = param.split('=', 2)
|
|
365
|
+
k.strip!
|
|
366
|
+
v.strip!
|
|
367
|
+
v = v[1..-2] if v.start_with?('"') && v.end_with?('"')
|
|
368
|
+
encoding = Encoding.find v if k == CHARSET
|
|
212
369
|
end
|
|
213
370
|
end
|
|
214
|
-
|
|
215
|
-
name.force_encoding encoding
|
|
216
|
-
body.force_encoding encoding
|
|
217
|
-
end
|
|
218
|
-
else
|
|
219
|
-
def scrub_filename(filename)
|
|
220
|
-
end
|
|
221
|
-
def tag_multipart_encoding(filename, content_type, name, body)
|
|
222
371
|
end
|
|
223
|
-
end
|
|
224
|
-
|
|
225
|
-
def get_data(filename, body, content_type, name, head)
|
|
226
|
-
data = body
|
|
227
|
-
if filename == ""
|
|
228
|
-
# filename is blank which means no file has been selected
|
|
229
|
-
return
|
|
230
|
-
elsif filename
|
|
231
|
-
body.rewind if body.respond_to?(:rewind)
|
|
232
|
-
|
|
233
|
-
# Take the basename of the upload's original filename.
|
|
234
|
-
# This handles the full Windows paths given by Internet Explorer
|
|
235
|
-
# (and perhaps other broken user agents) without affecting
|
|
236
|
-
# those which give the lone filename.
|
|
237
|
-
filename = filename.split(/[\/\\]/).last
|
|
238
372
|
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
body.rewind
|
|
373
|
+
name.force_encoding(encoding)
|
|
374
|
+
body.force_encoding(encoding)
|
|
375
|
+
end
|
|
243
376
|
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
377
|
+
def handle_empty_content!(content)
|
|
378
|
+
if content.nil? || content.empty?
|
|
379
|
+
raise EOFError
|
|
247
380
|
end
|
|
248
|
-
|
|
249
|
-
yield data
|
|
250
381
|
end
|
|
251
382
|
end
|
|
252
383
|
end
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module Rack
|
|
2
4
|
module Multipart
|
|
3
5
|
class UploadedFile
|
|
@@ -11,8 +13,7 @@ module Rack
|
|
|
11
13
|
raise "#{path} file does not exist" unless ::File.exist?(path)
|
|
12
14
|
@content_type = content_type
|
|
13
15
|
@original_filename = ::File.basename(path)
|
|
14
|
-
@tempfile = Tempfile.new([@original_filename, ::File.extname(path)])
|
|
15
|
-
@tempfile.set_encoding(Encoding::BINARY) if @tempfile.respond_to?(:set_encoding)
|
|
16
|
+
@tempfile = Tempfile.new([@original_filename, ::File.extname(path)], encoding: Encoding::BINARY)
|
|
16
17
|
@tempfile.binmode if binary
|
|
17
18
|
FileUtils.copy_file(path, @tempfile.path)
|
|
18
19
|
end
|
data/lib/rack/multipart.rb
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'rack/multipart/parser'
|
|
4
|
+
|
|
1
5
|
module Rack
|
|
2
6
|
# A multipart form data parser, adapted from IOWA.
|
|
3
7
|
#
|
|
4
8
|
# Usually, Rack::Request#POST takes care of calling this.
|
|
5
9
|
module Multipart
|
|
6
10
|
autoload :UploadedFile, 'rack/multipart/uploaded_file'
|
|
7
|
-
autoload :Parser, 'rack/multipart/parser'
|
|
8
11
|
autoload :Generator, 'rack/multipart/generator'
|
|
9
12
|
|
|
10
13
|
EOL = "\r\n"
|
|
@@ -12,17 +15,44 @@ module Rack
|
|
|
12
15
|
MULTIPART = %r|\Amultipart/.*boundary=\"?([^\";,]+)\"?|ni
|
|
13
16
|
TOKEN = /[^\s()<>,;:\\"\/\[\]?=]+/
|
|
14
17
|
CONDISP = /Content-Disposition:\s*#{TOKEN}\s*/i
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
BROKEN_QUOTED = /^#{CONDISP}.*;\sfilename="(.*?)"(?:\s*$|\s*;\s*#{TOKEN}=)/i
|
|
18
|
-
BROKEN_UNQUOTED = /^#{CONDISP}.*;\sfilename=(#{TOKEN})/i
|
|
18
|
+
VALUE = /"(?:\\"|[^"])*"|#{TOKEN}/
|
|
19
|
+
BROKEN = /^#{CONDISP}.*;\s*filename=(#{VALUE})/i
|
|
19
20
|
MULTIPART_CONTENT_TYPE = /Content-Type: (.*)#{EOL}/ni
|
|
20
|
-
MULTIPART_CONTENT_DISPOSITION = /Content-Disposition
|
|
21
|
+
MULTIPART_CONTENT_DISPOSITION = /Content-Disposition:[^:]*;\s*name=(#{VALUE})/ni
|
|
21
22
|
MULTIPART_CONTENT_ID = /Content-ID:\s*([^#{EOL}]*)/ni
|
|
23
|
+
# Updated definitions from RFC 2231
|
|
24
|
+
ATTRIBUTE_CHAR = %r{[^ \x00-\x1f\x7f)(><@,;:\\"/\[\]?='*%]}
|
|
25
|
+
ATTRIBUTE = /#{ATTRIBUTE_CHAR}+/
|
|
26
|
+
SECTION = /\*[0-9]+/
|
|
27
|
+
REGULAR_PARAMETER_NAME = /#{ATTRIBUTE}#{SECTION}?/
|
|
28
|
+
REGULAR_PARAMETER = /(#{REGULAR_PARAMETER_NAME})=(#{VALUE})/
|
|
29
|
+
EXTENDED_OTHER_NAME = /#{ATTRIBUTE}\*[1-9][0-9]*\*/
|
|
30
|
+
EXTENDED_OTHER_VALUE = /%[0-9a-fA-F]{2}|#{ATTRIBUTE_CHAR}/
|
|
31
|
+
EXTENDED_OTHER_PARAMETER = /(#{EXTENDED_OTHER_NAME})=(#{EXTENDED_OTHER_VALUE}*)/
|
|
32
|
+
EXTENDED_INITIAL_NAME = /#{ATTRIBUTE}(?:\*0)?\*/
|
|
33
|
+
EXTENDED_INITIAL_VALUE = /[a-zA-Z0-9\-]*'[a-zA-Z0-9\-]*'#{EXTENDED_OTHER_VALUE}*/
|
|
34
|
+
EXTENDED_INITIAL_PARAMETER = /(#{EXTENDED_INITIAL_NAME})=(#{EXTENDED_INITIAL_VALUE})/
|
|
35
|
+
EXTENDED_PARAMETER = /#{EXTENDED_INITIAL_PARAMETER}|#{EXTENDED_OTHER_PARAMETER}/
|
|
36
|
+
DISPPARM = /;\s*(?:#{REGULAR_PARAMETER}|#{EXTENDED_PARAMETER})\s*/
|
|
37
|
+
RFC2183 = /^#{CONDISP}(#{DISPPARM})+$/i
|
|
22
38
|
|
|
23
39
|
class << self
|
|
24
|
-
def parse_multipart(env)
|
|
25
|
-
|
|
40
|
+
def parse_multipart(env, params = Rack::Utils.default_query_parser)
|
|
41
|
+
extract_multipart Rack::Request.new(env), params
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def extract_multipart(req, params = Rack::Utils.default_query_parser)
|
|
45
|
+
io = req.get_header(RACK_INPUT)
|
|
46
|
+
io.rewind
|
|
47
|
+
content_length = req.content_length
|
|
48
|
+
content_length = content_length.to_i if content_length
|
|
49
|
+
|
|
50
|
+
tempfile = req.get_header(RACK_MULTIPART_TEMPFILE_FACTORY) || Parser::TEMPFILE_FACTORY
|
|
51
|
+
bufsize = req.get_header(RACK_MULTIPART_BUFFER_SIZE) || Parser::BUFSIZE
|
|
52
|
+
|
|
53
|
+
info = Parser.parse io, content_length, req.get_header('CONTENT_TYPE'), tempfile, bufsize, params
|
|
54
|
+
req.set_header(RACK_TEMPFILES, info.tmp_files)
|
|
55
|
+
info.params
|
|
26
56
|
end
|
|
27
57
|
|
|
28
58
|
def build_multipart(params, first = true)
|