1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158
|
# frozen_string_literal: true
# rubocop:todo all
# Copyright (C) 2017-2020 MongoDB Inc.
#
# Licensed under the Apache License, Version 2.0 (the 'License');
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an 'AS IS' BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
module Mongo
module Srv
# Encapsulates the necessary behavior for querying SRV records as
# required by the driver.
#
# @api private
class Resolver
include Loggable
# @return [ String ] RECORD_PREFIX The prefix prepended to each hostname
# before querying SRV records.
RECORD_PREFIX = '_mongodb._tcp.'.freeze
# Generates the record prefix with a custom SRV service name if it is
# provided.
#
# @option srv_service_name [ String | nil ] The SRV service name to use
# in the record prefix.
# @return [ String ] The generated record prefix.
def record_prefix(srv_service_name=nil)
return srv_service_name ? "_#{srv_service_name}._tcp." : RECORD_PREFIX
end
# Creates a new Resolver.
#
# @option opts [ Float ] :timeout The timeout, in seconds, to use for
# each DNS record resolution.
# @option opts [ Boolean ] :raise_on_invalid Whether or not to raise
# an exception if either a record with a mismatched domain is found
# or if no records are found. Defaults to true.
# @option opts [ Hash ] :resolv_options For internal driver use only.
# Options to pass through to Resolv::DNS constructor for SRV lookups.
def initialize(**opts)
@options = opts.freeze
@resolver = Resolv::DNS.new(@options[:resolv_options])
@resolver.timeouts = timeout
end
# @return [ Hash ] Resolver options.
attr_reader :options
def timeout
options[:timeout] || Monitor::DEFAULT_TIMEOUT
end
# Obtains all of the SRV records for a given hostname. If a srv_max_hosts
# is specified and it is greater than 0, return maximum srv_max_hosts records.
#
# In the event that a record with a mismatched domain is found or no
# records are found, if the :raise_on_invalid option is true,
# an exception will be raised, otherwise a warning will be logged.
#
# @param [ String ] hostname The hostname whose records should be obtained.
# @param [ String | nil ] srv_service_name The SRV service name for the DNS query.
# If nil, 'mongodb' is used.
# @param [ Integer | nil ] srv_max_hosts The maximum number of records to return.
# If this value is nil, return all of the records.
#
# @raise [ Mongo::Error::MismatchedDomain ] If the :raise_in_invalid
# Resolver option is true and a record with a domain name that does
# not match the hostname's is found.
# @raise [ Mongo::Error::NoSRVRecords ] If the :raise_in_invalid Resolver
# option is true and no records are found.
#
# @return [ Mongo::Srv::Result ] SRV lookup result.
def get_records(hostname, srv_service_name=nil, srv_max_hosts=nil)
query_name = record_prefix(srv_service_name) + hostname
resources = @resolver.getresources(query_name, Resolv::DNS::Resource::IN::SRV)
# Collect all of the records into a Result object, raising an error
# or logging a warning if a record with a mismatched domain is found.
# Note that in the case a warning is raised, the record is _not_
# added to the Result object.
result = Srv::Result.new(hostname)
resources.each do |record|
begin
result.add_record(record)
rescue Error::MismatchedDomain => e
if raise_on_invalid?
raise
else
log_warn(e.message)
end
end
end
# If no records are found, either raise an error or log a warning
# based on the Resolver's :raise_on_invalid option.
if result.empty?
if raise_on_invalid?
raise Error::NoSRVRecords.new(URI::SRVProtocol::NO_SRV_RECORDS % hostname)
else
log_warn(URI::SRVProtocol::NO_SRV_RECORDS % hostname)
end
end
# if srv_max_hosts is in [1, #addresses)
if (1...result.address_strs.length).include? srv_max_hosts
sampled_records = resources.shuffle.first(srv_max_hosts)
result = Srv::Result.new(hostname)
sampled_records.each { |record| result.add_record(record) }
end
result
end
# Obtains the TXT records of a host.
#
# @param [ String ] hostname The host whose TXT records should be obtained.
#
# @return [ nil | String ] URI options string from TXT record
# associated with the hostname, or nil if there is no such record.
#
# @raise [ Mongo::Error::InvalidTXTRecord ] If more than one TXT record is found.
def get_txt_options_string(hostname)
records = @resolver.getresources(hostname, Resolv::DNS::Resource::IN::TXT)
if records.empty?
return nil
end
if records.length > 1
msg = "Only one TXT record is allowed: querying hostname #{hostname} returned #{records.length} records"
raise Error::InvalidTXTRecord, msg
end
records[0].strings.join
end
private
# Checks whether an error should be raised due to either a record with
# a mismatched domain being found or no records being found.
#
# @return [ Boolean ] Whether an error should be raised.
def raise_on_invalid?
@raise_on_invalid ||= @options[:raise_on_invalid] || true
end
end
end
end
|