diff --git a/Gemfile.lock b/Gemfile.lock index f0a3b9493daf..a0e2d98ffe1e 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -205,7 +205,8 @@ GEM irb (>= 1.5.0) reline (>= 0.3.1) diff-lcs (1.5.1) - dnsruby (1.72.2) + dnsruby (1.72.3) + base64 (~> 0.2.0) simpleidn (~> 0.2.1) docile (1.4.1) domain_name (0.6.20240107) @@ -446,7 +447,8 @@ GEM metasm rex-core rex-text - rex-socket (0.1.57) + rex-socket (0.1.59) + dnsruby rex-core rex-sslscan (0.1.10) rex-core diff --git a/lib/msf/core/opt.rb b/lib/msf/core/opt.rb index 964e6dbcc5f5..81e479e585eb 100644 --- a/lib/msf/core/opt.rb +++ b/lib/msf/core/opt.rb @@ -35,8 +35,8 @@ def self.LPORT(default=nil, required=true, desc="The listen port") end # @return [OptString] - def self.Proxies(default=nil, required=false, desc="A proxy chain of format type:host:port[,type:host:port][...]") - Msf::OptString.new(__method__.to_s, [ required, desc, default ]) + def self.Proxies(default=nil, required=false, desc="A proxy chain of format type:host:port[,type:host:port][...]. Supported proxies: #{Rex::Socket::Proxies.supported_types.join(', ')}") + Msf::OptProxies.new(__method__.to_s, [ required, desc, default ]) end # @return [OptRhosts] diff --git a/lib/msf/core/opt_address.rb b/lib/msf/core/opt_address.rb index 1558c8119925..fd5a6d37d369 100644 --- a/lib/msf/core/opt_address.rb +++ b/lib/msf/core/opt_address.rb @@ -12,7 +12,7 @@ def type return 'address' end - def valid?(value, check_empty: true) + def valid?(value, check_empty: true, datastore: nil) return false if check_empty && empty_required_value?(value) return false unless value.kind_of?(String) or value.kind_of?(NilClass) diff --git a/lib/msf/core/opt_address_local.rb b/lib/msf/core/opt_address_local.rb index d1b809f110fa..cffeb23cdcb7 100644 --- a/lib/msf/core/opt_address_local.rb +++ b/lib/msf/core/opt_address_local.rb @@ -39,7 +39,7 @@ def normalize(value) sorted_addrs.any? ? sorted_addrs.first : '' end - def valid?(value, check_empty: true) + def valid?(value, check_empty: true, datastore: nil) return false if check_empty && empty_required_value?(value) return false unless value.kind_of?(String) || value.kind_of?(NilClass) diff --git a/lib/msf/core/opt_address_range.rb b/lib/msf/core/opt_address_range.rb index f2cfecf3e581..d8537744c84b 100644 --- a/lib/msf/core/opt_address_range.rb +++ b/lib/msf/core/opt_address_range.rb @@ -36,7 +36,7 @@ def normalize(value) return value end - def valid?(value, check_empty: true) + def valid?(value, check_empty: true, datastore: nil) return false if check_empty && empty_required_value?(value) return false unless value.kind_of?(String) or value.kind_of?(NilClass) diff --git a/lib/msf/core/opt_address_routable.rb b/lib/msf/core/opt_address_routable.rb index 837955403594..486e8e27e0e7 100644 --- a/lib/msf/core/opt_address_routable.rb +++ b/lib/msf/core/opt_address_routable.rb @@ -9,7 +9,7 @@ module Msf ### class OptAddressRoutable < OptAddress - def valid?(value, check_empty: true) + def valid?(value, check_empty: true, datastore: nil) return false if Rex::Socket.is_ip_addr?(value) && Rex::Socket.addr_atoi(value) == 0 super end diff --git a/lib/msf/core/opt_base.rb b/lib/msf/core/opt_base.rb index dccda5031d8b..95b6d3465ca7 100644 --- a/lib/msf/core/opt_base.rb +++ b/lib/msf/core/opt_base.rb @@ -116,7 +116,7 @@ def validate_on_assignment? # # If it's required and the value is nil or empty, then it's not valid. # - def valid?(value, check_empty: true) + def valid?(value, check_empty: true, datastore: nil) if check_empty && required? # required variable not set return false if (value.nil? || value.to_s.empty?) diff --git a/lib/msf/core/opt_bool.rb b/lib/msf/core/opt_bool.rb index bbe8241bfdda..48c04f2acafe 100644 --- a/lib/msf/core/opt_bool.rb +++ b/lib/msf/core/opt_bool.rb @@ -16,7 +16,7 @@ def type return 'bool' end - def valid?(value, check_empty: true) + def valid?(value, check_empty: true, datastore: nil) return false if check_empty && empty_required_value?(value) return true if value.nil? && !required? diff --git a/lib/msf/core/opt_enum.rb b/lib/msf/core/opt_enum.rb index 344b45b3a5f6..e25603febd17 100644 --- a/lib/msf/core/opt_enum.rb +++ b/lib/msf/core/opt_enum.rb @@ -17,7 +17,7 @@ def initialize(in_name, attrs = [], super end - def valid?(value = self.value, check_empty: true) + def valid?(value = self.value, check_empty: true, datastore: nil) return false if check_empty && empty_required_value?(value) return true if value.nil? && !required? diff --git a/lib/msf/core/opt_float.rb b/lib/msf/core/opt_float.rb index 80f0935c7e5b..b14aa2c2957f 100644 --- a/lib/msf/core/opt_float.rb +++ b/lib/msf/core/opt_float.rb @@ -15,7 +15,7 @@ def normalize(value) Float(value) if value.present? && valid?(value) end - def valid?(value, check_empty: true) + def valid?(value, check_empty: true, datastore: nil) return false if check_empty && empty_required_value?(value) Float(value) rescue return false if value.present? super diff --git a/lib/msf/core/opt_int.rb b/lib/msf/core/opt_int.rb index 74d4c064a6f7..2aab61824dc0 100644 --- a/lib/msf/core/opt_int.rb +++ b/lib/msf/core/opt_int.rb @@ -19,7 +19,7 @@ def normalize(value) end end - def valid?(value, check_empty: true) + def valid?(value, check_empty: true, datastore: nil) return false if check_empty && empty_required_value?(value) return false if value.present? && !value.to_s.match(/^0x[0-9a-fA-F]+$|^-?\d+$/) super diff --git a/lib/msf/core/opt_meterpreter_debug_logging.rb b/lib/msf/core/opt_meterpreter_debug_logging.rb index 7bdc3785e7b2..d40074ed5ca2 100644 --- a/lib/msf/core/opt_meterpreter_debug_logging.rb +++ b/lib/msf/core/opt_meterpreter_debug_logging.rb @@ -19,7 +19,7 @@ def validate_on_assignment? true end - def valid?(value, check_empty: true) + def valid?(value, check_empty: true, datastore: nil) return false if !super(value, check_empty: check_empty) begin diff --git a/lib/msf/core/opt_path.rb b/lib/msf/core/opt_path.rb index 0ba41dff21e5..57f696349559 100644 --- a/lib/msf/core/opt_path.rb +++ b/lib/msf/core/opt_path.rb @@ -21,7 +21,7 @@ def validate_on_assignment? end # Generally, 'value' should be a file that exists. - def valid?(value, check_empty: true) + def valid?(value, check_empty: true, datastore: nil) return false if check_empty && empty_required_value?(value) if value and !value.empty? if value =~ /^memory:\s*([0-9]+)/i diff --git a/lib/msf/core/opt_port.rb b/lib/msf/core/opt_port.rb index 115c720d13e1..cbbf92835406 100644 --- a/lib/msf/core/opt_port.rb +++ b/lib/msf/core/opt_port.rb @@ -12,7 +12,7 @@ def type return 'port' end - def valid?(value, check_empty: true) + def valid?(value, check_empty: true, datastore: nil) port = normalize(value).to_i super && port <= 65535 && port >= 0 end diff --git a/lib/msf/core/opt_proxies.rb b/lib/msf/core/opt_proxies.rb new file mode 100644 index 000000000000..e50f6fdff257 --- /dev/null +++ b/lib/msf/core/opt_proxies.rb @@ -0,0 +1,35 @@ +# -*- coding: binary -*- + +module Msf + +### +# +# Proxies option +# +### +class OptProxies < OptBase + + def type + 'proxies' + end + + def validate_on_assignment? + true + end + + def normalize(value) + value + end + + def valid?(value, check_empty: true, datastore: nil) + return false if check_empty && empty_required_value?(value) + + parsed = Rex::Socket::Proxies.parse(value) + allowed_types = Rex::Socket::Proxies.supported_types + parsed.all? do |type, host, port| + allowed_types.include?(type) && host.present? && port.present? + end + end +end + +end diff --git a/lib/msf/core/opt_regexp.rb b/lib/msf/core/opt_regexp.rb index 36900de610a5..c0aa602c587e 100644 --- a/lib/msf/core/opt_regexp.rb +++ b/lib/msf/core/opt_regexp.rb @@ -12,7 +12,7 @@ def type return 'regexp' end - def valid?(value, check_empty: true) + def valid?(value, check_empty: true, datastore: nil) if check_empty && empty_required_value?(value) return false elsif value.nil? diff --git a/lib/msf/core/opt_rhosts.rb b/lib/msf/core/opt_rhosts.rb index 6b9fef94e0a2..9dd60344b0c0 100644 --- a/lib/msf/core/opt_rhosts.rb +++ b/lib/msf/core/opt_rhosts.rb @@ -19,12 +19,13 @@ def normalize(value) value end - def valid?(value, check_empty: true) + def valid?(value, check_empty: true, datastore: nil) return false if check_empty && empty_required_value?(value) return false unless value.is_a?(String) || value.is_a?(NilClass) - if !value.nil? && value.empty? == false - return Msf::RhostsWalker.new(value).valid? + if !value.nil? && !value.empty? + rhost_walker = datastore ? Msf::RhostsWalker.new(value, datastore) : Msf::RhostsWalker.new(value) + return rhost_walker.valid? end super diff --git a/lib/msf/core/opt_string.rb b/lib/msf/core/opt_string.rb index c3ecb0202918..2d7b3429ee93 100644 --- a/lib/msf/core/opt_string.rb +++ b/lib/msf/core/opt_string.rb @@ -34,7 +34,7 @@ def normalize(value) value end - def valid?(value=self.value, check_empty: true) + def valid?(value=self.value, check_empty: true, datastore: nil) value = normalize(value) return false if check_empty && empty_required_value?(value) return false if invalid_value_length?(value) diff --git a/lib/msf/core/option_container.rb b/lib/msf/core/option_container.rb index 6ba0a70f2fb0..d1798b0648da 100644 --- a/lib/msf/core/option_container.rb +++ b/lib/msf/core/option_container.rb @@ -197,7 +197,7 @@ def add_evasion_options(opts, owner = nil) def validate(datastore) # First mutate the datastore and normalize all valid values before validating permutations of RHOST/etc. each_pair do |name, option| - if option.valid?(datastore[name]) && (val = option.normalize(datastore[name])) != nil + if option.valid?(datastore[name], datastore: datastore) && (val = option.normalize(datastore[name])) != nil # This *will* result in a module that previously used the # global datastore to have its local datastore set, which # means that changing the global datastore and re-running @@ -232,7 +232,7 @@ def validate(datastore) rhosts_walker.each do |datastore| each_pair do |name, option| - unless option.valid?(datastore[name]) + unless option.valid?(datastore[name], datastore: datastore) error_options << name if rhosts_count > 1 error_reasons[name] << "for rhosts value #{datastore['UNPARSED_RHOSTS']}" @@ -248,7 +248,7 @@ def validate(datastore) else error_options = [] each_pair do |name, option| - unless option.valid?(datastore[name]) + unless option.valid?(datastore[name], datastore: datastore) error_options << name end end diff --git a/lib/msf/core/rhosts_walker.rb b/lib/msf/core/rhosts_walker.rb index b414abb214ff..eaf6aa381ed3 100644 --- a/lib/msf/core/rhosts_walker.rb +++ b/lib/msf/core/rhosts_walker.rb @@ -50,6 +50,8 @@ class RhostResolveError < StandardError MESSAGE = 'Host resolution failed' end + # @param [String] value + # @param [Msf::DataStore] datastore def initialize(value = '', datastore = Msf::ModuleDataStore.new(nil)) @value = value @datastore = datastore @@ -141,30 +143,42 @@ def parse(value, datastore) schema = Regexp.last_match(:schema) raise InvalidSchemaError unless SUPPORTED_SCHEMAS.include?(schema) - found = false parse_method = "parse_#{schema}_uri" parsed_options = send(parse_method, value, datastore) - Rex::Socket::RangeWalker.new(parsed_options['RHOSTS']).each_ip do |ip| - results << datastore.merge( - parsed_options.merge('RHOSTS' => ip, 'UNPARSED_RHOSTS' => value) - ) - found = true - end - unless found - raise RhostResolveError.new(value) + if perform_dns_resolution?(datastore) + found = false + Rex::Socket::RangeWalker.new(parsed_options['RHOSTS']).each_ip do |ip| + results << datastore.merge( + parsed_options.merge('RHOSTS' => ip, 'UNPARSED_RHOSTS' => value) + ) + found = true + end + unless found + raise RhostResolveError.new(value) + end + else + results << datastore.merge(parsed_options.merge('UNPARSED_RHOSTS' => value)) end else - found = false - Rex::Socket::RangeWalker.new(value).each_host do |rhost| + if perform_dns_resolution?(datastore) + found = false + Rex::Socket::RangeWalker.new(value).each_host do |rhost| + overrides = {} + overrides['UNPARSED_RHOSTS'] = value + overrides['RHOSTS'] = rhost[:address] + set_hostname(datastore, overrides, rhost[:hostname]) + results << datastore.merge(overrides) + found = true + end + unless found + raise RhostResolveError.new(value) + end + else overrides = {} overrides['UNPARSED_RHOSTS'] = value - overrides['RHOSTS'] = rhost[:address] - set_hostname(datastore, overrides, rhost[:hostname]) + overrides['RHOSTS'] = value + set_hostname(datastore, overrides, value) results << datastore.merge(overrides) - found = true - end - unless found - raise RhostResolveError.new(value) end end rescue ::Interrupt @@ -344,6 +358,14 @@ def parse_tcp_uri(value, datastore) protected + # @param [Msf::DataStore] datastore + # @return [Boolean] True if DNS resolution should be performed the RHOST values, false otherwise + def perform_dns_resolution?(datastore) + # If a socks proxy has been configured, don't perform DNS resolution - so that it instead happens via the proxy + # rex-socket does not currently support socks4a. SAPNI may need to be added to this list. + !(datastore['PROXIES'].to_s.include?(Rex::Socket::Proxies::ProxyType::HTTP) || datastore['PROXIES'].to_s.include?(Rex::Socket::Proxies::ProxyType::SOCKS5)) + end + def set_hostname(datastore, result, hostname) hostname = Rex::Socket.is_ip_addr?(hostname) ? nil : hostname result['RHOSTNAME'] = hostname if datastore['RHOSTNAME'].blank? diff --git a/spec/lib/msf/core/opt_proxies_spec.rb b/spec/lib/msf/core/opt_proxies_spec.rb new file mode 100644 index 000000000000..1d7f07fc23e1 --- /dev/null +++ b/spec/lib/msf/core/opt_proxies_spec.rb @@ -0,0 +1,27 @@ +# -*- coding:binary -*- + +require 'spec_helper' + +RSpec.describe Msf::OptProxies do + + valid_values = [ + nil, + '', + ' ', + 'socks5:198.51.100.1:1080', + 'socks4:198.51.100.1:1080', + 'http:198.51.100.1:8080,socks4:198.51.100.1:1080', + 'http:198.51.100.1:8080, socks4:198.51.100.1:1080', + 'sapni:198.51.100.1:8080, socks4:198.51.100.1:1080', + ].map { |value| { value: value, normalized: value } } + + invalid_values = [ + { :value => 123 }, + { :value => 'foo(' }, + { :value => 'foo:198.51.100.1:8080' }, + { :value => 'foo:198.51.100.18080' }, + { :value => 'foo::' }, + ] + + it_behaves_like "an option", valid_values, invalid_values, 'proxies' +end diff --git a/spec/lib/msf/core/opt_spec.rb b/spec/lib/msf/core/opt_spec.rb index 20c52e59a889..d438ddf0877a 100644 --- a/spec/lib/msf/core/opt_spec.rb +++ b/spec/lib/msf/core/opt_spec.rb @@ -33,7 +33,7 @@ context 'Proxies' do subject { described_class::Proxies } - it { is_expected.to be_a(Msf::OptString) } + it { is_expected.to be_a(Msf::OptProxies) } end context 'RHOST' do @@ -89,7 +89,7 @@ context 'Proxies()' do subject { described_class::Proxies(default) } - it { is_expected.to be_a(Msf::OptString) } + it { is_expected.to be_a(Msf::OptProxies) } specify 'sets default' do expect(subject.default).to eq(default) end diff --git a/spec/lib/msf/core/rhosts_walker_spec.rb b/spec/lib/msf/core/rhosts_walker_spec.rb index 213340748449..2f0bc79ffc09 100644 --- a/spec/lib/msf/core/rhosts_walker_spec.rb +++ b/spec/lib/msf/core/rhosts_walker_spec.rb @@ -325,7 +325,7 @@ def each_error_for(mod) before(:each) do @temp_files = [] - allow(::Addrinfo).to receive(:getaddrinfo).with('nonexistent.com', 0, ::Socket::AF_UNSPEC, ::Socket::SOCK_STREAM) do |*_args| + allow(::Addrinfo).to receive(:getaddrinfo).with('nonexistent.example.com', 0, ::Socket::AF_UNSPEC, ::Socket::SOCK_STREAM) do |*_args| [] end allow(::Addrinfo).to receive(:getaddrinfo).with('example.com', 0, ::Socket::AF_UNSPEC, ::Socket::SOCK_STREAM) do |*_args| @@ -399,6 +399,21 @@ def create_tempfile(content) { 'RHOSTS' => 'https://example.com:9000/foo', 'expected' => 1 }, { 'RHOSTS' => 'cidr:/30:https://user:pass@multiple_ips.example.com:9000/foo', 'expected' => 8 }, + # Perform DNS resolution by default + { 'RHOSTS' => 'https://user:pass@multiple_ips.example.com:9000/foo', 'PROXIES' => 'http:198.51.100.1:1080', 'expected' => 1 }, + + # Skip DNS resolution when socks5 proxy present + { 'RHOSTS' => 'https://user:pass@multiple_ips.example.com:9000/foo', 'PROXIES' => 'socks5:198.51.100.1:1080', 'expected' => 1 }, + + # Skip DNS resolution when http proxy present + { 'RHOSTS' => 'https://user:pass@multiple_ips.example.com:9000/foo', 'PROXIES' => 'http:198.51.100.1:1080', 'expected' => 1 }, + + # Perform DNS resolution if socks4 proxy present - as rex-socket doesn't support socks4a + { 'RHOSTS' => 'https://user:pass@multiple_ips.example.com:9000/foo', 'PROXIES' => 'socks4:198.51.100.1:1080', 'expected' => 2 }, + + # Perform DNS resolution if SAPNI proxy present - this can be removed if it causes issues + { 'RHOSTS' => 'https://user:pass@multiple_ips.example.com:9000/foo', 'PROXIES' => 'socks4:198.51.100.1:1080', 'expected' => 2 }, + # Edge cases { 'expected' => 0 }, { 'RHOSTS' => nil, 'expected' => 0 }, @@ -409,8 +424,10 @@ def create_tempfile(content) { 'RHOSTS' => '127.0.0.1 http:| 127.0.0.1', 'expected' => 2 }, { 'RHOSTS' => '127.0.0.1 unknown_protocol://127.0.0.1 ftpz://127.0.0.1', 'expected' => 1 }, ].each do |test| - it "counts #{test['RHOSTS'].inspect} as being #{test['expected']}" do - expect(described_class.new(test['RHOSTS'], aux_mod.datastore).count).to eq(test['expected']) + it "counts #{test['RHOSTS'].inspect} with PROXIES #{test['PROXIES'].inspect} as being #{test['expected']}" do + datastore = aux_mod.datastore + datastore['PROXIES'] = test['PROXIES'] if test.key?('PROXIES') + expect(described_class.new(test['RHOSTS'], datastore).count).to eq(test['expected']) end end end @@ -443,7 +460,7 @@ def create_tempfile(content) { 'RHOSTS' => 'cidr:%eth2:127.0.0.1', 'expected' => [Msf::RhostsWalker::Error.new('cidr:%eth2:127.0.0.1', cause: Msf::RhostsWalker::InvalidCIDRError.new)] }, # host resolution - { 'RHOSTS' => 'https://nonexistent.com:9000/foo', 'expected' => [Msf::RhostsWalker::Error.new('https://nonexistent.com:9000/foo', cause: Msf::RhostsWalker::RhostResolveError.new)] }, + { 'RHOSTS' => 'https://nonexistent.example.com:9000/foo', 'expected' => [Msf::RhostsWalker::Error.new('https://nonexistent.example.com:9000/foo', cause: Msf::RhostsWalker::RhostResolveError.new)] }, ].each do |test| it "handles the input #{test['RHOSTS'].inspect} as having the errors #{test['expected']}" do aux_mod.datastore['RHOSTS'] = test['RHOSTS'] @@ -525,6 +542,16 @@ def create_tempfile(content) expect(each_error_for(http_mod)).to be_empty end + it 'enumerates a single host without performing DNS resolution if a socks5 proxy is registered' do + http_mod.datastore['RHOSTS'] = 'http://multiple_ips.example.com/foo' + http_mod.datastore['PROXIES'] = 'socks5:198.51.100.1:1080' + expected = [ + { 'RHOSTNAME' => 'multiple_ips.example.com', 'RHOSTS' => 'multiple_ips.example.com', 'RPORT' => 80, 'VHOST' => 'multiple_ips.example.com', 'SSL' => false, 'HttpUsername' => '', 'HttpPassword' => '', 'TARGETURI' => '/foo' }, + ] + expect(each_host_for(http_mod)).to have_datastore_values(expected) + expect(each_error_for(http_mod)).to be_empty + end + it 'enumerates http values with user/passwords' do http_mod.datastore.import_options( Msf::OptionContainer.new( @@ -641,6 +668,19 @@ def create_tempfile(content) expect(each_error_for(kerberos_mod)).to be_empty end + it 'allows the user to specify a rhostname even if socks5 proxy is registered' do + kerberos_mod.datastore['RHOSTS'] = '192.0.2.2' + kerberos_mod.datastore['RHOSTNAME'] = 'example.com' + kerberos_mod.datastore['PROXIES'] = 'socks5:198.51.100.1:1080' + + expected = [ + { "RHOSTNAME"=> 'example.com', "RHOSTS"=>"192.0.2.2" } + ] + + expect(each_host_for(kerberos_mod)).to have_datastore_values(expected) + expect(each_error_for(kerberos_mod)).to be_empty + end + it 'preserves a RHOSTNAME even if RHOSTS resolved with a hostname' do kerberos_mod.datastore['RHOSTS'] = 'multiple_ips.example.com' kerberos_mod.datastore['RHOSTNAME'] = 'example.com' @@ -654,6 +694,19 @@ def create_tempfile(content) expect(each_error_for(kerberos_mod)).to be_empty end + it 'preserves a RHOSTNAME even if RHOSTS is set and a socks5 proxy is registered' do + kerberos_mod.datastore['RHOSTS'] = 'multiple_ips.example.com' + kerberos_mod.datastore['RHOSTNAME'] = 'example.com' + kerberos_mod.datastore['PROXIES'] = 'socks5:198.51.100.1:1080' + + expected = [ + {"RHOSTNAME"=> "example.com", "RHOSTS"=>"multiple_ips.example.com"}, + ] + + expect(each_host_for(kerberos_mod)).to have_datastore_values(expected) + expect(each_error_for(kerberos_mod)).to be_empty + end + it 'enumerates a cidr scheme with a single http value' do http_mod.datastore['RHOSTS'] = 'cidr:/30:http://127.0.0.1:3000/foo/bar' expected = [ @@ -884,6 +937,18 @@ def create_tempfile(content) expect(each_error_for(postgres_mod)).to be_empty expect(each_host_for(postgres_mod)).to have_datastore_values(expected) end + + it 'enumerates postgres schemes and avoids DNS resolution if a socks5 proxy is registered' do + postgres_mod.datastore['RHOSTS'] = 'postgres://postgres:@example.com "postgres://user:a b c@example.com/" "postgres://user:a b c@example.com:9001/database_name"' + postgres_mod.datastore['PROXIES'] = 'socks5:198.51.100.1:1080' + expected = [ + { 'RHOSTNAME' => 'example.com', 'RHOSTS' => 'example.com', 'RPORT' => 5432, 'USERNAME' => 'postgres', 'PASSWORD' => '', 'DATABASE' => 'template1' }, + { 'RHOSTNAME' => 'example.com', 'RHOSTS' => 'example.com', 'RPORT' => 5432, 'USERNAME' => 'user', 'PASSWORD' => 'a b c', 'DATABASE' => 'template1' }, + { 'RHOSTNAME' => 'example.com', 'RHOSTS' => 'example.com', 'RPORT' => 9001, 'USERNAME' => 'user', 'PASSWORD' => 'a b c', 'DATABASE' => 'database_name' } + ] + expect(each_error_for(postgres_mod)).to be_empty + expect(each_host_for(postgres_mod)).to have_datastore_values(expected) + end end context 'when using the tcp scheme' do