File: resolver.rb

package info (click to toggle)
ruby-mongo 2.21.3-1
  • links: PTS, VCS
  • area: main
  • in suites: sid
  • size: 14,764 kB
  • sloc: ruby: 108,806; makefile: 5; sh: 2
file content (158 lines) | stat: -rw-r--r-- 6,092 bytes parent folder | download
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