dns-sd 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +6 -0
- data/.rubocop.yml +104 -0
- data/.travis.yml +10 -0
- data/.yardopts +1 -0
- data/CODE_OF_CONDUCT.md +49 -0
- data/CONTRIBUTING.md +13 -0
- data/LICENCE +674 -0
- data/README.md +130 -0
- data/dns-sd.gemspec +40 -0
- data/lib/dns-sd.rb +110 -0
- data/lib/dns-sd/resource_cache.rb +47 -0
- data/lib/dns-sd/service.rb +96 -0
- data/lib/dns-sd/service_instance.rb +154 -0
- data/lib/dns-sd/target.rb +7 -0
- metadata +213 -0
data/README.md
ADDED
@@ -0,0 +1,130 @@
|
|
1
|
+
[DNS-SD](https://tools.ietf.org/html/rfc6763) is a method of laying out
|
2
|
+
standard DNS records in such a way that permits service discovery and
|
3
|
+
enumeration, high availability, load balancing, and failover of arbitrary
|
4
|
+
services. Whilst it is often used in concert with [Multicast DNS
|
5
|
+
(mDNS)](https://tools.ietf.org/html/rfc67621), it works just as well with
|
6
|
+
regular DNS services, and that is what this package is focused on. If
|
7
|
+
you're interested in mDNS-based DNS-SD interaction, the [similarly-named
|
8
|
+
dnssd gem](https://rubygems.org/gems/dnssd) might be more to your liking.
|
9
|
+
|
10
|
+
|
11
|
+
# Installation
|
12
|
+
|
13
|
+
It's a gem:
|
14
|
+
|
15
|
+
gem install dns-sd
|
16
|
+
|
17
|
+
There's also the wonders of [the Gemfile](http://bundler.io):
|
18
|
+
|
19
|
+
gem 'dns-sd'
|
20
|
+
|
21
|
+
If you're the sturdy type that likes to run from git:
|
22
|
+
|
23
|
+
rake install
|
24
|
+
|
25
|
+
Or, if you've eschewed the convenience of Rubygems entirely, then you
|
26
|
+
presumably know what to do already.
|
27
|
+
|
28
|
+
|
29
|
+
# Usage
|
30
|
+
|
31
|
+
The basic class, DNSSD, is the usual entrypoint for everything:
|
32
|
+
|
33
|
+
require 'dns-sd'
|
34
|
+
|
35
|
+
dnssd = DNSSD.new("example.com")
|
36
|
+
|
37
|
+
From there, you can connect directly to a service instance, get a service
|
38
|
+
type so you can ask it for all available instances, or even ask for all
|
39
|
+
services in the domain.
|
40
|
+
|
41
|
+
The sub-sections below show some common usage patterns. If your specific
|
42
|
+
use case isn't covered, the per-class documentation may be more
|
43
|
+
enlightening.
|
44
|
+
|
45
|
+
|
46
|
+
## Connecting to a service instance
|
47
|
+
|
48
|
+
If you know what service and instance you're interested in, you can go
|
49
|
+
straight there:
|
50
|
+
|
51
|
+
my_printer = dnssd.service_instance("My Printer", "ipp", :TCP)
|
52
|
+
|
53
|
+
Then get the connection targets and even try connecting to them:
|
54
|
+
|
55
|
+
targets = my_printer.targets
|
56
|
+
|
57
|
+
sock = begin
|
58
|
+
break nil if targets.empty?
|
59
|
+
t = targets.shift
|
60
|
+
TCPSocket.new(t.hostname, t.port)
|
61
|
+
rescue SystemCallError
|
62
|
+
# Automatically go on to the next server
|
63
|
+
retry
|
64
|
+
end
|
65
|
+
|
66
|
+
if sock.nil?
|
67
|
+
$stderr.puts "Failed to connect to My Printer"
|
68
|
+
else
|
69
|
+
# Work with `sock` as required
|
70
|
+
end
|
71
|
+
|
72
|
+
If there's more than one target registered for a given service instance
|
73
|
+
(common in high-availability and load-balanced systems), every time you call
|
74
|
+
DNSSD::ServiceInstance#targets, you'll get the server list in a different
|
75
|
+
order, respecting the priorities and weights of the constituent SRV records.
|
76
|
+
|
77
|
+
The SRV record lookups (like all DNS records) are cached, so if you wish
|
78
|
+
to connect to a service instance repeatedly, you should call #targets on
|
79
|
+
your service instance object each time you want to make a connection, rather
|
80
|
+
than re-using the result of a single call to #target. As long as you're
|
81
|
+
operating against the same DNSSD::ServiceInstance object and the record TTLs
|
82
|
+
haven't expired, successive calls to #target should be very efficient.
|
83
|
+
|
84
|
+
|
85
|
+
## Enumerating all instances of a service
|
86
|
+
|
87
|
+
If you know you want a printer, but aren't sure which one, you can ask for
|
88
|
+
the IPP service, and then enumerate all the service instances:
|
89
|
+
|
90
|
+
dnssd.service("ipp", :TCP).each do |name, instance|
|
91
|
+
puts "I found a printer named #{name}"
|
92
|
+
puts "Its targets are #{instance.targets.map { |t| "#{t.hostname}:#{t.port}" }.join(", ")}"
|
93
|
+
end
|
94
|
+
|
95
|
+
## Enumerating all services
|
96
|
+
|
97
|
+
If you're just curious about what might be on a domain, you can try the
|
98
|
+
"Service Type Enumeration" endpoint:
|
99
|
+
|
100
|
+
dnssd.services.each do |name, svc|
|
101
|
+
puts "I found a service named #{name}"
|
102
|
+
puts "It has instances of #{svc.instances.map { |i| i.name }.join(", ")}"
|
103
|
+
end
|
104
|
+
|
105
|
+
|
106
|
+
# Contributing
|
107
|
+
|
108
|
+
Bug reports should be sent to the [Github issue
|
109
|
+
tracker](https://github.com/discourse/dns-sd/issues). Patches can be sent as a
|
110
|
+
[Github pull request](https://github.com/discourse/dns-sd/pulls).
|
111
|
+
|
112
|
+
|
113
|
+
# Licence
|
114
|
+
|
115
|
+
Unless otherwise stated, everything in this repo is covered by the following
|
116
|
+
copyright notice:
|
117
|
+
|
118
|
+
Copyright (C) 2017 Civilzed Discourse Construction Kit, Inc.
|
119
|
+
|
120
|
+
This program is free software: you can redistribute it and/or modify it
|
121
|
+
under the terms of the GNU General Public License version 3, as
|
122
|
+
published by the Free Software Foundation.
|
123
|
+
|
124
|
+
This program is distributed in the hope that it will be useful,
|
125
|
+
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
126
|
+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
127
|
+
GNU General Public License for more details.
|
128
|
+
|
129
|
+
You should have received a copy of the GNU General Public License
|
130
|
+
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
data/dns-sd.gemspec
ADDED
@@ -0,0 +1,40 @@
|
|
1
|
+
begin
|
2
|
+
require 'git-version-bump'
|
3
|
+
rescue LoadError
|
4
|
+
nil
|
5
|
+
end
|
6
|
+
|
7
|
+
Gem::Specification.new do |s|
|
8
|
+
s.name = "dns-sd"
|
9
|
+
|
10
|
+
s.version = GVB.version rescue "0.0.0.1.NOGVB"
|
11
|
+
s.date = GVB.date rescue Time.now.strftime("%Y-%m-%d")
|
12
|
+
|
13
|
+
s.platform = Gem::Platform::RUBY
|
14
|
+
|
15
|
+
s.summary = "Query RFC6763 DNS-SD records"
|
16
|
+
s.description = <<~EOF
|
17
|
+
If you need to retrieve and work with DNS-SD records, this is the library
|
18
|
+
you've been waiting for.
|
19
|
+
EOF
|
20
|
+
|
21
|
+
s.authors = ["Matt Palmer"]
|
22
|
+
s.email = ["matt.palmer@discourse.org"]
|
23
|
+
s.homepage = "https://github.com/discourse/dns-sd"
|
24
|
+
|
25
|
+
s.files = `git ls-files -z`.split("\0").reject { |f| f =~ /^(G|spec|Rakefile)/ }
|
26
|
+
|
27
|
+
s.required_ruby_version = ">= 2.3.0"
|
28
|
+
|
29
|
+
s.add_development_dependency 'bundler'
|
30
|
+
s.add_development_dependency 'github-release'
|
31
|
+
s.add_development_dependency 'git-version-bump'
|
32
|
+
s.add_development_dependency 'guard-rspec'
|
33
|
+
s.add_development_dependency 'guard-rubocop'
|
34
|
+
s.add_development_dependency 'rake', "~> 12.0"
|
35
|
+
s.add_development_dependency 'redcarpet'
|
36
|
+
s.add_development_dependency 'rspec'
|
37
|
+
s.add_development_dependency 'rubocop'
|
38
|
+
s.add_development_dependency 'simplecov'
|
39
|
+
s.add_development_dependency 'yard'
|
40
|
+
end
|
data/lib/dns-sd.rb
ADDED
@@ -0,0 +1,110 @@
|
|
1
|
+
require 'dns-sd/resource_cache'
|
2
|
+
require 'dns-sd/service'
|
3
|
+
|
4
|
+
require 'resolv'
|
5
|
+
|
6
|
+
# Interact with RFC6763 DNS-SD records.
|
7
|
+
#
|
8
|
+
# Given a domain to work in, instances of this class use the standard Ruby DNS
|
9
|
+
# resolution mechanisms to look up services, service instances, and the
|
10
|
+
# individual servers that make up service instances. You can also obtain the
|
11
|
+
# "metadata" associated with service instances.
|
12
|
+
#
|
13
|
+
# If you know the service, or even service instance, you wish to connect to,
|
14
|
+
# you can go straight there, using #service or #service_instance (as
|
15
|
+
# appropriate). Otherwise, if you're just curious about what is available, you
|
16
|
+
# can use #services to get a list of everything advertised under the "Service
|
17
|
+
# Type Enumeration" record for the domain.
|
18
|
+
#
|
19
|
+
class DNSSD
|
20
|
+
include DNSSD::ResourceCache
|
21
|
+
|
22
|
+
# Create a new DNS-SD instance.
|
23
|
+
#
|
24
|
+
# @param domain [String, Resolv::DNS::Name] Specify the base domain under
|
25
|
+
# which all the records we're interested in are registered.
|
26
|
+
#
|
27
|
+
def initialize(domain)
|
28
|
+
@domain = if domain.is_a?(Resolv::DNS::Name)
|
29
|
+
domain
|
30
|
+
else
|
31
|
+
Resolv::DNS::Name.create(domain)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
# The current search domain.
|
36
|
+
#
|
37
|
+
# @return [String]
|
38
|
+
#
|
39
|
+
def domain
|
40
|
+
@domain.to_s
|
41
|
+
end
|
42
|
+
|
43
|
+
# Create a new instance of DNSSD::Service for this given name and protocol.
|
44
|
+
#
|
45
|
+
# If you know the name and protocol of the service you wish to query for,
|
46
|
+
# this is the method for you! Note that just calling this method doesn't
|
47
|
+
# make any DNS requests, so you may get a service that has no instances.
|
48
|
+
#
|
49
|
+
# @param name [String] the name of the service, *without* the leading
|
50
|
+
# underscore that goes into the DNS name.
|
51
|
+
#
|
52
|
+
# @param protocol [Symbol] One of `:TCP` or `:UDP`, to indicate that you want
|
53
|
+
# to talk to a TCP or non-TCP service, respectively. Yes, `:UDP` means
|
54
|
+
# "non-TCP"; for more laughs, read RFC6763 s. 7.
|
55
|
+
#
|
56
|
+
# @return [DNSSD::Service]
|
57
|
+
#
|
58
|
+
def service(name, protocol)
|
59
|
+
proto = case protocol
|
60
|
+
when :TCP
|
61
|
+
"_tcp"
|
62
|
+
when :UDP
|
63
|
+
"_udp"
|
64
|
+
else
|
65
|
+
raise ArgumentError,
|
66
|
+
"Invalid protocol (must be one of :TCP or :UDP)"
|
67
|
+
end
|
68
|
+
|
69
|
+
DNSSD::Service.new(Resolv::DNS::Name.new(["_#{name}", proto] + @domain.to_a))
|
70
|
+
end
|
71
|
+
|
72
|
+
# Create a new DNSSD::ServiceInstance.
|
73
|
+
#
|
74
|
+
# If you know everything about what you're trying to talk to except the
|
75
|
+
# server list, you can go straight to the boss level with this method.
|
76
|
+
#
|
77
|
+
# @param name [String] the name of the service instance.
|
78
|
+
#
|
79
|
+
# @param service_name [String] the generic name of the service which the
|
80
|
+
# desired instance implements, *without* the leading underscore that
|
81
|
+
# is in the DNS name.
|
82
|
+
#
|
83
|
+
# @param service_protocol [Symbol] one of `:TCP` or `:UDP`.
|
84
|
+
#
|
85
|
+
# @return [DNSSD::ServiceInstance]
|
86
|
+
#
|
87
|
+
def service_instance(name, service_name, service_protocol)
|
88
|
+
service(service_name, service_protocol).instance(name)
|
89
|
+
end
|
90
|
+
|
91
|
+
# Enumerate all known services in the domain.
|
92
|
+
#
|
93
|
+
# RFC6763 s. 9 provides a special "Service Type Enumeration" DNS record,
|
94
|
+
# `_services._dns-sd._udp.<domain>`, which is a list of PTR records for
|
95
|
+
# the services available in the domain. If your DNS-SD registration system
|
96
|
+
# provisions names in there, you can use this to enumerate the available
|
97
|
+
# services.
|
98
|
+
#
|
99
|
+
# @return [Hash<String, DNSSD::Service>] the list of services, indexed by
|
100
|
+
# the service name.
|
101
|
+
#
|
102
|
+
def services
|
103
|
+
{}.tap do |services|
|
104
|
+
cached_resources(Resolv::DNS::Name.new(["_services", "_dns-sd", "_udp"] + @domain.to_a), Resolv::DNS::Resource::IN::PTR).each do |ptr|
|
105
|
+
svc = DNSSD::Service.new(ptr.name)
|
106
|
+
services[svc.name] = svc
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
require 'resolv'
|
2
|
+
|
3
|
+
class DNSSD
|
4
|
+
# A mix-in to provide a TTL-respecting caching layer.
|
5
|
+
module ResourceCache
|
6
|
+
private
|
7
|
+
|
8
|
+
# Return a list of resource records.
|
9
|
+
#
|
10
|
+
# We'll ask the DNS, via `Resolv::DNS`, for DNS records matching the
|
11
|
+
# given FQDN and type, unless we've recently seen a response for the
|
12
|
+
# same query, in which case we'll just give that back instead.
|
13
|
+
#
|
14
|
+
# Note that we don't currently implement negative caching, but it's
|
15
|
+
# not a massive optimisation for our use-case, anyway.
|
16
|
+
#
|
17
|
+
# @param fqdn [Resolv::DNS::Name] the name to look up resources at.
|
18
|
+
#
|
19
|
+
# @param type [Resolv::DNS::Resource] the type of resource to request.
|
20
|
+
#
|
21
|
+
# @return [Array<Resolv::DNS::Resource>]
|
22
|
+
#
|
23
|
+
def cached_resources(fqdn, type)
|
24
|
+
@rrcache ||= {}
|
25
|
+
|
26
|
+
k = [fqdn, type]
|
27
|
+
|
28
|
+
if @rrcache[k] && @rrcache[k][:expiry] > Time.now
|
29
|
+
@rrcache[k][:records].dup
|
30
|
+
else
|
31
|
+
Resolv::DNS.new.getresources(fqdn, type).tap do |rrs|
|
32
|
+
@rrcache[k] = { records: rrs.dup, expiry: Time.now + rrs.map { |rr| rr.ttl }.min }
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def entry_expiry_time(fqdn, type)
|
38
|
+
k = [fqdn, type]
|
39
|
+
|
40
|
+
if @rrcache && @rrcache[k]
|
41
|
+
@rrcache[k][:expiry].dup
|
42
|
+
else
|
43
|
+
nil
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,96 @@
|
|
1
|
+
require 'dns-sd/resource_cache'
|
2
|
+
require 'dns-sd/service_instance'
|
3
|
+
|
4
|
+
require 'resolv'
|
5
|
+
|
6
|
+
class DNSSD
|
7
|
+
# The generic service.
|
8
|
+
#
|
9
|
+
# Allows you to get the list of instances of a given service.
|
10
|
+
#
|
11
|
+
class Service
|
12
|
+
include DNSSD::ResourceCache
|
13
|
+
|
14
|
+
# The name of the service, without the leading underscore.
|
15
|
+
#
|
16
|
+
# @return [String]
|
17
|
+
#
|
18
|
+
attr_reader :name
|
19
|
+
|
20
|
+
# The protocol of the service.
|
21
|
+
#
|
22
|
+
# @return [Symbol] `:TCP` or `:UDP`.
|
23
|
+
#
|
24
|
+
attr_reader :protocol
|
25
|
+
|
26
|
+
# Create the service.
|
27
|
+
#
|
28
|
+
# @param fqdn [Resolv::DNS::Name]
|
29
|
+
#
|
30
|
+
def initialize(fqdn)
|
31
|
+
unless fqdn.is_a?(Resolv::DNS::Name)
|
32
|
+
raise ArgumentError,
|
33
|
+
"FQDN must be a Resolv::DNS::Name (got an instance of #{fqdn.class})"
|
34
|
+
end
|
35
|
+
|
36
|
+
@fqdn = fqdn
|
37
|
+
|
38
|
+
if fqdn[0].to_s =~ /\A_([A-Za-z0-9][A-Za-z0-9-]+)\z/
|
39
|
+
@name = $1
|
40
|
+
else
|
41
|
+
raise ArgumentError,
|
42
|
+
"Invalid service name #{fqdn[0].inspect}; see RFC6763 s. 7"
|
43
|
+
end
|
44
|
+
|
45
|
+
@protocol = case fqdn[1].to_s.downcase
|
46
|
+
when "_tcp"
|
47
|
+
:TCP
|
48
|
+
when "_udp"
|
49
|
+
:UDP
|
50
|
+
else
|
51
|
+
raise ArgumentError,
|
52
|
+
"Invalid service protocol #{@protocol}, must be '_tcp' or '_udp'"
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
# Create an object for a specific instance of this service.
|
57
|
+
#
|
58
|
+
# @param name [String] the name of the service instance.
|
59
|
+
#
|
60
|
+
# @return [DNSSD::ServiceInstance]
|
61
|
+
#
|
62
|
+
def instance(name)
|
63
|
+
DNSSD::ServiceInstance.new(Resolv::DNS::Name.new([name] + @fqdn.to_a))
|
64
|
+
end
|
65
|
+
|
66
|
+
# Enumerate all existing instances of this service.
|
67
|
+
#
|
68
|
+
# @return [Hash<String, DNSSD::ServiceInstance>] objects for all the
|
69
|
+
# service instances, indexed by their names.
|
70
|
+
#
|
71
|
+
def instances
|
72
|
+
{}.tap do |instances|
|
73
|
+
cached_resources(@fqdn, Resolv::DNS::Resource::IN::PTR).each do |rr|
|
74
|
+
i = DNSSD::ServiceInstance.new(rr.name)
|
75
|
+
instances[i.name] = i
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
# Let us know how long until the cache expires.
|
81
|
+
#
|
82
|
+
# This can be handy if you've got something that wants to poll the record
|
83
|
+
# repeatedly; this way we can be a bit more intelligent about when to
|
84
|
+
# retry, since if we re-poll too often, we'll just get the cached data back
|
85
|
+
# again anyway.
|
86
|
+
#
|
87
|
+
# @return [Time, nil] if the entry is currently cached, a Time instance
|
88
|
+
# will be returned indicating when the entry will expire (potentially
|
89
|
+
# this could be in the past, if the cache has expired). If we have no
|
90
|
+
# knowledge of the entry, `nil` will be returned.
|
91
|
+
#
|
92
|
+
def cached_until
|
93
|
+
entry_expiry_time(@fqdn, Resolv::DNS::Resource::IN::PTR)
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
@@ -0,0 +1,154 @@
|
|
1
|
+
require 'dns-sd/resource_cache'
|
2
|
+
require 'dns-sd/service'
|
3
|
+
require 'dns-sd/target'
|
4
|
+
|
5
|
+
require 'resolv'
|
6
|
+
|
7
|
+
class DNSSD
|
8
|
+
# A single instance of a service.
|
9
|
+
#
|
10
|
+
# This is where the rubber hits the road: servers to talk to, and instance
|
11
|
+
# metadata, is all under here.
|
12
|
+
#
|
13
|
+
class ServiceInstance
|
14
|
+
include DNSSD::ResourceCache
|
15
|
+
|
16
|
+
# The name of this service instance.
|
17
|
+
#
|
18
|
+
# This is just the left-most component of the service instance FQDN, and
|
19
|
+
# can, as such, contain practically anything at all.
|
20
|
+
#
|
21
|
+
# @return [String]
|
22
|
+
#
|
23
|
+
attr_reader :name
|
24
|
+
|
25
|
+
# The FQDN of this service instance.
|
26
|
+
#
|
27
|
+
# @return [Resolv::DNS::Name]
|
28
|
+
#
|
29
|
+
attr_reader :fqdn
|
30
|
+
|
31
|
+
# The generic service which this instance implements.
|
32
|
+
#
|
33
|
+
# If you happen to forget what protocol to use, this might come in
|
34
|
+
# handy.
|
35
|
+
#
|
36
|
+
# @return [DNSSD::Service]
|
37
|
+
#
|
38
|
+
attr_reader :service
|
39
|
+
|
40
|
+
# Create the service instance.
|
41
|
+
#
|
42
|
+
# @param fqdn [Resolv::DNS::Name]
|
43
|
+
#
|
44
|
+
def initialize(fqdn)
|
45
|
+
unless fqdn.is_a?(Resolv::DNS::Name)
|
46
|
+
raise ArgumentError,
|
47
|
+
"FQDN must be a Resolv::DNS::Name (got an instance of #{fqdn.class})"
|
48
|
+
end
|
49
|
+
|
50
|
+
@fqdn = fqdn
|
51
|
+
|
52
|
+
@name = fqdn[0].to_s
|
53
|
+
@service = DNSSD::Service.new(Resolv::DNS::Name.new(fqdn[1..-1]))
|
54
|
+
end
|
55
|
+
|
56
|
+
# Return the metadata for the service instance.
|
57
|
+
#
|
58
|
+
# RFC6763 s. 6 describes a means by which specially formatted TXT records
|
59
|
+
# can be used to provide metadata for a service instance. If your
|
60
|
+
# services populate such data, you can access it here.
|
61
|
+
#
|
62
|
+
# @return [Hash<String, String or nil>] the key-value metadata, presented
|
63
|
+
# as a nice hash for your looking-up convenience. If your metadata
|
64
|
+
# contains "Attribute present, with no value" tags, then the value of
|
65
|
+
# the associated key will be `nil`, whereas "Attribute present, with
|
66
|
+
# empty value" will have a value of the empty string (`""`).
|
67
|
+
#
|
68
|
+
def data
|
69
|
+
{}.tap do |data|
|
70
|
+
cached_resources(@fqdn, Resolv::DNS::Resource::IN::TXT).each do |rr|
|
71
|
+
rr.strings.each do |s|
|
72
|
+
s =~ /\A([^=]+)(=(.*))?$/
|
73
|
+
data[$1.to_sym] = $3
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
# The things to connect to for this service instance.
|
80
|
+
#
|
81
|
+
# This is what you're here for, I'll bet. Everything comes down to this.
|
82
|
+
# Each DNSSD::Target object in this list contains an FQDN (`#hostname`) and
|
83
|
+
# port (`#port`) to connect to, which you can walk in order in order to
|
84
|
+
# get to something that will talk to you.
|
85
|
+
#
|
86
|
+
# Every time you call this method, even if the records are cached, you may
|
87
|
+
# get the targets in a different order. This is because we automatically
|
88
|
+
# sort the list of targets according to the rules for SRV record priority
|
89
|
+
# and weight. Thus, it is recommended that every time you want to make a
|
90
|
+
# connection to the service instance, you call `#targets` again, both
|
91
|
+
# because the DNS records may have expired (and thus will be re-queried),
|
92
|
+
# but also because it'll ensure that the weight-based randomisation of the
|
93
|
+
# server list is respected.
|
94
|
+
#
|
95
|
+
# @return [Array<DNSSD::Target>]
|
96
|
+
def targets
|
97
|
+
[].tap do |list|
|
98
|
+
left = cached_resources(@fqdn, Resolv::DNS::Resource::IN::SRV)
|
99
|
+
|
100
|
+
# Happily, this algorithm, whilst a bit involved, maps quite directly
|
101
|
+
# to the description from RFC2782, page 4, of which parts are quoted as
|
102
|
+
# appropriate below. A practical example of how this process runs is
|
103
|
+
# described in the test suite, also, which might help explain what's
|
104
|
+
# happening.
|
105
|
+
#
|
106
|
+
# > This process is repeated for each Priority.
|
107
|
+
until left.empty?
|
108
|
+
# > A client MUST attempt to contact the target host with the
|
109
|
+
# > lowest-numbered priority it can reach; target hosts with the
|
110
|
+
# > same priority SHOULD be tried in an order defined by the weight
|
111
|
+
# > field.
|
112
|
+
prio = left.map { |rr| rr.priority }.uniq.min
|
113
|
+
|
114
|
+
# > The following algorithm SHOULD be used to order the SRV RRs of the
|
115
|
+
# > same priority:
|
116
|
+
candidates = left.select { |rr| rr.priority == prio }
|
117
|
+
left -= candidates
|
118
|
+
|
119
|
+
# > arrange all SRV RRs (that have not been ordered yet) in any
|
120
|
+
# > order, except that all those with weight 0 are placed at the
|
121
|
+
# > beginning of the list.
|
122
|
+
#
|
123
|
+
# Because it makes it easier to test, I like to sort by weight and
|
124
|
+
# name (<lawyer>it counts as "any order"</lawyer>). This does mean
|
125
|
+
# that all the zero-weight entries come back in name order, but
|
126
|
+
# if you don't want that behaviour, it's easy enough to give
|
127
|
+
# everything weight=1 and they'll be properly randomised.
|
128
|
+
candidates.sort_by! { |rr| [rr.weight, rr.target.to_s] }
|
129
|
+
|
130
|
+
# > Continue the ordering process until there are no unordered SRV
|
131
|
+
# > RRs.
|
132
|
+
until candidates.empty?
|
133
|
+
# > Compute the sum of the weights of those RRs, and with each RR
|
134
|
+
# > associate the running sum in the selected order. Then choose a
|
135
|
+
# > uniform random number between 0 and the sum computed
|
136
|
+
# > (inclusive)
|
137
|
+
selector = rand(candidates.inject(1) { |n, rr| n + rr.weight })
|
138
|
+
|
139
|
+
# > select the RR whose running sum value is the first in the
|
140
|
+
# > selected order which is greater than or equal to the random
|
141
|
+
# > number selected
|
142
|
+
chosen = candidates.inject(0) do |n, rr|
|
143
|
+
break rr if n + rr.weight >= selector
|
144
|
+
n + rr.weight
|
145
|
+
end
|
146
|
+
# > Remove this SRV RR from the set of the unordered SRV RRs
|
147
|
+
candidates.delete(chosen)
|
148
|
+
list << DNSSD::Target.new(chosen.target.to_s, chosen.port)
|
149
|
+
end
|
150
|
+
end
|
151
|
+
end
|
152
|
+
end
|
153
|
+
end
|
154
|
+
end
|