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