1require 'test/unit' 2require 'cgi' 3require 'tempfile' 4require 'stringio' 5require_relative '../ruby/envutil' 6 7 8## 9## usage: 10## boundary = 'foobar1234' # or nil 11## multipart = MultiPart.new(boundary) 12## multipart.append('name1', 'value1') 13## multipart.append('file1', File.read('file1.html'), 'file1.html') 14## str = multipart.close() 15## str.each_line {|line| p line } 16## ## output: 17## # "--foobar1234\r\n" 18## # "Content-Disposition: form-data: name=\"name1\"\r\n" 19## # "\r\n" 20## # "value1\r\n" 21## # "--foobar1234\r\n" 22## # "Content-Disposition: form-data: name=\"file1\"; filename=\"file1.html\"\r\n" 23## # "Content-Type: text/html\r\n" 24## # "\r\n" 25## # "<html>\n" 26## # "<body><p>Hello</p></body>\n" 27## # "</html>\n" 28## # "\r\n" 29## # "--foobar1234--\r\n" 30## 31class MultiPart 32 33 def initialize(boundary=nil) 34 @boundary = boundary || create_boundary() 35 @buf = '' 36 @buf.force_encoding(::Encoding::ASCII_8BIT) if defined?(::Encoding) 37 end 38 attr_reader :boundary 39 40 def append(name, value, filename=nil, content_type=nil) 41 content_type = detect_content_type(filename) if filename && content_type.nil? 42 s = filename ? "; filename=\"#{filename}\"" : '' 43 buf = @buf 44 buf << "--#{boundary}\r\n" 45 buf << "Content-Disposition: form-data: name=\"#{name}\"#{s}\r\n" 46 buf << "Content-Type: #{content_type}\r\n" if content_type 47 buf << "\r\n" 48 value = value.dup.force_encoding(::Encoding::ASCII_8BIT) if defined?(::Encoding) 49 buf << value 50 buf << "\r\n" 51 return self 52 end 53 54 def close 55 buf = @buf 56 @buf = '' 57 return buf << "--#{boundary}--\r\n" 58 end 59 60 def create_boundary() #:nodoc: 61 return "--boundary#{rand().to_s[2..-1]}" 62 end 63 64 def detect_content_type(filename) #:nodoc: 65 filename =~ /\.(\w+)\z/ 66 return MIME_TYPES[$1] || 'application/octet-stream' 67 end 68 69 MIME_TYPES = { 70 'gif' => 'image/gif', 71 'jpg' => 'image/jpeg', 72 'jpeg' => 'image/jpeg', 73 'png' => 'image/png', 74 'bmp' => 'image/bmp', 75 'tif' => 'image/tiff', 76 'tiff' => 'image/tiff', 77 'htm' => 'text/html', 78 'html' => 'text/html', 79 'xml' => 'text/xml', 80 'txt' => 'text/plain', 81 'text' => 'text/plain', 82 'css' => 'text/css', 83 'mpg' => 'video/mpeg', 84 'mpeg' => 'video/mpeg', 85 'mov' => 'video/quicktime', 86 'avi' => 'video/x-msvideo', 87 'mp3' => 'audio/mpeg', 88 'mid' => 'audio/midi', 89 'wav' => 'audio/x-wav', 90 'zip' => 'application/zip', 91 #'tar.gz' => 'application/gtar', 92 'gz' => 'application/gzip', 93 'bz2' => 'application/bzip2', 94 'rtf' => 'application/rtf', 95 'pdf' => 'application/pdf', 96 'ps' => 'application/postscript', 97 'js' => 'application/x-javascript', 98 'xls' => 'application/vnd.ms-excel', 99 'doc' => 'application/msword', 100 'ppt' => 'application/vnd.ms-powerpoint', 101 } 102 103end 104 105 106 107class CGIMultipartTest < Test::Unit::TestCase 108 109 def setup 110 ENV['REQUEST_METHOD'] = 'POST' 111 @tempfiles = [] 112 end 113 114 def teardown 115 %w[ REQUEST_METHOD CONTENT_TYPE CONTENT_LENGTH REQUEST_METHOD ].each do |name| 116 ENV.delete(name) 117 end 118 $stdin.close() if $stdin.is_a?(Tempfile) 119 $stdin = STDIN 120 @tempfiles.each {|t| 121 t.unlink 122 } 123 end 124 125 def _prepare(data) 126 ## create multipart input 127 multipart = MultiPart.new(defined?(@boundary) ? @boundary : nil) 128 data.each do |hash| 129 multipart.append(hash[:name], hash[:value], hash[:filename]) 130 end 131 input = multipart.close() 132 input = yield(input) if block_given? 133 #$stderr.puts "*** debug: input=\n#{input.collect{|line| line.inspect}.join("\n")}" 134 @boundary ||= multipart.boundary 135 ## set environment 136 ENV['CONTENT_TYPE'] = "multipart/form-data; boundary=#{@boundary}" 137 ENV['CONTENT_LENGTH'] = input.length.to_s 138 ENV['REQUEST_METHOD'] = 'POST' 139 ## set $stdin 140 tmpfile = Tempfile.new('test_cgi_multipart') 141 @tempfiles << tmpfile 142 tmpfile.binmode 143 tmpfile << input 144 tmpfile.rewind() 145 $stdin = tmpfile 146 end 147 148 def _test_multipart 149 caller(0).find {|s| s =~ /in `test_(.*?)'/ } 150 #testname = $1 151 #$stderr.puts "*** debug: testname=#{testname.inspect}" 152 _prepare(@data) 153 cgi = RUBY_VERSION>="1.9" ? CGI.new(:accept_charset=>"UTF-8") : CGI.new 154 expected_names = @data.collect{|hash| hash[:name] }.sort 155 assert_equal(expected_names, cgi.params.keys.sort) 156 threshold = 1024*10 157 @data.each do |hash| 158 name = hash[:name] 159 expected = hash[:value] 160 if RUBY_VERSION>="1.9" 161 if hash[:filename] #if file 162 expected_class = @expected_class || (hash[:value].length < threshold ? StringIO : Tempfile) 163 assert(cgi.files.keys.member?(hash[:name])) 164 else 165 expected_class = String 166 assert_equal(expected, cgi[name]) 167 assert_equal(false,cgi.files.keys.member?(hash[:name])) 168 end 169 else 170 expected_class = @expected_class || (hash[:value].length < threshold ? StringIO : Tempfile) 171 end 172 assert_kind_of(expected_class, cgi[name]) 173 assert_equal(expected, cgi[name].read()) 174 assert_equal(hash[:filename] || '', cgi[name].original_filename) #if hash[:filename] 175 assert_equal(hash[:content_type] || '', cgi[name].content_type) #if hash[:content_type] 176 end 177 ensure 178 if cgi 179 cgi.params.each {|name, vals| 180 vals.each {|val| 181 if val.kind_of?(Tempfile) && val.path 182 val.unlink 183 end 184 } 185 } 186 end 187 end 188 189 190 def _read(basename) 191 filename = File.join(File.dirname(__FILE__), 'testdata', basename) 192 s = File.open(filename, 'rb') {|f| f.read() } 193 194 return s 195 end 196 197 198 def test_cgi_multipart_stringio 199 @boundary = '----WebKitFormBoundaryAAfvAII+YL9102cX' 200 @data = [ 201 {:name=>'hidden1', :value=>'foobar'}, 202 {:name=>'text1', :value=>"\xE3\x81\x82\xE3\x81\x84\xE3\x81\x86\xE3\x81\x88\xE3\x81\x8A"}, 203 {:name=>'file1', :value=>_read('file1.html'), 204 :filename=>'file1.html', :content_type=>'text/html'}, 205 {:name=>'image1', :value=>_read('small.png'), 206 :filename=>'small.png', :content_type=>'image/png'}, # small image 207 ] 208 @data[1][:value].force_encoding(::Encoding::UTF_8) if defined?(::Encoding) 209 @expected_class = StringIO 210 _test_multipart() 211 end 212 213 214 def test_cgi_multipart_tempfile 215 @boundary = '----WebKitFormBoundaryAAfvAII+YL9102cX' 216 @data = [ 217 {:name=>'hidden1', :value=>'foobar'}, 218 {:name=>'text1', :value=>"\xE3\x81\x82\xE3\x81\x84\xE3\x81\x86\xE3\x81\x88\xE3\x81\x8A"}, 219 {:name=>'file1', :value=>_read('file1.html'), 220 :filename=>'file1.html', :content_type=>'text/html'}, 221 {:name=>'image1', :value=>_read('large.png'), 222 :filename=>'large.png', :content_type=>'image/png'}, # large image 223 ] 224 @data[1][:value].force_encoding(::Encoding::UTF_8) if defined?(::Encoding) 225 @expected_class = Tempfile 226 _test_multipart() 227 end 228 229 230 def _set_const(klass, name, value) 231 old = nil 232 klass.class_eval do 233 old = const_get(name) 234 remove_const(name) 235 const_set(name, value) 236 end 237 return old 238 end 239 240 241 def test_cgi_multipart_maxmultipartlength 242 @data = [ 243 {:name=>'image1', :value=>_read('large.png'), 244 :filename=>'large.png', :content_type=>'image/png'}, # large image 245 ] 246 original = _set_const(CGI, :MAX_MULTIPART_LENGTH, 2 * 1024) 247 begin 248 ex = assert_raise(StandardError) do 249 _test_multipart() 250 end 251 assert_equal("too large multipart data.", ex.message) 252 ensure 253 _set_const(CGI, :MAX_MULTIPART_LENGTH, original) 254 end 255 end if CGI.const_defined?(:MAX_MULTIPART_LENGTH) 256 257 258 def test_cgi_multipart_maxmultipartcount 259 @data = [ 260 {:name=>'file1', :value=>_read('file1.html'), 261 :filename=>'file1.html', :content_type=>'text/html'}, 262 ] 263 item = @data.first 264 500.times { @data << item } 265 #original = _set_const(CGI, :MAX_MULTIPART_COUNT, 128) 266 begin 267 ex = assert_raise(StandardError) do 268 _test_multipart() 269 end 270 assert_equal("too many parameters.", ex.message) 271 ensure 272 #_set_const(CGI, :MAX_MULTIPART_COUNT, original) 273 end 274 end if CGI.const_defined?(:MAX_MULTIPART_COUNT) 275 276 277 def test_cgi_multipart_badbody ## [ruby-dev:28470] 278 @data = [ 279 {:name=>'file1', :value=>_read('file1.html'), 280 :filename=>'file1.html', :content_type=>'text/html'}, 281 ] 282 _prepare(@data) do |input| 283 input2 = input.sub(/--(\r\n)?\z/, "\r\n") 284 assert input2 != input 285 #p input2 286 input2 287 end 288 ex = assert_raise(EOFError) do 289 RUBY_VERSION>="1.9" ? CGI.new(:accept_charset=>"UTF-8") : CGI.new 290 end 291 assert_equal("bad content body", ex.message) 292 # 293 _prepare(@data) do |input| 294 input2 = input.sub(/--(\r\n)?\z/, "") 295 assert input2 != input 296 #p input2 297 input2 298 end 299 ex = assert_raise(EOFError) do 300 RUBY_VERSION>="1.9" ? CGI.new(:accept_charset=>"UTF-8") : CGI.new 301 end 302 assert_equal("bad content body", ex.message) 303 end 304 305 306 def test_cgi_multipart_quoteboundary ## [JVN#84798830] 307 @boundary = '(.|\n)*' 308 @data = [ 309 {:name=>'hidden1', :value=>'foobar'}, 310 {:name=>'text1', :value=>"\xE3\x81\x82\xE3\x81\x84\xE3\x81\x86\xE3\x81\x88\xE3\x81\x8A"}, 311 {:name=>'file1', :value=>_read('file1.html'), 312 :filename=>'file1.html', :content_type=>'text/html'}, 313 {:name=>'image1', :value=>_read('small.png'), 314 :filename=>'small.png', :content_type=>'image/png'}, # small image 315 ] 316 @data[1][:value].force_encoding("UTF-8") if RUBY_VERSION>="1.9" 317 _prepare(@data) 318 cgi = RUBY_VERSION>="1.9" ? CGI.new(:accept_charset=>"UTF-8") : CGI.new 319 assert_equal('file1.html', cgi['file1'].original_filename) 320 end 321 322 def test_cgi_multipart_boundary_10240 # [Bug #3866] 323 @boundary = 'AaB03x' 324 @data = [ 325 {:name=>'file', :value=>"b"*10134, 326 :filename=>'file.txt', :content_type=>'text/plain'}, 327 {:name=>'foo', :value=>"bar"}, 328 ] 329 _prepare(@data) 330 cgi = RUBY_VERSION>="1.9" ? CGI.new(:accept_charset=>"UTF-8") : CGI.new 331 assert_equal(cgi['foo'], 'bar') 332 assert_equal(cgi['file'].read, 'b'*10134) 333 cgi['file'].unlink if cgi['file'].kind_of? Tempfile 334 end 335 336 def test_cgi_multipart_without_tempfile 337 assert_in_out_err([], <<-'EOM') 338 require 'cgi' 339 require 'stringio' 340 ENV['REQUEST_METHOD'] = 'POST' 341 ENV['CONTENT_TYPE'] = 'multipart/form-data; boundary=foobar1234' 342 body = <<-BODY 343--foobar1234 344Content-Disposition: form-data: name=\"name1\" 345 346value1 347--foobar1234 348Content-Disposition: form-data: name=\"file1\"; filename=\"file1.html\" 349Content-Type: text/html 350 351<html> 352<body><p>Hello</p></body> 353</html> 354 355--foobar1234-- 356BODY 357 body.gsub!(/\n/, "\r\n") 358 ENV['CONTENT_LENGTH'] = body.size.to_s 359 $stdin = StringIO.new(body) 360 CGI.new 361 EOM 362 end 363 364 ### 365 366 self.instance_methods.each do |method| 367 private method if method =~ /^test_(.*)/ && $1 != ENV['TEST'] 368 end if ENV['TEST'] 369 370end 371