Home | History | Annotate | Download | only in src
      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 
     19 require 'cgi'
     20 require 'yaml'
     21 
     22 require './scanner.rb'
     23 require './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 
     48 class 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
    771 end
    772 
    773 
    774 # for testing
    775 if $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
    792 end
    793