lib/msf/core/data_store.rb
# -*- 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