net-mdns 0.4

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,49 @@
1
+ =begin
2
+ Copyright (C) 2005 Sam Roberts
3
+
4
+ This library is free software; you can redistribute it and/or modify it
5
+ under the same terms as the ruby language itself, see the file COPYING for
6
+ details.
7
+ =end
8
+
9
+ require 'net/dns/resolvx'
10
+
11
+ BasicSocket.do_not_reverse_lookup = true
12
+
13
+ module Net
14
+ # DNS exposes some of Resolv::DNS from resolv.rb to make them easier to use
15
+ # outside of the context of the Resolv class and it's DNS resolver - such as
16
+ # in MDNS. In particular, Net::DNS can be included so that full names to DNS
17
+ # classes in Resolv::DNS can be imported into your namespace.
18
+ module DNS
19
+
20
+ Message = Resolv::DNS::Message
21
+ Name = Resolv::DNS::Name
22
+ DecodeError = Resolv::DNS::DecodeError
23
+
24
+ module IN
25
+ A = Resolv::DNS::Resource::IN::A
26
+ AAAA = Resolv::DNS::Resource::IN::AAAA
27
+ ANY = Resolv::DNS::Resource::IN::ANY
28
+ CNAME = Resolv::DNS::Resource::IN::CNAME
29
+ HINFO = Resolv::DNS::Resource::IN::HINFO
30
+ MINFO = Resolv::DNS::Resource::IN::MINFO
31
+ MX = Resolv::DNS::Resource::IN::MX
32
+ NS = Resolv::DNS::Resource::IN::NS
33
+ PTR = Resolv::DNS::Resource::IN::PTR
34
+ SOA = Resolv::DNS::Resource::IN::SOA
35
+ SRV = Resolv::DNS::Resource::IN::SRV
36
+ TXT = Resolv::DNS::Resource::IN::TXT
37
+ WKS = Resolv::DNS::Resource::IN::WKS
38
+ end
39
+
40
+ # Returns the resource record name of +rr+ as a short string ("IN::A",
41
+ # ...).
42
+ def self.rrname(rr)
43
+ rr = rr.class unless rr.class == Class
44
+ rr = rr.to_s.sub(/.*Resource::/, '')
45
+ rr = rr.to_s.sub(/.*DNS::/, '')
46
+ end
47
+ end
48
+ end
49
+
@@ -0,0 +1,240 @@
1
+ =begin
2
+ Copyright (C) 2005 Sam Roberts
3
+
4
+ This library is free software; you can redistribute it and/or modify it
5
+ under the same terms as the ruby language itself, see the file COPYING for
6
+ details.
7
+ =end
8
+
9
+ require 'net/dns/mdns'
10
+
11
+ module Net
12
+ module DNS
13
+
14
+ # = DNS-SD over mDNS
15
+ #
16
+ # An implementation of DNS Service-Discovery (DNS-SD) using Net::DNS::MDNS.
17
+ #
18
+ # DNS-SD is described in draft-cheshire-dnsext-dns-sd.txt, see
19
+ # http://www.dns-sd.org for more information. It is most often seen as part
20
+ # of Apple's OS X, but is widely useful.
21
+ #
22
+ # These APIs accept and return a set of arguments which are documented once,
23
+ # here, for convenience.
24
+ #
25
+ # - type: DNS-SD classifies services into types using a naming convention.
26
+ # That convention is <_service>.<_protocol>. The underscores ("_") serve
27
+ # to differentiate from normal DNS names. Protocol is always one of
28
+ # "_tcp" or "_udp". The service is a short name, see the list at
29
+ # http://www.dns-sd.org/ServiceTypes.html. A common service is "http", the type
30
+ # of which would be "_http._tcp".
31
+ #
32
+ # - domain: Services operate in a domain, theoretically. In current practice,
33
+ # that domain is always "local".
34
+ #
35
+ # - name: Service lookup with #browse results in a name of a service of that
36
+ # type. That name is associated with a target (a host name), port,
37
+ # priority, and weight, as well as series of key to value mappings,
38
+ # specific to the service. In practice, priority and weight are widely
39
+ # ignored.
40
+ #
41
+ # - fullname: The concatention of the service name (optionally), type, and
42
+ # domain results in a single dot-seperated domain name - the "fullname".
43
+ # See Util.parse_name for more information about the format.
44
+ #
45
+ # - text_record: Service information in the form of key/value pairs.
46
+ # See Util.parse_strings for more information about the format.
47
+ #
48
+ # - flags: should return flags, similar to DNSSD, but for now we just return the
49
+ # TTL of the DNS message. A TTL of zero means a deregistration of the record.
50
+ #
51
+ # Services are advertised and resolved over specific network interfaces.
52
+ # Currently, Net::DNS::MDNS supports only a single default interface, and
53
+ # the interface will always be +nil+.
54
+ module MDNSSD
55
+
56
+ # A reply yielded by #browse, see MDNSSD for a description of the attributes.
57
+ class BrowseReply
58
+ attr_reader :interface, :fullname, :name, :type, :domain, :flags
59
+ def initialize(an) # :nodoc:
60
+ @interface = nil
61
+ @fullname = an.name.to_s
62
+ @domain, @type, @name = MDNSSD::Util.parse_name(an.data.name)
63
+ @flags = an.ttl
64
+ end
65
+ end
66
+
67
+ # Lookup a service by +type+ and +domain+.
68
+ #
69
+ # Yields a BrowseReply as services are found, in a background thread, not
70
+ # the caller's thread!
71
+ #
72
+ # Returns a MDNS::BackgroundQuery, call MDNS::BackgroundQuery#stop when
73
+ # you have found all the replies you are interested in.
74
+ def self.browse(type, domain = '.local', *ignored) # :yield: BrowseReply
75
+ dnsname = DNS::Name.create(type)
76
+ dnsname << DNS::Name.create(domain)
77
+ dnsname.absolute = true
78
+
79
+ q = MDNS::BackgroundQuery.new(dnsname, IN::PTR) do |q, answers|
80
+ answers.each do |an|
81
+ yield BrowseReply.new( an )
82
+ end
83
+ end
84
+ q
85
+ end
86
+
87
+ # A reply yielded by #resolve, see MDNSSD for a description of the attributes.
88
+ class ResolveReply
89
+ attr_reader :interface, :fullname, :name, :type, :domain, :target, :port, :priority, :weight, :text_record, :flags
90
+ def initialize(ansrv, antxt) # :nodoc:
91
+ @interface = nil
92
+ @fullname = ansrv.name.to_s
93
+ @domain, @type, @name = MDNSSD::Util.parse_name(ansrv.name)
94
+ @target = ansrv.data.target.to_s
95
+ @port = ansrv.data.port
96
+ @priority = ansrv.data.priority
97
+ @weight = ansrv.data.weight
98
+ @text_record = MDNSSD::Util.parse_strings(antxt.data.strings)
99
+ @flags = ansrv.ttl
100
+ end
101
+ end
102
+
103
+ # Resolve a service instance by +name+, +type+ and +domain+.
104
+ #
105
+ # Yields a ResolveReply as service instances are found, in a background
106
+ # thread, not the caller's thread!
107
+ #
108
+ # Returns a MDNS::BackgroundQuery, call MDNS::BackgroundQuery#stop when
109
+ # you have found all the replies you are interested in.
110
+ def self.resolve(name, type, domain = '.local', *ignored) # :yield: ResolveReply
111
+ dnsname = DNS::Name.create(name)
112
+ dnsname << DNS::Name.create(type)
113
+ dnsname << DNS::Name.create(domain)
114
+ dnsname.absolute = true
115
+
116
+ rrs = {}
117
+
118
+ q = MDNS::BackgroundQuery.new(dnsname, IN::ANY) do |q, answers|
119
+ _rrs = {}
120
+ answers.each do |an|
121
+ if an.name == dnsname
122
+ _rrs[an.type] = an
123
+ end
124
+ end
125
+ # We queried for ANY, but don't yield unless we got a SRV or TXT.
126
+ if( _rrs[IN::SRV] || _rrs[IN::TXT] )
127
+ rrs.update _rrs
128
+
129
+ ansrv, antxt = rrs[IN::SRV], rrs[IN::TXT]
130
+
131
+ # puts "ansrv->#{ansrv}"
132
+ # puts "antxt->#{antxt}"
133
+
134
+ # Even though we got an SRV or TXT, we can't yield until we have both.
135
+ if ansrv && antxt
136
+ yield ResolveReply.new( ansrv, antxt )
137
+ end
138
+ end
139
+ end
140
+ q
141
+ end
142
+
143
+ # A reply yielded by #register, see MDNSSD for a description of the attributes.
144
+ class RegisterReply
145
+ attr_reader :interface, :fullname, :name, :type, :domain
146
+ def initialize(name, type, domain)
147
+ @interface = nil
148
+ @fullname = (DNS::Name.create(name) << type << domain).to_s
149
+ @name, @type, @domain = name, type, domain
150
+ end
151
+ end
152
+
153
+ # Register a service instance on the local host.
154
+ #
155
+ # +txt+ is a Hash of String keys to String values.
156
+ #
157
+ # Because the service +name+ may already be in use on the network, a
158
+ # different name may be registered than that requested. Because of this,
159
+ # if a block is supplied, a RegisterReply will be yielded so that the
160
+ # actual service name registered may be seen.
161
+ #
162
+ # Returns a MDNS::Service, call MDNS::Service#stop when you no longer
163
+ # want to advertise the service.
164
+ #
165
+ # NOTE - The service +name+ should be unique on the network, MDNSSD
166
+ # doesn't currently attempt to ensure this. This will be fixed in
167
+ # an upcoming release.
168
+ def self.register(name, type, domain, port, txt = {}, *ignored) # :yields: RegisterReply
169
+ dnsname = DNS::Name.create(name)
170
+ dnsname << DNS::Name.create(type)
171
+ dnsname << DNS::Name.create(domain)
172
+ dnsname.absolute = true
173
+
174
+ s = MDNS::Service.new(name, type, port, txt) do |s|
175
+ s.domain = domain
176
+ end
177
+
178
+ yield RegisterReply.new(name, type, domain) if block_given?
179
+
180
+ s
181
+ end
182
+
183
+ # Utility routines not for general use.
184
+ module Util
185
+ # Decode a DNS-SD domain name. The format is:
186
+ # [<instance>.]<_service>.<_protocol>.<domain>
187
+ #
188
+ # Examples are:
189
+ # _http._tcp.local
190
+ # guest._http._tcp.local
191
+ # Ensemble Musique._daap._tcp.local
192
+ #
193
+ # The <_service>.<_protocol> combined is the <type>.
194
+ #
195
+ # Return either:
196
+ # [ <domain>, <type> ]
197
+ # or
198
+ # [ <domain>, <type>, <instance>]
199
+ #
200
+ # Because of the order of the return values, it can be called like:
201
+ # domain, type = MDNSSD::Util.parse_name(fullname)
202
+ # or
203
+ # domain, type, name = MDNSSD::Util.parse_name(fullname)
204
+ # If there is no name component to fullname, name will be nil.
205
+ def self.parse_name(dnsname)
206
+ domain, t1, t0, name = dnsname.to_a.reverse.map {|n| n.to_s}
207
+ [ domain, t0 + '.' + t1, name].compact
208
+ end
209
+
210
+ # Decode TXT record strings, an array of String.
211
+ #
212
+ # DNS-SD defines formatting conventions for them:
213
+ # - Keys must be at least one char in range (0x20-0x7E), excluding '='
214
+ # (0x3D), and they must be matched case-insensitively.
215
+ # - There may be no '=', in which case value is nil.
216
+ # - There may be an '=' with no value, in which case value is empty string, "".
217
+ # - Anything following the '=' is a value, it is not case sensitive, can be binary,
218
+ # and can include whitespace.
219
+ # - Discard all keys but the first.
220
+ # - Discard a string that aren't formatting accorded to these rules.
221
+ def self.parse_strings(strings)
222
+ h = {}
223
+
224
+ strings.each do |kv|
225
+ if kv.match( /^([\x20-\x3c\x3f-\x7e]+)(?:=(.*))?$/ )
226
+ key = $1.downcase
227
+ value = $2
228
+ next if h.has_key? key
229
+ h[key] = value
230
+ end
231
+ end
232
+
233
+ h
234
+ end
235
+ end
236
+
237
+ end
238
+ end
239
+ end
240
+
@@ -0,0 +1,1189 @@
1
+ =begin
2
+ Copyright (C) 2005 Sam Roberts
3
+
4
+ This library is free software; you can redistribute it and/or modify it
5
+ under the same terms as the ruby language itself, see the file COPYING for
6
+ details.
7
+ =end
8
+
9
+ require 'ipaddr'
10
+ require 'logger'
11
+ require 'singleton'
12
+
13
+ require 'net/dns'
14
+ require 'net/dns/resolvx'
15
+
16
+ BasicSocket.do_not_reverse_lookup = true
17
+
18
+ module Net
19
+ module DNS
20
+
21
+ #:main:Net::DNS::MDNS
22
+ #:title:net-mdns - multicast DNS and DNS service discovery
23
+ #
24
+ # Author:: Sam Roberts <sroberts@uniserve.com>
25
+ # Copyright:: Copyright (C) 2005 Sam Roberts
26
+ # License:: May be distributed under the same terms as Ruby
27
+ # Version:: 0.4
28
+ # Homepage:: http://dnssd.rubyforge.org/net-mdns
29
+ # Download:: http://rubyforge.org/frs/?group_id=316
30
+ #
31
+ # == Summary
32
+ #
33
+ # An implementation of a multicast DNS (mDNS) responder. mDNS is an
34
+ # extension of hierarchical, unicast DNS to link-local multicast, used to
35
+ # do service discovery and address lookups over local networks. It is
36
+ # most widely known because it is part of Apple's OS X.
37
+ #
38
+ # net-mdns consists of:
39
+ # - Net::DNS::MDNSSD: a high-level API for browsing, resolving, and advertising
40
+ # services using DNS-SD over mDNS that aims to be compatible with DNSSD, see
41
+ # below for more information.
42
+ # - Resolv::MDNS: an extension to the 'resolv' resolver library that adds
43
+ # support for multicast DNS.
44
+ # - Net::DNS::MDNS: the low-level APIs and mDNS responder at the core of
45
+ # Resolv::MDNS and Net::DNS::MDNSSD.
46
+ #
47
+ # net-mdns can be used for:
48
+ # - name to address lookups on local networks
49
+ # - address to name lookups on local networks
50
+ # - discovery of services on local networks
51
+ # - advertisement of services on local networks
52
+ #
53
+ # == Client Example
54
+ #
55
+ # This is an example of finding all _http._tcp services, connecting to
56
+ # them, and printing the 'Server' field of the HTTP headers using Net::HTTP
57
+ # (from link:exhttp.txt):
58
+ #
59
+ # require 'net/http'
60
+ # require 'thread'
61
+ # require 'pp'
62
+ #
63
+ # # For MDNSSD
64
+ # require 'net/dns/mdns-sd'
65
+ #
66
+ # # To make Resolv aware of mDNS
67
+ # require 'net/dns/resolv-mdns'
68
+ #
69
+ # # To make TCPSocket use Resolv, not the C library resolver.
70
+ # require 'net/dns/resolv-replace'
71
+ #
72
+ # # Use a short name.
73
+ # DNSSD = Net::DNS::MDNSSD
74
+ #
75
+ # # Sync stdout, and don't write to console from multiple threads.
76
+ # $stdout.sync
77
+ # $lock = Mutex.new
78
+ #
79
+ # # Be quiet.
80
+ # debug = false
81
+ #
82
+ # DNSSD.browse('_http._tcp') do |b|
83
+ # $lock.synchronize { pp b } if debug
84
+ # DNSSD.resolve(b.name, b.type) do |r|
85
+ # $lock.synchronize { pp r } if debug
86
+ # begin
87
+ # http = Net::HTTP.new(r.target, r.port)
88
+ #
89
+ # path = r.text_record['path'] || '/'
90
+ #
91
+ # headers = http.head(path)
92
+ #
93
+ # $lock.synchronize do
94
+ # puts "#{r.name.inspect} on #{r.target}:#{r.port}#{path} was last-modified #{headers['server']}"
95
+ # end
96
+ # rescue
97
+ # $lock.synchronize { puts $!; puts $!.backtrace }
98
+ # end
99
+ # end
100
+ # end
101
+ #
102
+ # # Hit enter when you think that's all.
103
+ # STDIN.gets
104
+ #
105
+ # == Server Example
106
+ #
107
+ # This is an example of advertising a webrick server using DNS-SD (from
108
+ # link:exwebrick.txt).
109
+ #
110
+ # require 'webrick'
111
+ # require 'net/dns/mdns-sd'
112
+ #
113
+ # DNSSD = Net::DNS::MDNSSD
114
+ #
115
+ # class HelloServlet < WEBrick::HTTPServlet::AbstractServlet
116
+ # def do_GET(req, resp)
117
+ # resp.body = "hello, world\n"
118
+ # resp['content-type'] = 'text/plain'
119
+ # raise WEBrick::HTTPStatus::OK
120
+ # end
121
+ # end
122
+ #
123
+ # server = WEBrick::HTTPServer.new( :Port => 8080 )
124
+ #
125
+ # server.mount( '/hello/', HelloServlet )
126
+ #
127
+ # handle = DNSSD.register("hello", '_http._tcp', 'local', 8080, 'path' => '/hello/')
128
+ #
129
+ # ['INT', 'TERM'].each { |signal|
130
+ # trap(signal) { server.shutdown; handle.stop; }
131
+ # }
132
+ #
133
+ # server.start
134
+ #
135
+ # == Samples
136
+ #
137
+ # There are a few command line utilities in the samples/ directory:
138
+ # - link:mdns.txt, mdns.rb is a command line interface for to Net::DNS::MDNSSD (or to DNSSD)
139
+ # - link:v1demo.txt, v1demo.rb is a sample provided by Ben Giddings showing
140
+ # the call sequences to use with Resolv::MDNS for service resolution. This
141
+ # predates Net::DNS::MDNSSD, so while its a great sample, you might want
142
+ # to look at mdns.rb instead.
143
+ # - link:v1mdns.txt, v1mdns.rb is a low-level utility for exercising Resolv::MDNS.
144
+ # - link:mdns-watch.txt, mdns-watch.rb is a utility that dumps all mDNS traffic, useful
145
+ # for debugging.
146
+ #
147
+ # == Comparison to the DNS-SD Extension
148
+ #
149
+ # The DNS-SD project at http://dnssd.rubyforge.org is another
150
+ # approach to mDNS and service discovery.
151
+ #
152
+ # DNS-SD is a compiled ruby extension implemented on top of the dns_sd.h APIs
153
+ # published by Apple. These APIs work by contacting a local mDNS daemon
154
+ # (through unix domain sockets) and should be more efficient since they
155
+ # use a daemon written in C by a dedicated team at Apple.
156
+ #
157
+ # Currently, the only thing I'm aware of net-mdns doing that DNS-SD
158
+ # doesn't is integrate into the standard library so that link-local domain
159
+ # names can be used throughout the standard networking classes, and allow
160
+ # querying of arbitrary DNS record types. There is no reason DNS-SD can't
161
+ # do this, it just needs to wrap DNSServiceQueryRecord() and expose it, and
162
+ # that will happen sometime soon.
163
+ #
164
+ # Since net-mdns doesn't do significantly more than DNSSD, why would you be
165
+ # interested in it?
166
+ #
167
+ # The DNS-SD extension requires the dns_sd.h C language APIs for the Apple
168
+ # mDNS daemon. Installing the Apple responder can be quite difficult, and
169
+ # requires a running daemon. It also requires compiling the extension. If
170
+ # you need a pure ruby implementation, or if building DNS-SD turns out to be
171
+ # difficult for you, net-mdns may be useful to you.
172
+ #
173
+ # == For More Information
174
+ #
175
+ # See the following:
176
+ # - draft-cheshire-dnsext-multicastdns-04.txt for a description of mDNS
177
+ # - RFC 2782 for a description of DNS SRV records
178
+ # - draft-cheshire-dnsext-dns-sd-02.txt for a description of how to
179
+ # use SRV, PTR, and TXT records for service discovery
180
+ # - http://www.dns-sd.org (a list of services is at http://www.dns-sd.org/ServiceTypes.html).
181
+ # - http://dnssd.rubyforge.org - for DNSSD, a C extension for communicating
182
+ # with Apple's mDNSResponder daemon.
183
+ #
184
+ # == TODO
185
+ #
186
+ # See link:TODO.
187
+ #
188
+ # == Thanks
189
+ #
190
+ # - to Tanaka Akira for resolv.rb, I learned a lot about meta-programming
191
+ # and ruby idioms from it, as well as getting an almost-complete
192
+ # implementation of the DNS message format and a resolver framework I
193
+ # could plug mDNS support into.
194
+ #
195
+ # - to Charles Mills for letting me add net-mdns to DNS-SD's Rubyforge
196
+ # project when he hardly knew me, and hadn't even seen any code yet.
197
+ #
198
+ # - to Ben Giddings for promising to use this if I wrote it, which was
199
+ # the catalyst for resuming a year-old prototype.
200
+ #
201
+ # == Author
202
+ #
203
+ # Any feedback, questions, problems, etc., please contact me, Sam Roberts,
204
+ # via dnssd-developers@rubyforge.org, or directly.
205
+ module MDNS
206
+ class Answer
207
+ attr_reader :name, :ttl, :data, :cacheflush
208
+ # TOA - time of arrival (of an answer)
209
+ attr_reader :toa
210
+ attr_accessor :retries
211
+
212
+ def initialize(name, ttl, data, cacheflush)
213
+ @name = name
214
+ @ttl = ttl
215
+ @data = data
216
+ @cacheflush = cacheflush
217
+ @toa = Time.now.to_i
218
+ @retries = 0
219
+ end
220
+
221
+ def type
222
+ data.class
223
+ end
224
+
225
+ def refresh
226
+ # Percentage points are from mDNS
227
+ percent = [80,85,90,95][retries]
228
+
229
+ # TODO - add a 2% of TTL jitter
230
+ toa + ttl * percent / 100 if percent
231
+ end
232
+
233
+ def expiry
234
+ toa + (ttl == 0 ? 1 : ttl)
235
+ end
236
+
237
+ def expired?
238
+ true if Time.now.to_i > expiry
239
+ end
240
+
241
+ def absolute?
242
+ @cacheflush
243
+ end
244
+
245
+ def to_s
246
+ s = "#{name.to_s} (#{ttl}) "
247
+ s << '!' if absolute?
248
+ s << '-' if ttl == 0
249
+ s << " #{DNS.rrname(data)}"
250
+
251
+ case data
252
+ when IN::A
253
+ s << " #{data.address.to_s}"
254
+ when IN::PTR
255
+ s << " #{data.name}"
256
+ when IN::SRV
257
+ s << " #{data.target}:#{data.port}"
258
+ when IN::TXT
259
+ s << " #{data.strings.first.inspect}#{data.strings.length > 1 ? ', ...' : ''}"
260
+ when IN::HINFO
261
+ s << " os=#{data.os}, cpu=#{data.cpu}"
262
+ else
263
+ s << data.inspect
264
+ end
265
+ s
266
+ end
267
+ end
268
+
269
+ class Question
270
+ attr_reader :name, :type, :retries
271
+ attr_writer :retries
272
+
273
+ # Normally we see our own question, so an update will occur right away,
274
+ # causing retries to be set to 1. If we don't see our own question, for
275
+ # some reason, we'll ask again a second later.
276
+ RETRIES = [1, 1, 2, 4]
277
+
278
+ def initialize(name, type)
279
+ @name = name
280
+ @type = type
281
+
282
+ @lastq = Time.now.to_i
283
+
284
+ @retries = 0
285
+ end
286
+
287
+ # Update the number of times the question has been asked based on having
288
+ # seen the question, so that the question is considered asked whether
289
+ # we asked it, or another machine/process asked.
290
+ def update
291
+ @retries += 1
292
+ @lastq = Time.now.to_i
293
+ end
294
+
295
+ # Questions are asked 4 times, repeating at increasing intervals of 1,
296
+ # 2, and 4 seconds.
297
+ def refresh
298
+ r = RETRIES[retries]
299
+ @lastq + r if r
300
+ end
301
+
302
+ def to_s
303
+ "#{@name.to_s}/#{DNS.rrname @type} (#{@retries})"
304
+ end
305
+ end
306
+
307
+ class Cache # :nodoc:
308
+ # asked: Hash[Name] -> Hash[Resource] -> Question
309
+ attr_reader :asked
310
+
311
+ # cached: Hash[Name] -> Hash[Resource] -> Array -> Answer
312
+ attr_reader :cached
313
+
314
+ def initialize
315
+ @asked = Hash.new { |h,k| h[k] = Hash.new }
316
+
317
+ @cached = Hash.new { |h,k| h[k] = (Hash.new { |a,b| a[b] = Array.new }) }
318
+ end
319
+
320
+ # Return the question if we added it, or nil if question is already being asked.
321
+ def add_question(qu)
322
+ if qu && !@asked[qu.name][qu.type]
323
+ @asked[qu.name][qu.type] = qu
324
+ end
325
+ end
326
+
327
+ # Cache question. Increase the number of times we've seen it.
328
+ def cache_question(name, type)
329
+ if qu = @asked[name][type]
330
+ qu.update
331
+ end
332
+ qu
333
+ end
334
+
335
+ # Return cached answer, or nil if answer wasn't cached.
336
+ def cache_answer(an)
337
+ answers = @cached[an.name][an.type]
338
+
339
+ if( an.absolute? )
340
+ # Replace all answers older than a ~1 sec [mDNS].
341
+ # If the data is the same, don't delete it, we don't want it to look new.
342
+ now_m1 = Time.now.to_i - 1
343
+ answers.delete_if { |a| a.toa < now_m1 && a.data != an.data }
344
+ end
345
+
346
+ old_an = answers.detect { |a| a.name == an.name && a.data == an.data }
347
+
348
+ if( !old_an )
349
+ # new answer, cache it
350
+ answers << an
351
+ elsif( an.ttl == 0 )
352
+ # it's a "remove" notice, replace old_an
353
+ answers.delete( old_an )
354
+ answers << an
355
+ elsif( an.expiry > old_an.expiry)
356
+ # it's a fresher record than we have, cache it but the data is the
357
+ # same so don't report it as cached
358
+ answers.delete( old_an )
359
+ answers << an
360
+ an = nil
361
+ else
362
+ # don't cache it
363
+ an = nil
364
+ end
365
+
366
+ an
367
+ end
368
+
369
+ def answers_for(name, type)
370
+ answers = []
371
+ if( name.to_s == '*' )
372
+ @cached.keys.each { |n| answers += answers_for(n, type) }
373
+ elsif( type == IN::ANY )
374
+ @cached[name].each { |rtype,rdata| answers += rdata }
375
+ else
376
+ answers += @cached[name][type]
377
+ end
378
+ answers
379
+ end
380
+
381
+ def asked?(name, type)
382
+ return true if name.to_s == '*'
383
+
384
+ t = @asked[name][type] || @asked[name][IN::ANY]
385
+
386
+ # TODO - true if (Time.now - t) < some threshold...
387
+
388
+ t
389
+ end
390
+
391
+ end
392
+
393
+ class Responder # :nodoc:
394
+ include Singleton
395
+
396
+ # mDNS link-local multicast address
397
+ Addr = "224.0.0.251"
398
+ Port = 5353
399
+ UDPSize = 9000
400
+
401
+ attr_reader :cache
402
+ attr_reader :log
403
+ attr_reader :hostname
404
+ attr_reader :hostaddr
405
+ attr_reader :hostrr
406
+
407
+ # Log messages to +log+. +log+ must be +nil+ (no logging) or an object
408
+ # that responds to debug(), warn(), and error(). Default is a Logger to
409
+ # STDERR that logs only ERROR messages.
410
+ def log=(log)
411
+ unless !log || (log.respond_to?(:debug) && log.respond_to?(:warn) && log.respond_to?(:error))
412
+ raise ArgumentError, "log doesn't appear to be a kind of logger"
413
+ end
414
+ @log = log
415
+ end
416
+
417
+ def debug(*args)
418
+ @log.debug( *args ) if @log
419
+ end
420
+ def warn(*args)
421
+ @log.warn( *args ) if @log
422
+ end
423
+ def error(*args)
424
+ @log.error( *args ) if @log
425
+ end
426
+
427
+ def initialize
428
+ @log = Logger.new(STDERR)
429
+
430
+ @log.level = Logger::ERROR
431
+
432
+ @mutex = Mutex.new
433
+
434
+ @cache = Cache.new
435
+
436
+ @queries = []
437
+
438
+ @services = []
439
+
440
+ @hostname = Name.create(Socket.gethostname)
441
+ @hostname.absolute = true
442
+ @hostaddr = Socket.getaddrinfo(@hostname.to_s, 0, Socket::AF_INET, Socket::SOCK_STREAM)[0][3]
443
+ @hostrr = [ @hostname, 240, IN::A.new(@hostaddr) ]
444
+ @hostaddr = IPAddr.new(@hostaddr).hton
445
+
446
+ debug( "start" )
447
+
448
+ # TODO - I'm not sure about how robust this is. A better way to find the default
449
+ # ifx would be to do:
450
+ # s = UDPSocket.new
451
+ # s.connect(any addr, any port)
452
+ # s.getsockname => struct sockaddr_in => ip_addr
453
+ # But parsing a struct sockaddr_in is a PITA in ruby.
454
+
455
+ @sock = UDPSocket.new
456
+
457
+ # Set the close-on-exec flag, if supported.
458
+ if Fcntl.constants.include? 'F_SETFD'
459
+ @sock.fcntl(Fcntl::F_SETFD, 1)
460
+ end
461
+
462
+ # Allow 5353 to be shared.
463
+ so_reuseport = 0x0200
464
+ # The definition on OS X, where it is required, and where the shipped
465
+ # ruby version (1.6) does not have Socket::SO_REUSEPORT. The definition
466
+ # seems to be shared by at least some other BSD-derived stacks.
467
+ if Socket.constants.include? 'SO_REUSEPORT'
468
+ so_reuseport = Socket::SO_REUSEPORT
469
+ end
470
+ begin
471
+ @sock.setsockopt(Socket::SOL_SOCKET, so_reuseport, 1)
472
+ rescue
473
+ warn( "set SO_REUSEPORT raised #{$!}, try SO_REUSEADDR" )
474
+ so_reuseport = Socket::SO_REUSEADDR
475
+ @sock.setsockopt(Socket::SOL_SOCKET, so_reuseport, 1)
476
+ end
477
+
478
+ # Request dest addr and ifx ids... no.
479
+
480
+ # Bind to our port.
481
+ @sock.bind(Socket::INADDR_ANY, Port)
482
+
483
+ # Join the multicast group.
484
+ # option is a struct ip_mreq { struct in_addr, struct in_addr }
485
+ ip_mreq = IPAddr.new(Addr).hton + @hostaddr
486
+ @sock.setsockopt(Socket::IPPROTO_IP, Socket::IP_ADD_MEMBERSHIP, ip_mreq)
487
+ @sock.setsockopt(Socket::IPPROTO_IP, Socket::IP_MULTICAST_IF, @hostaddr)
488
+
489
+ # Set IP TTL for outgoing packets.
490
+ @sock.setsockopt(Socket::IPPROTO_IP, Socket::IP_TTL, 255)
491
+ @sock.setsockopt(Socket::IPPROTO_IP, Socket::IP_MULTICAST_TTL, 255)
492
+
493
+ # Apple source makes it appear that optval may need to be a "char" on
494
+ # some systems:
495
+ # @sock.setsockopt(Socket::IPPROTO_IP, Socket::IP_MULTICAST_TTL, 255 as int)
496
+ # - or -
497
+ # @sock.setsockopt(Socket::IPPROTO_IP, Socket::IP_MULTICAST_TTL, 255 as byte)
498
+
499
+ # Start responder and cacher threads.
500
+
501
+ @waketime = nil
502
+
503
+ @cacher_thrd = Thread.new do
504
+ begin
505
+ cacher_loop
506
+ rescue
507
+ error( "cacher_loop exited with #{$!}" )
508
+ $!.backtrace.each do |e| error(e) end
509
+ end
510
+ end
511
+
512
+ @responder_thrd = Thread.new do
513
+ begin
514
+ responder_loop
515
+ rescue
516
+ error( "responder_loop exited with #{$!}" )
517
+ $!.backtrace.each do |e| error(e) end
518
+ end
519
+ end
520
+ end
521
+
522
+ def responder_loop
523
+ loop do
524
+ # from is [ AF_INET, port, name, addr ]
525
+ reply, from = @sock.recvfrom(UDPSize)
526
+ qaddr = from[3]
527
+ qport = from[1]
528
+
529
+ @mutex.synchronize do
530
+
531
+ begin
532
+ msg = Message.decode(reply)
533
+
534
+ qid = msg.id
535
+ qr = msg.qr == 0 ? 'Q' : 'R'
536
+ qcnt = msg.question.size
537
+ acnt = msg.answer.size
538
+
539
+ debug( "from #{qaddr}:#{qport} -> id #{qid} qr=#{qr} qcnt=#{qcnt} acnt=#{acnt}" )
540
+
541
+ if( msg.query? )
542
+ # Cache questions:
543
+ # - ignore unicast queries
544
+ # - record the question as asked
545
+ # - TODO flush any answers we have over 1 sec old (otherwise if a machine goes down, its
546
+ # answers stay until there ttl, which can be very long!)
547
+ msg.each_question do |name, type, unicast|
548
+ next if unicast
549
+
550
+ debug( "++ q #{name.to_s}/#{DNS.rrname(type)}" )
551
+
552
+ @cache.cache_question(name, type)
553
+ end
554
+
555
+ # Answer questions for registered services:
556
+ # - don't multicast answers to unicast questions
557
+ # - let each service add any records that answer the question
558
+ # - delete duplicate answers
559
+ # - delete known answers (see MDNS:7.1)
560
+ # - send an answer if there are any answers
561
+ amsg = Message.new(0)
562
+ amsg.rd = 0
563
+ amsg.qr = 1
564
+ amsg.aa = 1
565
+ msg.each_question do |name, type, unicast|
566
+ next if unicast
567
+
568
+ debug( "ask? #{name}/#{DNS.rrname(type)}" )
569
+ @services.each do |svc|
570
+ svc.answer_question(name, type, amsg)
571
+ end
572
+ end
573
+
574
+ amsg.question.uniq!
575
+ amsg.answer.uniq!
576
+ amsg.additional.uniq!
577
+
578
+ amsg.answer.delete_if do |an|
579
+ msg.answer.detect do |known|
580
+ # Recall: an = [ name, ttl, data, cacheflush ]
581
+ if(an[0] == known[0] && an[2] == known[2] && (an[1]/2) < known[1])
582
+ true # an is a duplicate, and known is not about to expire
583
+ else
584
+ false
585
+ end
586
+ end
587
+ end
588
+
589
+ send(amsg, qid, qaddr, qport) if amsg.answer.first
590
+
591
+ else
592
+ # Cache answers:
593
+ cached = []
594
+ msg.each_answer do |n, ttl, data, cacheflush|
595
+
596
+ a = Answer.new(n, ttl, data, cacheflush)
597
+ debug( "++ a #{ a }" )
598
+ a = @cache.cache_answer(a)
599
+ debug( " cached" ) if a
600
+
601
+ # If a wasn't cached, then its an answer we already have, don't push it.
602
+ cached << a if a
603
+
604
+ wake_cacher_for(a)
605
+ end
606
+
607
+ # Push answers to Queries:
608
+ # TODO - push all answers, let the Query do what it wants with them.
609
+ @queries.each do |q|
610
+ answers = cached.select { |an| q.subscribes_to? an }
611
+
612
+ debug( "push #{answers.length} to #{q}" )
613
+
614
+ q.push( answers )
615
+ end
616
+
617
+ end
618
+
619
+ rescue DecodeError
620
+ warn( "decode error: #{reply.inspect}" )
621
+ end
622
+
623
+ end # end sync
624
+ end # end loop
625
+ end
626
+
627
+ # wake sweeper if cache item needs refreshing before current waketime
628
+ def wake_cacher_for(item)
629
+ return unless item
630
+
631
+ if !@waketime || @waketime == 0 || item.refresh < @waketime
632
+ @cacher_thrd.wakeup
633
+ end
634
+ end
635
+
636
+ def cacher_loop
637
+ delay = 0
638
+
639
+ loop do
640
+
641
+ if delay > 0
642
+ sleep(delay)
643
+ else
644
+ sleep
645
+ end
646
+
647
+ @mutex.synchronize do
648
+ debug( "sweep begin" )
649
+
650
+ @waketime = nil
651
+
652
+ msg = Message.new(0)
653
+ msg.rd = 0
654
+ msg.qr = 0
655
+ msg.aa = 0
656
+
657
+ now = Time.now.to_i
658
+
659
+ # the earliest question or answer we need to wake for
660
+ wakefor = nil
661
+
662
+ # TODO - A delete expired, that yields every answer before
663
+ # deleting it (so I can log it).
664
+ # TODO - A #each_answer?
665
+ @cache.cached.each do |name,rtypes|
666
+ rtypes.each do |rtype, answers|
667
+ # Delete expired answers.
668
+ answers.delete_if do |an|
669
+ if an.expired?
670
+ debug( "-- a #{an}" )
671
+ true
672
+ end
673
+ end
674
+ # Requery answers that need refreshing, if there is a query that wants it.
675
+ # Remember the earliest one we need to wake for.
676
+ answers.each do |an|
677
+ if an.refresh
678
+ unless @queries.detect { |q| q.subscribes_to? an }
679
+ debug( "no refresh of: a #{an}" )
680
+ next
681
+ end
682
+ if now >= an.refresh
683
+ an.retries += 1
684
+ msg.add_question(name, an.data.class)
685
+ end
686
+ # TODO: cacher_loop exited with comparison of Bignum with nil failed, v2mdns.rb:478:in `<'
687
+ begin
688
+ if !wakefor || an.refresh < wakefor.refresh
689
+ wakefor = an
690
+ end
691
+ rescue
692
+ error( "an #{an.inspect}" )
693
+ error( "wakefor #{wakefor.inspect}" )
694
+ raise
695
+ end
696
+ end
697
+ end
698
+ end
699
+ end
700
+
701
+ @cache.asked.each do |name,rtypes|
702
+ # Delete questions no query subscribes to, and that don't need refreshing.
703
+ rtypes.delete_if do |rtype, qu|
704
+ if !qu.refresh || !@queries.detect { |q| q.subscribes_to? qu }
705
+ debug( "no refresh of: q #{qu}" )
706
+ true
707
+ end
708
+ end
709
+ # Requery questions that need refreshing.
710
+ # Remember the earliest one we need to wake for.
711
+ rtypes.each do |rtype, qu|
712
+ if now >= qu.refresh
713
+ msg.add_question(name, rtype)
714
+ end
715
+ if !wakefor || qu.refresh < wakefor.refresh
716
+ wakefor = qu
717
+ end
718
+ end
719
+ end
720
+
721
+ msg.question.uniq!
722
+
723
+ msg.each_question { |n,r| debug( "-> q #{n} #{DNS.rrname(r)}" ) }
724
+
725
+ send(msg) if msg.question.first
726
+
727
+ @waketime = wakefor.refresh if wakefor
728
+
729
+ if @waketime
730
+ delay = @waketime - Time.now.to_i
731
+ delay = 1 if delay < 1
732
+
733
+ debug( "refresh in #{delay} sec for #{wakefor}" )
734
+ else
735
+ delay = 0
736
+ end
737
+
738
+ debug( "sweep end" )
739
+ end
740
+ end # end loop
741
+ end
742
+
743
+ def send(msg, qid = nil, qaddr = nil, qport = nil)
744
+ begin
745
+ msg.answer.each do |an|
746
+ debug( "-> an #{an[0]} (#{an[1]}) #{an[2].to_s} #{an[3].inspect}" )
747
+ end
748
+ msg.additional.each do |an|
749
+ debug( "-> ad #{an[0]} (#{an[1]}) #{an[2].to_s} #{an[3].inspect}" )
750
+ end
751
+ # Unicast response directly to questioner if source port is not 5353.
752
+ if qport && qport != Port
753
+ debug( "unicast for qid #{qid} to #{qaddr}:#{qport}" )
754
+ msg.id = qid
755
+ @sock.send(msg.encode, 0, qaddr, qport)
756
+ end
757
+ # ID is always zero for mcast, don't repeat questions for mcast
758
+ msg.id = 0
759
+ msg.question.clear unless msg.query?
760
+ @sock.send(msg.encode, 0, Addr, Port)
761
+ rescue
762
+ error( "send msg failed: #{$!}" )
763
+ raise
764
+ end
765
+ end
766
+
767
+ def query_start(query, qu)
768
+ @mutex.synchronize do
769
+ begin
770
+ debug( "start query #{query} with qu #{qu.inspect}" )
771
+
772
+ @queries << query
773
+
774
+ qu = @cache.add_question(qu)
775
+
776
+ wake_cacher_for(qu)
777
+
778
+ answers = @cache.answers_for(query.name, query.type)
779
+
780
+ query.push( answers )
781
+
782
+ # If it wasn't added, then we already are asking the question,
783
+ # don't ask it again.
784
+ if qu
785
+ qmsg = Message.new(0)
786
+ qmsg.rd = 0
787
+ qmsg.qr = 0
788
+ qmsg.aa = 0
789
+ qmsg.add_question(qu.name, qu.type)
790
+
791
+ send(qmsg)
792
+ end
793
+ rescue
794
+ warn( "fail query #{query} - #{$!}" )
795
+ @queries.delete(query)
796
+ raise
797
+ end
798
+ end
799
+ end
800
+
801
+ def query_stop(query)
802
+ @mutex.synchronize do
803
+ debug( "query #{query} - stop" )
804
+ @queries.delete(query)
805
+ end
806
+ end
807
+
808
+ def service_start(service, announce_answers = [])
809
+ @mutex.synchronize do
810
+ begin
811
+ @services << service
812
+
813
+ debug( "start service #{service.to_s}" )
814
+
815
+ if announce_answers.first
816
+ smsg = Message.new(0)
817
+ smsg.rd = 0
818
+ smsg.qr = 1
819
+ smsg.aa = 1
820
+ announce_answers.each do |a|
821
+ smsg.add_answer(*a)
822
+ end
823
+ send(smsg)
824
+ end
825
+
826
+ rescue
827
+ warn( "fail service #{service} - #{$!}" )
828
+ @queries.delete(service)
829
+ raise
830
+ end
831
+ end
832
+ end
833
+
834
+ def service_stop(service)
835
+ @mutex.synchronize do
836
+ debug( "service #{service} - stop" )
837
+ @services.delete(service)
838
+ end
839
+ end
840
+
841
+ end # Responder
842
+
843
+ # An mDNS query implementation.
844
+ module QueryImp
845
+ # This exists because I can't inherit Query to implement BackgroundQuery, I need
846
+ # to do something different with the block (yield it in a thread), and there doesn't seem to be
847
+ # a way to strip a block when calling super.
848
+ include Net::DNS
849
+
850
+ def subscribes_to?(an) # :nodoc:
851
+ if( name.to_s == '*' || name == an.name )
852
+ if( type == IN::ANY || type == an.type )
853
+ return true
854
+ end
855
+ end
856
+ false
857
+ end
858
+
859
+ def push(answers) # :nodoc:
860
+ @queue.push(answers) if answers.first
861
+ self
862
+ end
863
+
864
+ # The query +name+ from Query.new.
865
+ attr_reader :name
866
+ # The query +type+ from Query.new.
867
+ attr_reader :type
868
+
869
+ # Block, returning answers when available.
870
+ def pop
871
+ @queue.pop
872
+ end
873
+
874
+ # Loop forever, yielding answers as available.
875
+ def each # :yield: answers
876
+ loop do
877
+ yield pop
878
+ end
879
+ end
880
+
881
+ # Number of waiting answers.
882
+ def length
883
+ @queue.length
884
+ end
885
+
886
+ # A string describing this query.
887
+ def to_s
888
+ "q?#{name}/#{DNS.rrname(type)}"
889
+ end
890
+
891
+ def initialize_(name, type = IN::ANY)
892
+ @name = Name.create(name)
893
+ @type = type
894
+ @queue = Queue.new
895
+
896
+ qu = @name != "*" ? Question.new(@name, @type) : nil
897
+
898
+ Responder.instance.query_start(self, qu)
899
+ end
900
+
901
+ def stop
902
+ Responder.instance.query_stop(self)
903
+ self
904
+ end
905
+ end # Query
906
+
907
+ # An mDNS query.
908
+ class Query
909
+ include QueryImp
910
+
911
+ # Query for resource records of +type+ for the +name+. +type+ is one of
912
+ # the constants in Net::DNS::IN, such as A or ANY. +name+ is a DNS
913
+ # Name or String, see Name.create.
914
+ #
915
+ # +name+ can also be the wildcard "*". This will cause no queries to
916
+ # be multicast, but will return every answer seen by the responder.
917
+ #
918
+ # If the optional block is provided, self and any answers are yielded
919
+ # until an explicit break, return, or #stop is done.
920
+ def initialize(name, type = IN::ANY) # :yield: self, answers
921
+ initialize_(name, type)
922
+
923
+ if block_given?
924
+ self.each do |*args|
925
+ yield self, args
926
+ end
927
+ end
928
+ end
929
+
930
+ end # Query
931
+
932
+ # An mDNS query.
933
+ class BackgroundQuery
934
+ include QueryImp
935
+
936
+ # This is like Query.new, except the block is yielded in a background
937
+ # thread, and is not optional.
938
+ #
939
+ # In the thread, self and any answers are yielded until an explicit
940
+ # break, return, or #stop is done.
941
+ def initialize(name, type = IN::ANY, &proc) #:yield: self, answers
942
+ unless proc
943
+ raise ArgumentError, "require a proc to yield in background!"
944
+ end
945
+
946
+ initialize_(name, type)
947
+
948
+ @thread = Thread.new do
949
+ begin
950
+ loop do
951
+ answers = self.pop
952
+
953
+ proc.call(self, answers)
954
+ end
955
+ rescue
956
+ # This is noisy, but better than silent failure. If you don't want
957
+ # me to print your exceptions, make sure they don't get out of your
958
+ # block!
959
+ $stderr.puts "query #{self} yield raised #{$!}"
960
+ $!.backtrace.each do |e| $stderr.puts(e) end
961
+ ensure
962
+ Responder.instance.query_stop(self)
963
+ end
964
+ end
965
+ end
966
+
967
+ def stop
968
+ @thread.kill
969
+ self
970
+ end
971
+ end # BackgroundQuery
972
+
973
+ class Service
974
+ include Net::DNS
975
+
976
+ # Questions we can answer:
977
+ # @instance:
978
+ # name.type.domain -> SRV, TXT
979
+ # @type:
980
+ # type.domain -> PTR:name.type.domain
981
+ # @enum:
982
+ # _services._dns-sd._udp.<domain> -> PTR:type.domain
983
+ def answer_question(name, rtype, amsg)
984
+ case name
985
+ when @instance
986
+ # See [DNSSD:14.2]
987
+ case rtype.object_id
988
+ when IN::ANY.object_id
989
+ amsg.add_question(name, rtype)
990
+ amsg.add_answer(@instance, @srvttl, @rrsrv)
991
+ amsg.add_answer(@instance, @srvttl, @rrtxt)
992
+ amsg.add_additional(*@hostrr) if @hostrr
993
+
994
+ when IN::SRV.object_id
995
+ amsg.add_question(name, rtype)
996
+ amsg.add_answer(@instance, @srvttl, @rrsrv)
997
+ amsg.add_additional(*@hostrr) if @hostrr
998
+
999
+ when IN::TXT.object_id
1000
+ amsg.add_question(name, rtype)
1001
+ amsg.add_answer(@instance, @srvttl, @rrtxt)
1002
+ end
1003
+
1004
+ when @type
1005
+ # See [DNSSD:14.1]
1006
+ case rtype.object_id
1007
+ when IN::ANY.object_id, IN::PTR.object_id
1008
+ amsg.add_question(name, rtype)
1009
+ amsg.add_answer(@type, @ptrttl, @rrptr)
1010
+ amsg.add_additional(@instance, @srvttl, @rrsrv)
1011
+ amsg.add_additional(@instance, @srvttl, @rrtxt)
1012
+ amsg.add_additional(*@hostrr) if @hostrr
1013
+ end
1014
+
1015
+ when @enum
1016
+ case rtype.object_id
1017
+ when IN::ANY.object_id, IN::PTR.object_id
1018
+ amsg.add_question(name, rtype)
1019
+ amsg.add_answer(@type, @ptrttl, @rrenum)
1020
+ end
1021
+
1022
+ end
1023
+ end
1024
+
1025
+ # Default - 7 days
1026
+ def ttl=(secs)
1027
+ @ttl = secs.to_int
1028
+ end
1029
+ # Default - 0
1030
+ def priority=(secs)
1031
+ @priority = secs.to_int
1032
+ end
1033
+ # Default - 0
1034
+ def weight=(secs)
1035
+ @weight = secs.to_int
1036
+ end
1037
+ # Default - .local
1038
+ def domain=(domain)
1039
+ @domain = DNS::Name.create(domain.to_str)
1040
+ end
1041
+ # Set key/value pairs in a TXT record associated with SRV.
1042
+ def []=(key, value)
1043
+ @txt[key.to_str] = value.to_str
1044
+ end
1045
+
1046
+ def to_s
1047
+ "MDNS::Service: #{@instance} is #{@target}:#{@port}>"
1048
+ end
1049
+
1050
+ def inspect
1051
+ "#<#{self.class}: #{@instance} is #{@target}:#{@port}>"
1052
+ end
1053
+
1054
+ def initialize(name, type, port, txt = {}, target = nil, &proc)
1055
+ # TODO - escape special characters
1056
+ @name = DNS::Name.create(name.to_str)
1057
+ @type = DNS::Name.create(type.to_str)
1058
+ @domain = DNS::Name.create('local')
1059
+ @port = port.to_int
1060
+ if target
1061
+ @target = DNS::Name.create(target)
1062
+ @hostrr = nil
1063
+ else
1064
+ @target = Responder.instance.hostname
1065
+ @hostrr = Responder.instance.hostrr
1066
+ end
1067
+
1068
+ @txt = txt || {}
1069
+ @ttl = nil
1070
+ @priority = 0
1071
+ @weight = 0
1072
+
1073
+ proc.call(self) if proc
1074
+
1075
+ @srvttl = @ttl || 240
1076
+ @ptrttl = @ttl || 7200
1077
+
1078
+ @domain = Name.new(@domain.to_a, true)
1079
+ @type = @type + @domain
1080
+ @instance = @name + @type
1081
+ @enum = Name.create('_services._dns-sd._udp.') + @domain
1082
+
1083
+ # build the RRs
1084
+
1085
+ @rrenum = IN::PTR.new(@type)
1086
+
1087
+ @rrptr = IN::PTR.new(@instance)
1088
+
1089
+ @rrsrv = IN::SRV.new(@priority, @weight, @port, @target)
1090
+
1091
+ strings = @txt.map { |k,v| k + '=' + v }
1092
+
1093
+ @rrtxt = IN::TXT.new(*strings)
1094
+
1095
+ # class << self
1096
+ # undef_method 'ttl='
1097
+ # end
1098
+ # -or-
1099
+ # undef :ttl=
1100
+ #
1101
+ # TODO - all the others
1102
+
1103
+ start
1104
+ end
1105
+
1106
+ def start
1107
+ Responder.instance.service_start(self, [
1108
+ [@type, @ptrttl, @rrptr],
1109
+ [@instance, @srvttl, @rrsrv],
1110
+ [@instance, @srvttl, @rrtxt],
1111
+ @hostrr
1112
+ ].compact)
1113
+ self
1114
+ end
1115
+
1116
+ def stop
1117
+ Responder.instance.service_stop(self)
1118
+ self
1119
+ end
1120
+
1121
+ end
1122
+ end
1123
+
1124
+ end
1125
+ end
1126
+
1127
+ if $0 == __FILE__
1128
+
1129
+ include Net::DNS
1130
+
1131
+ $stdout.sync = true
1132
+ $stderr.sync = true
1133
+
1134
+ log = Logger.new(STDERR)
1135
+ log.level = Logger::DEBUG
1136
+
1137
+ MDNS::Responder.instance.log = log
1138
+
1139
+ require 'pp'
1140
+
1141
+ # I don't want lines of this report intertwingled.
1142
+ $print_mutex = Mutex.new
1143
+
1144
+ def print_answers(q,answers)
1145
+ $print_mutex.synchronize do
1146
+ puts "#{q} ->"
1147
+ answers.each do |an| puts " #{an}" end
1148
+ end
1149
+ end
1150
+
1151
+ questions = [
1152
+ [ IN::ANY, '*'],
1153
+ # [ IN::PTR, '_http._tcp.local.' ],
1154
+ # [ IN::SRV, 'Sam Roberts._http._tcp.local.' ],
1155
+ # [ IN::ANY, '_ftp._tcp.local.' ],
1156
+ # [ IN::ANY, '_daap._tcp.local.' ],
1157
+ # [ IN::A, 'ensemble.local.' ],
1158
+ # [ IN::ANY, 'ensemble.local.' ],
1159
+ # [ IN::PTR, '_services._dns-sd.udp.local.' ],
1160
+ nil
1161
+ ]
1162
+
1163
+ questions.each do |question|
1164
+ next unless question
1165
+
1166
+ type, name = question
1167
+ MDNS::BackgroundQuery.new(name, type) do |q, answers|
1168
+ print_answers(q, answers)
1169
+ end
1170
+ end
1171
+
1172
+ =begin
1173
+ q = MDNS::Query.new('ensemble.local.', IN::ANY)
1174
+ print_answers( q, q.pop )
1175
+ q.stop
1176
+
1177
+ svc = MDNS::Service.new('julie', '_example._tcp', 0xdead) do |s|
1178
+ s.ttl = 10
1179
+ end
1180
+ =end
1181
+
1182
+ Signal.trap('USR1') do
1183
+ PP.pp( MDNS::Responder.instance.cache, $stderr )
1184
+ end
1185
+
1186
+ sleep
1187
+
1188
+ end
1189
+