toolmantim-zeroconf 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
|