• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1# encoding: ASCII-8BIT
2
3# iExploder - Generates bad HTML files to perform QA for web browsers.
4#
5# Copyright 2010 Thomas Stromberg - All Rights Reserved.
6#
7# Licensed under the Apache License, Version 2.0 (the "License");
8# you may not use this file except in compliance with the License.
9# You may obtain a copy of the License at
10#
11#      http://www.apache.org/licenses/LICENSE-2.0
12#
13# Unless required by applicable law or agreed to in writing, software
14# distributed under the License is distributed on an "AS IS" BASIS,
15# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16# See the License for the specific language governing permissions and
17# limitations under the License.
18
19require 'cgi'
20require 'yaml'
21
22require './scanner.rb'
23require './version.rb'
24
25# Used to speed up subtest generation
26$TEST_CACHE = {}
27
28# Media extensions to proper mime type map (not that we always listen'
29$MIME_MAP = {
30  'bmp' => 'image/bmp',
31  'gif' => 'image/gif',
32  'jpg' => 'image/jpeg',
33  'png' => 'image/png',
34  'svg' => 'image/svg+xml',
35  'tiff' => 'image/tiff',
36  'xbm' => 'image/xbm',
37  'ico' => 'image/x-icon',
38  'jng' => 'image/x-jng',
39  'xpm' => 'image/x-portable-pixmap',
40  'ogg' => 'audio/ogg',
41  'snd' => 'audio/basic',
42  'wav' => 'audio/wav'
43}
44
45# These tags get src properties more often than others
46$SRC_TAGS = ['img', 'audio', 'video', 'embed']
47
48class IExploder
49  attr_accessor :test_num, :subtest_data, :lookup_mode, :random_mode, :cgi_url, :browser, :claimed_browser
50  attr_accessor :offset, :lines, :stop_num, :config
51
52  def initialize(config_path)
53    @config = YAML::load(File.open(config_path))
54    @stop_num = nil
55    @subtest_data = nil
56    @test_num = 0
57    @cgi_url = '/iexploder.cgi'
58    @browser = 'UNKNOWN'
59    @claimed_browser = nil
60    readTagFiles()
61    return nil
62  end
63
64  def setRandomSeed
65    if @test_num > 0
66      srand(@test_num)
67    else
68      srand
69    end
70  end
71
72
73  def readTagFiles
74    # These if statements are so that mod_ruby doesn't have to reload the files
75    # each time
76    data_path = @config['mangle_data_path']
77    @cssTags = readTagsDir("#{data_path}/css-properties")
78    @cssPseudoTags = readTagsDir("#{data_path}/css-pseudo")
79    @cssAtRules = readTagsDir("#{data_path}/css-atrules")
80    @htmlTags = readTagsDir("#{data_path}/html-tags")
81    @htmlAttr = readTagsDir("#{data_path}/html-attrs")
82    @htmlValues = readTagsDir("#{data_path}/html-values")
83    @cssValues = readTagsDir("#{data_path}/css-values")
84    @headerValues = readTagsDir("#{data_path}/headers")
85    @protocolValues = readTagsDir("#{data_path}/protocols")
86    @mimeTypes = readTagsDir("#{data_path}/mime-types")
87    @media = readMediaDir("#{data_path}/media")
88  end
89
90  def readTagsDir(directory)
91    values = []
92    Dir.foreach(directory) { |filename|
93      if File.file?(directory + "/" + filename)
94        values = values + readTagFile(directory + "/" + filename)
95      end
96    }
97    return values.uniq
98  end
99
100  def readMediaDir(directory)
101    data = {}
102    Dir.foreach(directory) { |filename|
103      if File.file?(directory + "/" + filename)
104       (base, extension) = filename.split('.')
105        mime_type = $MIME_MAP[extension]
106        data[mime_type] = File.read(directory + "/" + filename)
107      end
108    }
109    return data
110  end
111
112  def readTagFile(filename)
113    list = Array.new
114    File.new(filename).readlines.each { |line|
115      line.chop!
116
117      # Don't include comments.
118      if (line !~ /^# /) && (line.length > 0)
119        list << line
120      end
121    }
122    return list
123  end
124
125
126  def generateHtmlValue(tag)
127    choice = rand(100)
128    tag = tag.sub('EXCLUDED_', '')
129    if tag =~ /^on/ and choice < 90
130      return generateHtmlValue('') + "()"
131    elsif tag == 'src' or tag == 'data' or tag == 'profile' and choice < 90
132      return generateGarbageUrl(tag)
133    end
134
135    case choice
136      when 0..50 then
137        return @htmlValues[rand(@htmlValues.length)]
138      when 51..75
139        return generateGarbageNumber()
140      when 76..85
141        return generateGarbageValue()
142      when 86..90
143        return generateGarbageNumber() + ',' + generateGarbageNumber()
144      when 91..98
145        return generateGarbageUrl(tag)
146    else
147      return generateOverflow()
148    end
149  end
150
151  def generateMediaUrl(tag)
152    mime_type = @media.keys[rand(@media.keys.length)]
153    return generateTestUrl(@test_num, nil, nil, mime_type)
154  end
155
156  def generateGarbageUrl(tag)
157    choice = rand(100)
158    case choice
159      when 0..30
160      return generateMediaUrl(tag)
161      when 31..50
162      return @protocolValues[rand(@protocolValues.length)] + '%' + generateGarbageValue()
163      when 51..60
164      return @protocolValues[rand(@protocolValues.length)] + '//../' + generateGarbageValue()
165      when 60..75
166      return @protocolValues[rand(@protocolValues.length)] + '//' + generateGarbageValue()
167      when 75..85
168      return generateOverflow() + ":" + generateGarbageValue()
169      when 86..97
170      return generateGarbageValue() + ":" + generateOverflow()
171    else
172      return generateOverflow()
173    end
174  end
175
176  def generateCssValue(property)
177    size_types = ['', 'em', 'px', '%', 'pt', 'pc', 'ex', 'in', 'cm', 'mm']
178
179    choice = rand(100)
180    case choice
181      when 0..50 then
182      # return the most likely scenario
183      case property.sub('EXCLUDED_', '')
184        when /-image|content/
185          return 'url(' + generateGarbageUrl(property) + ')'
186        when /-width|-radius|-spacing|margin|padding|height/
187          return generateGarbageValue() + size_types[rand(size_types.length)]
188        when /-color/
189          return generateGarbageColor()
190        when /-delay|-duration/
191          return generateGarbageValue() + 'ms'
192      else
193        return @cssValues[rand(@cssValues.length)]
194      end
195      when 51..75 then return generateGarbageNumber()
196      when 76..85 then return 'url(' + generateGarbageUrl(property) + ')'
197      when 85..98 then return generateGarbageValue()
198    else
199      return generateOverflow()
200    end
201  end
202
203  def generateGarbageColor()
204    case rand(100)
205      when 0..50 then return '#' + generateGarbageValue()
206      when 51..70 then return 'rgb(' + generateGarbageNumber() + ',' + generateGarbageNumber() + ',' + generateGarbageNumber() + ')'
207      when 71..98 then return 'rgb(' + generateGarbageNumber() + '%,' + generateGarbageNumber() + '%,' + generateGarbageNumber() + '%)'
208    else
209      return generateOverflow()
210    end
211  end
212
213  def generateGarbageNumber()
214    choice = rand(100)
215    case choice
216      when 0 then return '0'
217      when 1..40 then return '9' * rand(100)
218      when 41..60 then return '999999.' + rand(999999999999999999999).to_s
219      when 61..80 then return '-' + ('9' * rand(100))
220      when 81..90 then return '-999999.' + rand(999999999999999999999).to_s
221      when 91..98 then return generateGarbageText()
222    else
223      return generateOverflow()
224    end
225  end
226
227  def generateGarbageValue()
228    case rand(100)
229      when 0..30 then return rand(255).chr * rand(@config['buffer_overflow_length'])
230      when 31..50 then return "%n" * 50
231      when 51..65 then return ("&#" + rand(999999).to_s + ";") * rand(@config['max_garbage_text_size'])
232      when 66..70 then
233      junk = []
234      0.upto(rand(20)+1) do
235        junk << "\\x" + rand(65535).to_s(16)
236      end
237      return junk.join('') * rand(@config['max_garbage_text_size'])
238      when 71..99 then
239      junk = []
240      chars = '%?!$#^0123456789ABCDEF%#./\&|;'
241      0.upto(rand(20)+1) do
242        junk << chars[rand(chars.length)].chr
243      end
244      return junk.join('') * rand(@config['max_garbage_text_size'])
245    end
246  end
247
248  def generateOverflow()
249    return rand(255).chr * (@config['buffer_overflow_length'] + rand(500))
250  end
251
252  def generateGarbageText
253    case rand(100)
254      when 0..70 then return 'X' * 129
255      when 71..75 then return "%n" * 15
256      when 76..85 then return ("&#" + rand(9999999999999).to_s + ";") * rand(@config['max_garbage_text_size'])
257      when 86..90 then return generateGarbageValue()
258      when 91..98 then return rand(255).chr * rand(@config['max_garbage_text_size'])
259    else
260      return generateOverflow()
261    end
262  end
263
264  def isPropertyInBlacklist(properties)
265    # Format: [img, src] or [img, style, property]
266    blacklist_entries = []
267    if @config.has_key?('exclude') and @config['exclude']
268      blacklist_entries << properties.join('.')
269      wildcard_property = properties.dup
270      wildcard_property[0] = '*'
271      blacklist_entries << wildcard_property.join('.')
272      blacklist_entries.each do |entry|
273        if @config['exclude'].has_key?(entry) and @browser =~ /#{@config['exclude'][entry]}/
274          return true
275        end
276      end
277    end
278    return false
279  end
280
281  def generateCssStyling(tag)
282    out = ' style="'
283    0.upto(rand(@config['properties_per_style_max'])) {
284      property = @cssTags[rand(@cssTags.length)]
285      if isPropertyInBlacklist([tag, 'style', property])
286        property = "EXCLUDED_#{property}"
287      end
288      out << property
289
290      # very small chance we let the tag run on.
291      if rand(65) > 1
292        out << ": "
293      end
294
295      values = []
296      0.upto(rand(@config['attributes_per_style_property_max'])) {
297        values << generateCssValue(property)
298      }
299      out << values.join(' ')
300      # we almost always put the ; there.
301      if rand(65) > 1
302        out << ";\n    "
303      end
304    }
305    out << "\""
306    return out
307  end
308
309  def mangleTag(tag, no_close_chance=false)
310    if not no_close_chance and rand(100) < 15
311      return "</" + tag + ">"
312    end
313    out = "<" + tag
314    if rand(100) > 1
315      out << ' '
316    else
317      out << generateOverflow()
318    end
319
320    attrNum = rand(@config['attributes_per_html_tag_max']) + 1
321    attrs = []
322    # The HTML head tag does not have many useful attributes, but is always included in tests.
323    if tag == 'head' and rand(100) < 75
324      case rand(3)
325        when 0 then attrs << 'lang'
326        when 1 then attrs << 'dir'
327        when 2 then attrs << 'profile'
328      end
329    end
330    # 75% of the time, these tags get a src attribute
331    if $SRC_TAGS.include?(tag) and rand(100) < 75
332      if @config.has_key?('exclude') and @config['exclude'] and @config['exclude'].has_key?("#{tag}.src")
333        attrs << 'EXCLUDED_src'
334      else
335        attrs << 'src'
336      end
337    end
338
339    while attrs.length < attrNum
340      attribute = @htmlAttr[rand(@htmlAttr.length)]
341      if isPropertyInBlacklist([tag, attribute])
342        attribute = "EXCLUDED_#{attribute}"
343      end
344      attrs << attribute
345    end
346
347    # Add a few HTML attributes
348    for attr in attrs
349      out << attr
350      if rand(100) > 1
351        out << '='
352      end
353      if (rand(100) >= 50)
354        quoted = 1
355        out << "\""
356      else
357        quoted = nil
358      end
359      out << generateHtmlValue(attr)
360      if quoted
361        if rand(100) >= 10
362          out << "\""
363        end
364      end
365      if rand(100) >= 1
366        out << "\n  "
367      end
368    end
369
370    if rand(100) >= 25
371      out << generateCssStyling(tag)
372    end
373    out << ">\n"
374    return out
375  end
376
377  def nextTestNum()
378    if @subtest_data
379      return @test_num
380    elsif @random_mode
381      return rand(99999999999)
382    else
383      return @test_num  + 1
384    end
385  end
386
387  def generateCssPattern()
388    # Generate a CSS selector pattern.
389    choice = rand(100)
390    pattern = ''
391    case choice
392      when 0..84 then pattern = @htmlTags[rand(@htmlTags.length)].dup
393      when 85..89 then pattern = "*"
394      when 90..94 then pattern = @cssAtRules[rand(@cssAtRules.length)].dup
395      when 95..100 then pattern = ''
396    end
397
398    if rand(100) < 25
399      pattern << " " + @htmlTags[rand(@htmlTags.length)]
400    end
401
402    if rand(100) < 25
403      pattern << " > " + @htmlTags[rand(@htmlTags.length)]
404    end
405
406    if rand(100) < 25
407      pattern << " + " + @htmlTags[rand(@htmlTags.length)]
408    end
409
410    if rand(100) < 10
411      pattern << "*"
412    end
413
414
415    if rand(100) < 25
416      pseudo = @cssPseudoTags[rand(@cssPseudoTags.length)].dup
417      # These tags typically have a parenthesis
418      if (pseudo =~ /^lang|^nth|^not/ and rand(100) < 75 and pseudo !~ /\(/) or rand(100) < 20
419        pseudo << '('
420      end
421
422      if pseudo =~ /\(/
423        if rand(100) < 75
424          pseudo << generateGarbageValue()
425        end
426        if rand(100) < 75
427          pseudo << ')'
428        end
429      end
430      pattern << ":" + pseudo
431    end
432
433    if rand(100) < 20
434      html_attr = @htmlAttr[rand(@htmlAttr.length)]
435      match = '[' + html_attr
436      choice = rand(100)
437      garbage = generateGarbageValue()
438      case choice
439        when 0..25 then match << ']'
440        when 26..50 then match << "=\"#{garbage}\"]"
441        when 51..75 then match << "=~\"#{garbage}\"]"
442        when 76..99 then match << "|=\"#{garbage}\"]"
443      end
444      pattern << match
445    end
446
447    if rand(100) < 20
448      if rand(100) < 50
449        pattern << '.' + generateGarbageValue()
450      else
451        pattern << '.*'
452      end
453    end
454
455    if rand(100) < 20
456      pattern << '#' + generateGarbageValue()
457    end
458
459    if rand(100) < 5
460      pattern << ' #' + generateGarbageValue()
461    end
462
463    return pattern
464  end
465
466  def buildStyleTag()
467    out = "\n"
468    0.upto(rand(@config['properties_per_style_max'])) {
469      out << generateCssPattern()
470      if rand(100) < 90
471        out << " {\n"
472      end
473
474      0.upto(rand(@config['properties_per_style_max'])) {
475        property = @cssTags[rand(@cssTags.length)].dup
476        if isPropertyInBlacklist(['style', 'style', property])
477          property = "  EXCLUDED_#{property}"
478        end
479        out << "  #{property}: "
480
481        values = []
482        0.upto(rand(@config['attributes_per_style_property_max'])) {
483          values << generateCssValue(property)
484        }
485        out << values.join(' ')
486        if rand(100) < 95
487          out << ";\n"
488        end
489      }
490      if rand(100) < 90
491        out << "\n}\n"
492      end
493
494    }
495    return out
496  end
497
498
499  # Build any malicious javascript here. Fairly naive at the moment.
500  def buildJavaScript
501    target = @htmlTags[rand(@htmlTags.length)]
502    css_property = @cssTags[rand(@cssTags.length)]
503    css_property2 = @cssTags[rand(@cssTags.length)]
504    html_attr = @htmlAttr[rand(@htmlAttr.length)]
505    css_value = generateCssValue(css_property)
506    html_value = generateHtmlValue(html_attr)
507    html_value2 = generateGarbageNumber()
508    mangled = mangleTag(@htmlTags[rand(@htmlTags.length)]);
509    mangled2 = mangleTag(@htmlTags[rand(@htmlTags.length)]);
510
511    js = []
512    js << "window.onload=function(){"
513    js << "  var ietarget = document.createElement('#{target}');"
514    js << "  ietarget.style.#{css_property} = '#{css_value}';"
515    js << "  ietarget.#{html_attr} = '#{html_value}';"
516    js << "  document.body.appendChild(ietarget);"
517    js << "  ietarget.style.#{css_property2} = #{html_value2};"
518
519    js << "  document.write('#{mangled}');"
520    js << "  document.write('#{mangled2}');"
521    js << "}"
522    return js.join("\n")
523  end
524
525  def buildMediaFile(mime_type)
526    if @media.has_key?(mime_type)
527      data = @media[mime_type].dup
528    else
529      puts "No media found for #{mime_type}"
530      data = generateGarbageText()
531    end
532
533    # corrupt it in a subtle way
534    choice = rand(100)
535    if choice > 50
536      garbage = generateGarbageValue()
537    else
538      garbage = rand(255).chr * rand(8)
539    end
540
541    if "1.9".respond_to?(:encoding)
542      garbage.force_encoding('ASCII-8BIT')
543      data.force_encoding('ASCII-8BIT')
544    end
545
546    garbage_start = rand(data.length)
547    garbage_end = garbage_start + garbage.length
548    data[garbage_start..garbage_end] = garbage
549    if rand(100) < 15
550      data << generateGarbageValue()
551    end
552    return data
553  end
554
555  # Parse the subtest data passed in as part of the URL
556  def parseSubTestData(subtest_data)
557    # Initialize with one line at 0
558    if not subtest_data or subtest_data.to_i == 0
559      return [@config['initial_subtest_width'], [0]]
560    end
561     (lines_at_time, offsets_string) = subtest_data.split('_')
562    offsets = offsets_string.split(',').map! {|x| x.to_i }
563    return [lines_at_time.to_i, offsets]
564  end
565
566  def generateTestUrl(test_num, subtest_width=nil, subtest_offsets=nil, mime_type=nil)
567    url = @cgi_url + '?'
568    if subtest_width
569      if subtest_offsets.length > @config['subtest_combinations_max']
570        url << "t=" << test_num.to_s << "&l=test_redirect&z=THE_END"
571      else
572        url << "t=" << test_num.to_s << "&s=" << subtest_width.to_s << "_" << subtest_offsets.join(',')
573      end
574    else
575      url << "t=" << test_num.to_s
576    end
577
578    if @random_mode
579      url << "&r=1"
580    elsif @stop_num
581      url << "&x=" << @stop_num.to_s
582    end
583
584    if mime_type
585      url << '&m=' + CGI::escape(mime_type)
586    end
587
588    url << "&b=" << CGI::escape(@browser)
589    return url
590  end
591
592  def buildBodyTags(tag_count)
593    tagList = ['body']
594    # subtract the <body> tag from tag_count.
595    1.upto(tag_count-1) { tagList << @htmlTags[rand(@htmlTags.length)] }
596
597    # Lean ourselves toward lots of img and src tests
598    for tag, percent in @config['favor_html_tags']
599      if rand(100) < percent.to_f
600        # Don't overwrite the body tag.
601        tagList[rand(tagList.length-1)+1] = tag
602      end
603    end
604
605    # Now we have our hitlist of tags,lets mangle them.
606    mangled_tags = []
607    tagList.each do |tag|
608      tag_data = mangleTag(tag)
609      if tag == 'script'
610        if rand(100) < 40
611          tag_data = "<script>"
612        end
613        tag_data << buildJavaScript() + "\n" + "</script>\n"
614      elsif tag == 'style'
615        if rand(100) < 40
616          tag_data = "<style>"
617        end
618        tag_data << buildStyleTag() + "\n" + "</style>\n"
619      elsif rand(100) <= 90
620        tag_data << generateGarbageText() << "\n"
621      else
622        tag_data << "\n"
623      end
624
625      if rand(100) <= 33
626        tag_data << "</#{tag}>\n"
627      end
628      mangled_tags << "\n<!-- START #{tag} -->\n" + tag_data + "\n<!-- END #{tag} -->\n"
629    end
630    return mangled_tags
631  end
632
633  def buildHeaderTags(tag_count)
634    valid_head_tags = ['title', 'base', 'link', 'meta']
635    header_tags = ['html', 'head']
636    1.upto(tag_count-1) { header_tags << valid_head_tags[rand(valid_head_tags.length)] }
637    header_tags << @htmlTags[rand(@htmlTags.length)]
638    mangled_tags = []
639    header_tags.each do |tag|
640      mangled_tags << mangleTag(tag, no_close_chance=true)
641    end
642    return mangled_tags
643  end
644
645  def buildSurvivedPage(page_type)
646    page = "<html><head>"
647    page << "<body>Bummer. You survived both redirects. Let me go sulk in the corner.</body>"
648    page << "</html>"
649    return page
650  end
651
652  def buildRedirect(test_num, subtest_data, lookup_mode, stop_num=nil)
653    # no more redirects.
654    if lookup_mode == '1' or stop_num == test_num
655      return ''
656    end
657
658    if subtest_data
659      width, offsets = parseSubTestData(@subtest_data)
660    else
661      width, offsets = nil
662    end
663
664    # We still need a redirect, but don't bother generating new data.
665    if lookup_mode
666      redirect_url = generateTestUrl(test_num, width, offsets)
667      if lookup_mode == 'test_redirect'
668        redirect_url << "&l=test_another_redirect"
669      elsif lookup_mode == 'test_another_redirect'
670        redirect_url << "&l=survived_redirect"
671      else
672        redirect_url << "&l=#{lookup_mode}"
673      end
674    else
675      # This is a normal redirect going on to the next page. If we have subtest, get the next one.
676      if subtest_data
677        width, offsets = combine_combo_creator(@config['html_tags_per_page'], width, offsets)[0..1]
678      end
679      redirect_url = generateTestUrl(nextTestNum(), width, offsets)
680    end
681
682    redirect_code = "\t<META HTTP-EQUIV=\"Refresh\" content=\"0;URL=#{redirect_url}\">\n"
683    # use both techniques, because you never know how you might be corrupting yourself.
684    redirect_code << "\t<script language=\"javascript\">setTimeout('window.location=\"#{redirect_url}\"', 1000);</script>\n"
685    return redirect_code
686  end
687
688  def buildPage()
689    if @lookup_mode == 'survived_redirect'
690      return self.buildSurvivedPage(@lookup_mode)
691    end
692    tag_count = @config['html_tags_per_page']
693
694    if $TEST_CACHE.has_key?(@test_num)
695     (header_tags, body_tags) = $TEST_CACHE[@test_num]
696    else
697      header_tags = buildHeaderTags(3)
698      body_tags = buildBodyTags(tag_count - header_tags.length)
699    end
700    required_tags = {
701      0 => 'html',
702      1 => 'head',
703      header_tags.length => 'body'
704    }
705
706    if @subtest_data and @subtest_data.length > 0
707      if not $TEST_CACHE.has_key?(@test_num)
708        $TEST_CACHE[@test_num] = [header_tags, body_tags]
709      end
710      (width, offsets) = parseSubTestData(@subtest_data)
711      lines = combine_combo_creator(tag_count, width, offsets)[2]
712      all_tags = header_tags + body_tags
713      body_start = header_tags.length
714      header_tags = []
715      body_tags = []
716      # <html> and <body> are required, regardless of their existence in the subtest data.
717      0.upto(tag_count) do |line_number|
718        tag_data = nil
719        if lines.include?(line_number)
720          tag_data = all_tags[line_number]
721        elsif required_tags.key?(line_number)
722          tag_data = "<" + required_tags[line_number] + ">"
723        end
724        if tag_data
725          if line_number < body_start
726            header_tags << tag_data
727          else
728            body_tags << tag_data
729          end
730        end
731      end
732      header_tags << "<!-- subtest mode: #{offsets.length} combinations, width: #{width} -->"
733    end
734
735    htmlText = header_tags[0..1].join("\n\t")
736    htmlText << buildRedirect(@test_num, @subtest_data, @lookup_mode, @stop_num)
737    htmlText << "<title>[#{@test_num}:#{@subtest_data}] iExploder #{$VERSION} - #{generateGarbageText()}</title>\n"
738    if @claimed_browser and @claimed_browser.length > 1
739      show_browser = @claimed_browser
740    else
741      show_browser = @browser
742    end
743    htmlText << "\n<!-- iExploder #{$VERSION} | test #{@test_num}:#{@subtest_data} at #{Time.now} -->\n"
744    htmlText << "<!-- browser: #{show_browser} -->\n"
745    htmlText << header_tags[2..-1].join("\n\t")
746    htmlText << "\n</head>\n\n"
747    htmlText << body_tags.join("\n")
748    htmlText << "</body>\n</html>"
749    return htmlText
750  end
751
752  def buildHeaders(mime_type)
753    use_headers = []
754    banned_headers = []
755    response = {'Content-Type' => mime_type}
756    0.upto(rand(@config['headers_per_page_max'])) do
757      try_header = @headerValues[rand(@headerValues.length)]
758      if ! banned_headers.include?(try_header.downcase)
759        use_headers << try_header
760      end
761    end
762    for header in use_headers.uniq
763      if rand(100) > 75
764        response[header] = generateGarbageNumber()
765      else
766        response[header] = generateGarbageUrl(header)
767      end
768    end
769    return response
770  end
771end
772
773
774# for testing
775if $0 == __FILE__
776  ie = IExploder.new('config.yaml')
777  ie.test_num = ARGV[0].to_i || 1
778  ie.subtest_data = ARGV[1] || nil
779  mime_type = ARGV[2] || nil
780  ie.setRandomSeed()
781  if not mime_type
782    html_output = ie.buildPage()
783    puts html_output
784  else
785    headers = ie.buildHeaders(mime_type)
786    for (key, value) in headers
787      puts "#{key}: #{value}"
788    end
789    puts "Mime-Type: #{mime_type}"
790    puts ie.buildMediaFile(mime_type)
791  end
792end
793