# frozen_string_literal: true require 'test/unit' require 'cgi' require 'tempfile' require 'stringio' require_relative 'update_env' ## ## usage: ## boundary = 'foobar1234' # or nil ## multipart = MultiPart.new(boundary) ## multipart.append('name1', 'value1') ## multipart.append('file1', File.read('file1.html'), 'file1.html') ## str = multipart.close() ## str.each_line {|line| p line } ## ## output: ## # "--foobar1234\r\n" ## # "Content-Disposition: form-data: name=\"name1\"\r\n" ## # "\r\n" ## # "value1\r\n" ## # "--foobar1234\r\n" ## # "Content-Disposition: form-data: name=\"file1\"; filename=\"file1.html\"\r\n" ## # "Content-Type: text/html\r\n" ## # "\r\n" ## # "\n" ## # "
Hello
\n" ## # "\n" ## # "\r\n" ## # "--foobar1234--\r\n" ## class MultiPart def initialize(boundary=nil) @boundary = boundary || create_boundary() @buf = ''.dup @buf.force_encoding(::Encoding::ASCII_8BIT) if defined?(::Encoding) end attr_reader :boundary def append(name, value, filename=nil, content_type=nil) content_type = detect_content_type(filename) if filename && content_type.nil? s = filename ? "; filename=\"#{filename}\"" : '' buf = @buf buf << "--#{boundary}\r\n" buf << "Content-Disposition: form-data: name=\"#{name}\"#{s}\r\n" buf << "Content-Type: #{content_type}\r\n" if content_type buf << "\r\n" buf << value.b buf << "\r\n" return self end def close buf = @buf @buf = ''.dup return buf << "--#{boundary}--\r\n" end def create_boundary() #:nodoc: return "--boundary#{rand().to_s[2..-1]}" end def detect_content_type(filename) #:nodoc: filename =~ /\.(\w+)\z/ return MIME_TYPES[$1] || 'application/octet-stream' end MIME_TYPES = { 'gif' => 'image/gif', 'jpg' => 'image/jpeg', 'jpeg' => 'image/jpeg', 'png' => 'image/png', 'bmp' => 'image/bmp', 'tif' => 'image/tiff', 'tiff' => 'image/tiff', 'htm' => 'text/html', 'html' => 'text/html', 'xml' => 'text/xml', 'txt' => 'text/plain', 'text' => 'text/plain', 'css' => 'text/css', 'mpg' => 'video/mpeg', 'mpeg' => 'video/mpeg', 'mov' => 'video/quicktime', 'avi' => 'video/x-msvideo', 'mp3' => 'audio/mpeg', 'mid' => 'audio/midi', 'wav' => 'audio/x-wav', 'zip' => 'application/zip', #'tar.gz' => 'application/gtar', 'gz' => 'application/gzip', 'bz2' => 'application/bzip2', 'rtf' => 'application/rtf', 'pdf' => 'application/pdf', 'ps' => 'application/postscript', 'js' => 'application/x-javascript', 'xls' => 'application/vnd.ms-excel', 'doc' => 'application/msword', 'ppt' => 'application/vnd.ms-powerpoint', } end class CGIMultipartTest < Test::Unit::TestCase include UpdateEnv def setup @environ = {} update_env( 'REQUEST_METHOD' => 'POST', 'CONTENT_TYPE' => nil, 'CONTENT_LENGTH' => nil, ) @tempfiles = [] end def teardown ENV.update(@environ) $stdin.close() if $stdin.is_a?(Tempfile) $stdin = STDIN @tempfiles.each {|t| t.close! } end def _prepare(data) ## create multipart input multipart = MultiPart.new(defined?(@boundary) ? @boundary : nil) data.each do |hash| multipart.append(hash[:name], hash[:value], hash[:filename]) end input = multipart.close() input = yield(input) if block_given? #$stderr.puts "*** debug: input=\n#{input.collect{|line| line.inspect}.join("\n")}" @boundary ||= multipart.boundary ## set environment ENV['CONTENT_TYPE'] = "multipart/form-data; boundary=#{@boundary}" ENV['CONTENT_LENGTH'] = input.length.to_s ENV['REQUEST_METHOD'] = 'POST' ## set $stdin tmpfile = Tempfile.new('test_cgi_multipart') @tempfiles << tmpfile tmpfile.binmode tmpfile << input tmpfile.rewind() $stdin = tmpfile end def _test_multipart(cgi_options={}) caller(0).find {|s| s =~ /in `test_(.*?)'/ } #testname = $1 #$stderr.puts "*** debug: testname=#{testname.inspect}" _prepare(@data) options = {:accept_charset=>"UTF-8"} options.merge! cgi_options cgi = CGI.new(options) expected_names = @data.collect{|hash| hash[:name] }.sort assert_equal(expected_names, cgi.params.keys.sort) threshold = 1024*10 @data.each do |hash| name = hash[:name] expected = hash[:value] if hash[:filename] #if file expected_class = @expected_class || (hash[:value].length < threshold ? StringIO : Tempfile) assert(cgi.files.keys.member?(hash[:name])) else expected_class = String assert_equal(expected, cgi[name]) assert_equal(false,cgi.files.keys.member?(hash[:name])) end assert_kind_of(expected_class, cgi[name]) assert_equal(expected, cgi[name].read()) assert_equal(hash[:filename] || '', cgi[name].original_filename) #if hash[:filename] assert_equal(hash[:content_type] || '', cgi[name].content_type) #if hash[:content_type] end ensure if cgi cgi.params.each {|name, vals| vals.each {|val| if val.kind_of?(Tempfile) && val.path val.close! end } } end end def _read(basename) filename = File.join(File.dirname(__FILE__), 'testdata', basename) s = File.open(filename, 'rb') {|f| f.read() } return s end def test_cgi_multipart_stringio @boundary = '----WebKitFormBoundaryAAfvAII+YL9102cX' @data = [ {:name=>'hidden1', :value=>'foobar'}, {:name=>'text1', :value=>"\xE3\x81\x82\xE3\x81\x84\xE3\x81\x86\xE3\x81\x88\xE3\x81\x8A".dup}, {:name=>'file1', :value=>_read('file1.html'), :filename=>'file1.html', :content_type=>'text/html'}, {:name=>'image1', :value=>_read('small.png'), :filename=>'small.png', :content_type=>'image/png'}, # small image ] @data[1][:value].force_encoding(::Encoding::UTF_8) if defined?(::Encoding) @expected_class = StringIO _test_multipart() end def test_cgi_multipart_tempfile @boundary = '----WebKitFormBoundaryAAfvAII+YL9102cX' @data = [ {:name=>'hidden1', :value=>'foobar'}, {:name=>'text1', :value=>"\xE3\x81\x82\xE3\x81\x84\xE3\x81\x86\xE3\x81\x88\xE3\x81\x8A".dup}, {:name=>'file1', :value=>_read('file1.html'), :filename=>'file1.html', :content_type=>'text/html'}, {:name=>'image1', :value=>_read('large.png'), :filename=>'large.png', :content_type=>'image/png'}, # large image ] @data[1][:value].force_encoding(::Encoding::UTF_8) if defined?(::Encoding) @expected_class = Tempfile _test_multipart() end def _set_const(klass, name, value) old = nil klass.class_eval do old = const_get(name) remove_const(name) const_set(name, value) end return old end def test_cgi_multipart_maxmultipartlength @data = [ {:name=>'image1', :value=>_read('large.png'), :filename=>'large.png', :content_type=>'image/png'}, # large image ] begin ex = assert_raise(StandardError) do _test_multipart(:max_multipart_length=>2 * 1024) # set via simple scalar end assert_equal("too large multipart data.", ex.message) ensure end end def test_cgi_multipart_maxmultipartlength_lambda @data = [ {:name=>'image1', :value=>_read('large.png'), :filename=>'large.png', :content_type=>'image/png'}, # large image ] begin ex = assert_raise(StandardError) do _test_multipart(:max_multipart_length=>lambda{2*1024}) # set via lambda end assert_equal("too large multipart data.", ex.message) ensure end end def test_cgi_multipart_maxmultipartcount @data = [ {:name=>'file1', :value=>_read('file1.html'), :filename=>'file1.html', :content_type=>'text/html'}, ] item = @data.first 500.times { @data << item } #original = _set_const(CGI, :MAX_MULTIPART_COUNT, 128) begin ex = assert_raise(StandardError) do _test_multipart() end assert_equal("too many parameters.", ex.message) ensure #_set_const(CGI, :MAX_MULTIPART_COUNT, original) end end if CGI.const_defined?(:MAX_MULTIPART_COUNT) def test_cgi_multipart_badbody ## [ruby-dev:28470] @data = [ {:name=>'file1', :value=>_read('file1.html'), :filename=>'file1.html', :content_type=>'text/html'}, ] _prepare(@data) do |input| input2 = input.sub(/--(\r\n)?\z/, "\r\n") assert input2 != input #p input2 input2 end ex = assert_raise(EOFError) do CGI.new(:accept_charset=>"UTF-8") end assert_equal("bad content body", ex.message) # _prepare(@data) do |input| input2 = input.sub(/--(\r\n)?\z/, "") assert input2 != input #p input2 input2 end ex = assert_raise(EOFError) do CGI.new(:accept_charset=>"UTF-8") end assert_equal("bad content body", ex.message) end def test_cgi_multipart_quoteboundary ## [JVN#84798830] @boundary = '(.|\n)*' @data = [ {:name=>'hidden1', :value=>'foobar'}, {:name=>'text1', :value=>"\xE3\x81\x82\xE3\x81\x84\xE3\x81\x86\xE3\x81\x88\xE3\x81\x8A".dup}, {:name=>'file1', :value=>_read('file1.html'), :filename=>'file1.html', :content_type=>'text/html'}, {:name=>'image1', :value=>_read('small.png'), :filename=>'small.png', :content_type=>'image/png'}, # small image ] @data[1][:value].force_encoding("UTF-8") _prepare(@data) cgi = CGI.new(:accept_charset=>"UTF-8") assert_equal('file1.html', cgi['file1'].original_filename) end def test_cgi_multipart_boundary_10240 # [Bug #3866] @boundary = 'AaB03x' @data = [ {:name=>'file', :value=>"b"*10134, :filename=>'file.txt', :content_type=>'text/plain'}, {:name=>'foo', :value=>"bar"}, ] _prepare(@data) cgi = CGI.new(:accept_charset=>"UTF-8") assert_equal(cgi['foo'], 'bar') assert_equal(cgi['file'].read, 'b'*10134) cgi['file'].close! if cgi['file'].kind_of? Tempfile end def test_cgi_multipart_without_tempfile assert_in_out_err([], <<-'EOM') require 'cgi' require 'stringio' ENV['REQUEST_METHOD'] = 'POST' ENV['CONTENT_TYPE'] = 'multipart/form-data; boundary=foobar1234' body = <<-BODY --foobar1234 Content-Disposition: form-data: name=\"name1\" value1 --foobar1234 Content-Disposition: form-data: name=\"file1\"; filename=\"file1.html\" Content-Type: text/htmlHello
--foobar1234-- BODY body.gsub!(/\n/, "\r\n") ENV['CONTENT_LENGTH'] = body.size.to_s $stdin = StringIO.new(body) CGI.new EOM end ### self.instance_methods.each do |method| private method if method =~ /^test_(.*)/ && $1 != ENV['TEST'] end if ENV['TEST'] end