toolmantim-zeroconf 0.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/README.rdoc +44 -0
- data/Rakefile +71 -0
- data/lib/dnssd.rb +137 -0
- data/lib/net/dns.rb +49 -0
- data/lib/net/dns/mdns-sd.rb +240 -0
- data/lib/net/dns/mdns.rb +1189 -0
- data/lib/net/dns/resolv-mdns.rb +230 -0
- data/lib/net/dns/resolv-replace.rb +66 -0
- data/lib/net/dns/resolv.rb +2012 -0
- data/lib/net/dns/resolvx.rb +219 -0
- data/lib/zeroconf.rb +15 -0
- data/lib/zeroconf/common.rb +0 -0
- data/lib/zeroconf/ext.rb +7 -0
- data/lib/zeroconf/pure.rb +13 -0
- data/lib/zeroconf/version.rb +3 -0
- data/originals/dnssd-0.6.0/COPYING +56 -0
- data/originals/dnssd-0.6.0/README +50 -0
- data/originals/net-mdns-0.4/COPYING +58 -0
- data/originals/net-mdns-0.4/README +21 -0
- data/originals/net-mdns-0.4/TODO +278 -0
- data/samples/exhttp.rb +50 -0
- data/samples/exhttpv1.rb +29 -0
- data/samples/exwebrick.rb +56 -0
- data/samples/mdns-watch.rb +132 -0
- data/samples/mdns.rb +238 -0
- data/samples/test_dns.rb +128 -0
- data/samples/v1demo.rb +167 -0
- data/samples/v1mdns.rb +111 -0
- data/test/stress/stress_register.rb +48 -0
- data/test/test_browse.rb +24 -0
- data/test/test_highlevel_api.rb +35 -0
- data/test/test_register.rb +32 -0
- data/test/test_resolve.rb +23 -0
- data/test/test_resolve_ichat.rb +56 -0
- data/test/test_textrecord.rb +58 -0
- metadata +93 -0
data/lib/net/dns/mdns.rb
ADDED
@@ -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
|
+
|