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.
@@ -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
+