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