rapid7/metasploit-framework

View on GitHub
lib/msf/core/data_store.rb

Summary

Maintainability
C
1 day
Test Coverage
# -*- coding: binary -*-
module Msf

###
#
# The data store is just a bitbucket that holds keyed values.  It is used
# by various classes to hold option values and other state information.
#
###
class DataStore < Hash

  # Temporary forking logic for conditionally using the {Msf::ModuleDatastoreWithFallbacks} implementation.
  #
  # This method replaces the default `ModuleDataStore.new` with the ability to instantiate the `ModuleDataStoreWithFallbacks`
  # class instead, if the feature is enabled
  def self.new
    if Msf::FeatureManager.instance.enabled?(Msf::FeatureManager::DATASTORE_FALLBACKS)
      return Msf::DataStoreWithFallbacks.new
    end

    instance = allocate
    instance.send(:initialize)
    instance
  end

  #
  # Initializes the data store's internal state.
  #
  def initialize()
    @options     = Hash.new
    @aliases     = Hash.new
    @imported    = Hash.new
    @imported_by = Hash.new
  end

  attr_accessor :options
  attr_accessor :aliases
  attr_accessor :imported
  attr_accessor :imported_by

  #
  # Clears the imported flag for the supplied key since it's being set
  # directly.
  #
  def []=(k, v)
    k = find_key_case(k)
    @imported[k] = false
    @imported_by[k] = nil

    opt = @options[k]
    unless opt.nil?
      if opt.validate_on_assignment?
        unless opt.valid?(v, check_empty: false)
          raise Msf::OptionValidateError.new(["Value '#{v}' is not valid for option '#{k}'"])
        end
        v = opt.normalize(v)
      end
    end

    super(k,v)
  end

  #
  # Case-insensitive wrapper around hash lookup
  #
  def [](k)
    super(find_key_case(k))
  end

  #
  # Case-insensitive wrapper around store
  #
  def store(k,v)
    super(find_key_case(k), v)
  end

  #
  # Case-insensitive wrapper around delete
  #
  def delete(k)
    @aliases.delete_if { |_, v| v.casecmp(k) == 0 }
    super(find_key_case(k))
  end


  #
  # Updates a value in the datastore with the specified name, k, to the
  # specified value, v.  This update does not alter the imported status of
  # the value.
  #
  def update_value(k, v)
    self.store(k, v)
  end

  #
  # This method is a helper method that imports the default value for
  # all of the supplied options
  #
  def import_options(options, imported_by = nil, overwrite = false)
    options.each_option do |name, opt|
      if self[name].nil? || overwrite
        import_option(name, opt.default, true, imported_by, opt)
      end
    end
  end

  #
  # Imports option values from a whitespace separated string in
  # VAR=VAL format.
  #
  def import_options_from_s(option_str, delim = nil)
    hash = {}

    # Figure out the delimiter, default to space.
    if (delim.nil?)
      delim = /\s/

      if (option_str.split('=').length <= 2 or option_str.index(',') != nil)
        delim = ','
      end
    end

    # Split on the delimiter
    option_str.split(delim).each { |opt|
      var, val = opt.split('=', 2)

      next if (var =~ /^\s+$/)


      # Invalid parse?  Raise an exception and let those bastards know.
      if (var == nil or val == nil)
        var = "unknown" if (!var)

        raise Rex::ArgumentParseError, "Invalid option specified: #{var}",
          caller
      end

      # Remove trailing whitespaces from the value
      val.gsub!(/\s+$/, '')

      # Store the value
      hash[var] = val
    }

    import_options_from_hash(hash)
  end

  #
  # Imports options from a hash and stores them in the datastore.
  #
  def import_options_from_hash(option_hash, imported = true, imported_by = nil)
    option_hash.each_pair { |key, val|
      import_option(key, val, imported, imported_by)
    }
  end

  # TODO: Doesn't normalize data in the same vein as:
  # https://github.com/rapid7/metasploit-framework/pull/6644
  def import_option(key, val, imported = true, imported_by = nil, option = nil)
    self.store(key, val)

    if option
      option.aliases.each do |a|
        @aliases[a.downcase] = key.downcase
      end
    end
    @options[key] = option
    @imported[key] = imported
    @imported_by[key] = imported_by
  end

  #
  # Serializes the options in the datastore to a string.
  #
  def to_s(delim = ' ')
    str = ''

    keys.sort.each { |key|
      str << "#{key}=#{self[key]}" + ((str.length) ? delim : '')
    }

    return str
  end

  # Override Hash's to_h method so we can include the original case of each key
  # (failing to do this breaks a number of places in framework and pro that use
  # serialized datastores)
  def to_h
    datastore_hash = {}
    self.keys.each do |k|
      datastore_hash[k.to_s] = self[k].to_s
    end
    datastore_hash
  end

  # Hack on a hack for the external modules
  def to_external_message_h
    datastore_hash = {}

    array_nester = ->(arr) do
      if arr.first.is_a? Array
        arr.map &array_nester
      else
        arr.map { |item| item.to_s.dup.force_encoding('UTF-8') }
      end
    end

    self.keys.each do |k|
      # TODO arbitrary depth
      if self[k].is_a? Array
        datastore_hash[k.to_s.dup.force_encoding('UTF-8')] = array_nester.call(self[k])
      else
        datastore_hash[k.to_s.dup.force_encoding('UTF-8')] = self[k].to_s.dup.force_encoding('UTF-8')
      end
    end
    datastore_hash
  end

  #
  # Persists the contents of the data store to a file
  #
  def to_file(path, name = 'global')
    ini = Rex::Parser::Ini.new(path)

    ini.add_group(name)

    # Save all user-defined options to the file.
    user_defined.each_pair { |k, v|
      ini[name][k] = v
    }

    ini.to_file(path)
  end

  #
  # Imports datastore values from the specified file path using the supplied
  # name
  #
  def from_file(path, name = 'global')
    begin
      ini = Rex::Parser::Ini.from_file(path)
    rescue
      return
    end

    if (ini.group?(name))
      import_options_from_hash(ini[name], false)
    end
  end

  #
  # Return a deep copy of this datastore.
  #
  def copy
    ds = self.class.new
    self.keys.each do |k|
      ds.import_option(k, self[k].kind_of?(String) ? self[k].dup : self[k], @imported[k], @imported_by[k])
    end
    ds.aliases = self.aliases.dup
    ds
  end

  #
  # Override merge! so that we merge the aliases and imported hashes
  #
  def merge!(other)
    if other.is_a? DataStore
      self.aliases.merge!(other.aliases)
      self.imported.merge!(other.imported)
      self.imported_by.merge!(other.imported_by)
    end
    # call super last so that we return a reference to ourselves
    super
  end

  #
  # Override merge to ensure we merge the aliases and imported hashes
  #
  def merge(other)
    ds = self.copy
    ds.merge!(other)
  end

  #
  # Returns a hash of user-defined datastore values.  The returned hash does
  # not include default option values.
  #
  def user_defined
    reject { |k, v|
      @imported[k] == true
    }
  end

  #
  # Remove all imported options from the data store.
  #
  def clear_non_user_defined
    @imported.delete_if { |k, v|
      if (v and @imported_by[k] != 'self')
        self.delete(k)
        @imported_by.delete(k)
      end

      v
    }
  end

  #
  # Completely clear all values in the hash
  #
  def clear
    self.keys.each {|k| self.delete(k) }
    self
  end

  #
  # Overrides the builtin 'each' operator to avoid the following exception on Ruby 1.9.2+
  #    "can't add a new key into hash during iteration"
  #
  def each(&block)
    list = []
    self.keys.sort.each do |sidx|
      list << [sidx, self[sidx]]
    end
    list.each(&block)
  end

  #
  # Case-insensitive key lookup
  #
  def find_key_case(k)

    # Scan each alias looking for a key
    search_k = k.downcase
    if self.aliases.has_key?(search_k)
      search_k = self.aliases[search_k]
    end

    # Scan each key looking for a match
    self.each_key do |rk|
      if rk.casecmp(search_k) == 0
        return rk
      end
    end

    # Fall through to the non-existent value
    return k
  end

end

end