toolmantim-zeroconf 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,44 @@
1
+ = Zeroconf
2
+
3
+ Frankenstein marriage of net-mdns and dnssd.
4
+
5
+
6
+ == Installation
7
+
8
+ sudo gem sources -a http://gems.github.com
9
+ sudo gem install lachie-zeroconf
10
+
11
+ == Usage
12
+
13
+ Use zeroconf like dnssd. If you find a disparity between the pure and ext that trips you up, send me a patch!
14
+
15
+ Currently I'm thinking that the interface should be more dnssd-like, since I develop an a mac and get that for free :)
16
+
17
+ The basic discovery and publishing interfaces are similar. However the details, semantics (esp threading model, exceptions) and implementations are obviously quite different.
18
+
19
+ == Raison d'être
20
+
21
+ The interfaces of the C-based dnssd and pure ruby net-mdns are quite similar. However, there's no gem-based mechanism for switching between them based on availability.
22
+
23
+ This has lead to the forking of many of the *jour apps into dnssd and net-mdns based versions.
24
+
25
+ Zeroconf provides:
26
+
27
+ * a json-gem-style way of falling back to the pure-ruby implementation if the ext doesn't work.
28
+ * bridging discrepancies between the two extant libraries' implementations.
29
+ * perhaps a rubycocoa based implementation for osx.
30
+
31
+ Additionally, I'm hoping that this fork will breathe new life into the maintenance and development of the code; net-mdns 0.4.0 was released on 2006-05-30; dnssd 0.6.0 was released on 2004-10-07.
32
+
33
+ == Thanks
34
+
35
+ To the original authors of
36
+
37
+ * dnssd: Charlie Mills, Rich Kilmer, Chad Fowler and Stuart Cheshire.
38
+ * net-mdns: Sam Roberts
39
+
40
+ == TODO
41
+
42
+ * make the build failing warn but be non-fatal, so that the gem will install on systems without dnssd native libraries.
43
+ * make a windows gem.
44
+ * continue bridging discrepancies between dnssd and net-mdns interfaces
@@ -0,0 +1,71 @@
1
+ begin
2
+ require 'rake/gempackagetask'
3
+ rescue LoadError
4
+ end
5
+ require 'rake/clean'
6
+
7
+ require 'rbconfig'
8
+ include Config
9
+
10
+ require "./lib/zeroconf/version"
11
+
12
+ PKG = "zeroconf"
13
+ ON_WINDOWS = RUBY_PLATFORM =~ /mswin32/i
14
+ EXT_ROOT = "ext"
15
+ EXT_DL = "#{EXT_ROOT}/rdnssd.#{CONFIG['DLEXT']}"
16
+ EXT_SRC = FileList.new("#{EXT_ROOT}/*.c","#{EXT_ROOT}/*.h")
17
+ CLEAN.include 'doc', 'coverage',
18
+ FileList["ext/**/*.{so,bundle,#{CONFIG['DLEXT']},o,obj,pdb,lib,manifest,exp,def}"],
19
+ FileList["ext/**/Makefile"]
20
+
21
+ desc "compile the native extension"
22
+ task :compile => EXT_DL
23
+
24
+ file EXT_DL => EXT_SRC do
25
+ cd EXT_ROOT do
26
+ ruby 'extconf.rb'
27
+ sh 'make'
28
+ end
29
+ end
30
+
31
+
32
+ zeroconf_gemspec = Gem::Specification.new do |s|
33
+ s.name = PKG
34
+ s.version = Zeroconf::VERSION
35
+ s.platform = Gem::Platform::RUBY
36
+ s.has_rdoc = true
37
+ s.extra_rdoc_files = ["README.rdoc"]
38
+ s.summary = "Cross-platform zeroconf (aka bonjour™) library."
39
+ s.description = s.summary
40
+ s.authors = ["Lachie Cox"]
41
+ s.email = "lachiec@gmail.com"
42
+ s.homepage = "http://github.com/lachie/zeroconf"
43
+ s.require_path = "lib"
44
+ s.files = %w(README.rdoc Rakefile) + Dir.glob("{bin,lib,spec,originals,samples,test}/**/*")
45
+ end
46
+
47
+ Rake::GemPackageTask.new(zeroconf_gemspec) do |pkg|
48
+ pkg.gem_spec = zeroconf_gemspec
49
+ end
50
+
51
+ namespace :gem do
52
+ namespace :spec do
53
+ desc "Update #{PKG}.gemspec"
54
+ task :generate do
55
+ File.open("#{PKG}.gemspec", "w") do |f|
56
+ f.puts(zeroconf_gemspec.to_ruby)
57
+ end
58
+ end
59
+
60
+ desc "test spec in github cleanroom"
61
+ task :test => :generate do
62
+ require 'rubygems/specification'
63
+ data = File.read("#{PKG}.gemspec")
64
+ spec = nil
65
+ Thread.new { spec = eval("$SAFE = 3\n#{data}") }.join
66
+ puts spec
67
+ end
68
+ end
69
+ end
70
+
71
+ task :install => [ :compile, :package ]
@@ -0,0 +1,137 @@
1
+ require 'rdnssd'
2
+
3
+ =begin
4
+
5
+ module DNSSD
6
+ class MalformedDomainException < Exception; end
7
+ class MalformedPortException < Exception; end
8
+
9
+ def self.new_text_record(hash={})
10
+ TextRecord.new(hash)
11
+ end
12
+
13
+ class ServiceDescription
14
+
15
+ class Location
16
+ attr_accessor :name, :port, :iface
17
+ def initialize(port, name=nil, iface=0)
18
+ @name = name
19
+ @port = port
20
+ @iface = iface
21
+ end
22
+ end
23
+
24
+ def initialize(type, name, domain, location)
25
+ @type = type
26
+ @name = name
27
+ @domain = validate_domain(domain)
28
+ @location = validate_location(location)
29
+ end
30
+
31
+ def self.for_browse_notification(name, domain,
32
+
33
+ def stop
34
+ @registrar.stop
35
+ end
36
+
37
+ def validate_location(location)
38
+ unless(location.port.respond_to?(:to_int))
39
+ raise MalformedPortException.new("#{location.port} is not a valid port number")
40
+ end
41
+ location
42
+ end
43
+
44
+ def validate_domain(domain)
45
+ unless(domain.empty? || domain =~ /^[a-z_]+$/)
46
+ raise MalformedDomainException.new("#{domain} is not a valid domain name")
47
+ end
48
+ domain
49
+ end
50
+
51
+ def advertise_and_confirm
52
+ thread = Thread.current
53
+ @registrar = register(@name, @type, @domain, @location.port, TextRecord.new) do |service, name, type, domain|
54
+ @name = name
55
+ @type = type
56
+ @domain = domain
57
+ thread.wakeup
58
+ end
59
+ Thread.stop
60
+ end
61
+
62
+ def self.advertise_http(name, port=80, domain="", iface=0, &block)
63
+ self.advertise("_http._tcp", name, port, domain, iface, &block)
64
+ end
65
+
66
+ ##
67
+ # iface: Numerical interface (0 = all interfaces, This should be used for most applications)
68
+ #
69
+ def self.advertise(type, name, port, domain="", iface=0, &block)
70
+ service_description = ServiceDescription.new(type, name, domain, Location.new(port,nil,iface))
71
+ service_description.advertise_and_confirm
72
+ yield service_description if block_given?
73
+ service_description
74
+ end
75
+ end
76
+
77
+ class Browser
78
+
79
+ Context = Struct.new(:service, :name, :type, :domain, :operation, :interface)
80
+
81
+ class Context
82
+ def ==(other)
83
+ self.to_s == other.to_s
84
+ end
85
+
86
+ def to_s
87
+ "#{name}.#{type}.#{domain}"
88
+ end
89
+
90
+ def eql?(other)
91
+ self == other
92
+ end
93
+
94
+ def hash
95
+ to_s.hash
96
+ end
97
+ end
98
+
99
+ def on_change(&block)
100
+ @change_listener ||= []
101
+ @change_listeners << block
102
+ end
103
+
104
+ def on_add(&block)
105
+ @add_listeners || = []
106
+ @add_listeners << block
107
+ end
108
+
109
+ def on_remove(&block)
110
+ @remove_listeners || = []
111
+ @remove_listeners << block
112
+ end
113
+
114
+ def initialize(type, domain="")
115
+ @list = []
116
+ @browse_service = DNSSD::Protocol.browse(type, domain) do
117
+ |service, name, type, domain, operation, interface|
118
+ context = Context.new(service, name, type, domain, operation, interface)
119
+ puts "Name: #{name} Type: #{type} Domain: #{domain} Operation: #{operation} Interface: #{interface}"
120
+ end
121
+ end
122
+
123
+ def service_descriptions
124
+ @list.clone
125
+ end
126
+
127
+ def stop
128
+ @browse_service.stop
129
+ end
130
+
131
+ def self.for_http(domain="")
132
+ self.new("_http._tcp", domain)
133
+ end
134
+ end
135
+ end
136
+
137
+ =end
@@ -0,0 +1,49 @@
1
+ =begin
2
+ Copyright (C) 2005 Sam Roberts
3
+
4
+ This library is free software; you can redistribute it and/or modify it
5
+ under the same terms as the ruby language itself, see the file COPYING for
6
+ details.
7
+ =end
8
+
9
+ require 'net/dns/resolvx'
10
+
11
+ BasicSocket.do_not_reverse_lookup = true
12
+
13
+ module Net
14
+ # DNS exposes some of Resolv::DNS from resolv.rb to make them easier to use
15
+ # outside of the context of the Resolv class and it's DNS resolver - such as
16
+ # in MDNS. In particular, Net::DNS can be included so that full names to DNS
17
+ # classes in Resolv::DNS can be imported into your namespace.
18
+ module DNS
19
+
20
+ Message = Resolv::DNS::Message
21
+ Name = Resolv::DNS::Name
22
+ DecodeError = Resolv::DNS::DecodeError
23
+
24
+ module IN
25
+ A = Resolv::DNS::Resource::IN::A
26
+ AAAA = Resolv::DNS::Resource::IN::AAAA
27
+ ANY = Resolv::DNS::Resource::IN::ANY
28
+ CNAME = Resolv::DNS::Resource::IN::CNAME
29
+ HINFO = Resolv::DNS::Resource::IN::HINFO
30
+ MINFO = Resolv::DNS::Resource::IN::MINFO
31
+ MX = Resolv::DNS::Resource::IN::MX
32
+ NS = Resolv::DNS::Resource::IN::NS
33
+ PTR = Resolv::DNS::Resource::IN::PTR
34
+ SOA = Resolv::DNS::Resource::IN::SOA
35
+ SRV = Resolv::DNS::Resource::IN::SRV
36
+ TXT = Resolv::DNS::Resource::IN::TXT
37
+ WKS = Resolv::DNS::Resource::IN::WKS
38
+ end
39
+
40
+ # Returns the resource record name of +rr+ as a short string ("IN::A",
41
+ # ...).
42
+ def self.rrname(rr)
43
+ rr = rr.class unless rr.class == Class
44
+ rr = rr.to_s.sub(/.*Resource::/, '')
45
+ rr = rr.to_s.sub(/.*DNS::/, '')
46
+ end
47
+ end
48
+ end
49
+
@@ -0,0 +1,240 @@
1
+ =begin
2
+ Copyright (C) 2005 Sam Roberts
3
+
4
+ This library is free software; you can redistribute it and/or modify it
5
+ under the same terms as the ruby language itself, see the file COPYING for
6
+ details.
7
+ =end
8
+
9
+ require 'net/dns/mdns'
10
+
11
+ module Net
12
+ module DNS
13
+
14
+ # = DNS-SD over mDNS
15
+ #
16
+ # An implementation of DNS Service-Discovery (DNS-SD) using Net::DNS::MDNS.
17
+ #
18
+ # DNS-SD is described in draft-cheshire-dnsext-dns-sd.txt, see
19
+ # http://www.dns-sd.org for more information. It is most often seen as part
20
+ # of Apple's OS X, but is widely useful.
21
+ #
22
+ # These APIs accept and return a set of arguments which are documented once,
23
+ # here, for convenience.
24
+ #
25
+ # - type: DNS-SD classifies services into types using a naming convention.
26
+ # That convention is <_service>.<_protocol>. The underscores ("_") serve
27
+ # to differentiate from normal DNS names. Protocol is always one of
28
+ # "_tcp" or "_udp". The service is a short name, see the list at
29
+ # http://www.dns-sd.org/ServiceTypes.html. A common service is "http", the type
30
+ # of which would be "_http._tcp".
31
+ #
32
+ # - domain: Services operate in a domain, theoretically. In current practice,
33
+ # that domain is always "local".
34
+ #
35
+ # - name: Service lookup with #browse results in a name of a service of that
36
+ # type. That name is associated with a target (a host name), port,
37
+ # priority, and weight, as well as series of key to value mappings,
38
+ # specific to the service. In practice, priority and weight are widely
39
+ # ignored.
40
+ #
41
+ # - fullname: The concatention of the service name (optionally), type, and
42
+ # domain results in a single dot-seperated domain name - the "fullname".
43
+ # See Util.parse_name for more information about the format.
44
+ #
45
+ # - text_record: Service information in the form of key/value pairs.
46
+ # See Util.parse_strings for more information about the format.
47
+ #
48
+ # - flags: should return flags, similar to DNSSD, but for now we just return the
49
+ # TTL of the DNS message. A TTL of zero means a deregistration of the record.
50
+ #
51
+ # Services are advertised and resolved over specific network interfaces.
52
+ # Currently, Net::DNS::MDNS supports only a single default interface, and
53
+ # the interface will always be +nil+.
54
+ module MDNSSD
55
+
56
+ # A reply yielded by #browse, see MDNSSD for a description of the attributes.
57
+ class BrowseReply
58
+ attr_reader :interface, :fullname, :name, :type, :domain, :flags
59
+ def initialize(an) # :nodoc:
60
+ @interface = nil
61
+ @fullname = an.name.to_s
62
+ @domain, @type, @name = MDNSSD::Util.parse_name(an.data.name)
63
+ @flags = an.ttl
64
+ end
65
+ end
66
+
67
+ # Lookup a service by +type+ and +domain+.
68
+ #
69
+ # Yields a BrowseReply as services are found, in a background thread, not
70
+ # the caller's thread!
71
+ #
72
+ # Returns a MDNS::BackgroundQuery, call MDNS::BackgroundQuery#stop when
73
+ # you have found all the replies you are interested in.
74
+ def self.browse(type, domain = '.local', *ignored) # :yield: BrowseReply
75
+ dnsname = DNS::Name.create(type)
76
+ dnsname << DNS::Name.create(domain)
77
+ dnsname.absolute = true
78
+
79
+ q = MDNS::BackgroundQuery.new(dnsname, IN::PTR) do |q, answers|
80
+ answers.each do |an|
81
+ yield BrowseReply.new( an )
82
+ end
83
+ end
84
+ q
85
+ end
86
+
87
+ # A reply yielded by #resolve, see MDNSSD for a description of the attributes.
88
+ class ResolveReply
89
+ attr_reader :interface, :fullname, :name, :type, :domain, :target, :port, :priority, :weight, :text_record, :flags
90
+ def initialize(ansrv, antxt) # :nodoc:
91
+ @interface = nil
92
+ @fullname = ansrv.name.to_s
93
+ @domain, @type, @name = MDNSSD::Util.parse_name(ansrv.name)
94
+ @target = ansrv.data.target.to_s
95
+ @port = ansrv.data.port
96
+ @priority = ansrv.data.priority
97
+ @weight = ansrv.data.weight
98
+ @text_record = MDNSSD::Util.parse_strings(antxt.data.strings)
99
+ @flags = ansrv.ttl
100
+ end
101
+ end
102
+
103
+ # Resolve a service instance by +name+, +type+ and +domain+.
104
+ #
105
+ # Yields a ResolveReply as service instances are found, in a background
106
+ # thread, not the caller's thread!
107
+ #
108
+ # Returns a MDNS::BackgroundQuery, call MDNS::BackgroundQuery#stop when
109
+ # you have found all the replies you are interested in.
110
+ def self.resolve(name, type, domain = '.local', *ignored) # :yield: ResolveReply
111
+ dnsname = DNS::Name.create(name)
112
+ dnsname << DNS::Name.create(type)
113
+ dnsname << DNS::Name.create(domain)
114
+ dnsname.absolute = true
115
+
116
+ rrs = {}
117
+
118
+ q = MDNS::BackgroundQuery.new(dnsname, IN::ANY) do |q, answers|
119
+ _rrs = {}
120
+ answers.each do |an|
121
+ if an.name == dnsname
122
+ _rrs[an.type] = an
123
+ end
124
+ end
125
+ # We queried for ANY, but don't yield unless we got a SRV or TXT.
126
+ if( _rrs[IN::SRV] || _rrs[IN::TXT] )
127
+ rrs.update _rrs
128
+
129
+ ansrv, antxt = rrs[IN::SRV], rrs[IN::TXT]
130
+
131
+ # puts "ansrv->#{ansrv}"
132
+ # puts "antxt->#{antxt}"
133
+
134
+ # Even though we got an SRV or TXT, we can't yield until we have both.
135
+ if ansrv && antxt
136
+ yield ResolveReply.new( ansrv, antxt )
137
+ end
138
+ end
139
+ end
140
+ q
141
+ end
142
+
143
+ # A reply yielded by #register, see MDNSSD for a description of the attributes.
144
+ class RegisterReply
145
+ attr_reader :interface, :fullname, :name, :type, :domain
146
+ def initialize(name, type, domain)
147
+ @interface = nil
148
+ @fullname = (DNS::Name.create(name) << type << domain).to_s
149
+ @name, @type, @domain = name, type, domain
150
+ end
151
+ end
152
+
153
+ # Register a service instance on the local host.
154
+ #
155
+ # +txt+ is a Hash of String keys to String values.
156
+ #
157
+ # Because the service +name+ may already be in use on the network, a
158
+ # different name may be registered than that requested. Because of this,
159
+ # if a block is supplied, a RegisterReply will be yielded so that the
160
+ # actual service name registered may be seen.
161
+ #
162
+ # Returns a MDNS::Service, call MDNS::Service#stop when you no longer
163
+ # want to advertise the service.
164
+ #
165
+ # NOTE - The service +name+ should be unique on the network, MDNSSD
166
+ # doesn't currently attempt to ensure this. This will be fixed in
167
+ # an upcoming release.
168
+ def self.register(name, type, domain, port, txt = {}, *ignored) # :yields: RegisterReply
169
+ dnsname = DNS::Name.create(name)
170
+ dnsname << DNS::Name.create(type)
171
+ dnsname << DNS::Name.create(domain)
172
+ dnsname.absolute = true
173
+
174
+ s = MDNS::Service.new(name, type, port, txt) do |s|
175
+ s.domain = domain
176
+ end
177
+
178
+ yield RegisterReply.new(name, type, domain) if block_given?
179
+
180
+ s
181
+ end
182
+
183
+ # Utility routines not for general use.
184
+ module Util
185
+ # Decode a DNS-SD domain name. The format is:
186
+ # [<instance>.]<_service>.<_protocol>.<domain>
187
+ #
188
+ # Examples are:
189
+ # _http._tcp.local
190
+ # guest._http._tcp.local
191
+ # Ensemble Musique._daap._tcp.local
192
+ #
193
+ # The <_service>.<_protocol> combined is the <type>.
194
+ #
195
+ # Return either:
196
+ # [ <domain>, <type> ]
197
+ # or
198
+ # [ <domain>, <type>, <instance>]
199
+ #
200
+ # Because of the order of the return values, it can be called like:
201
+ # domain, type = MDNSSD::Util.parse_name(fullname)
202
+ # or
203
+ # domain, type, name = MDNSSD::Util.parse_name(fullname)
204
+ # If there is no name component to fullname, name will be nil.
205
+ def self.parse_name(dnsname)
206
+ domain, t1, t0, name = dnsname.to_a.reverse.map {|n| n.to_s}
207
+ [ domain, t0 + '.' + t1, name].compact
208
+ end
209
+
210
+ # Decode TXT record strings, an array of String.
211
+ #
212
+ # DNS-SD defines formatting conventions for them:
213
+ # - Keys must be at least one char in range (0x20-0x7E), excluding '='
214
+ # (0x3D), and they must be matched case-insensitively.
215
+ # - There may be no '=', in which case value is nil.
216
+ # - There may be an '=' with no value, in which case value is empty string, "".
217
+ # - Anything following the '=' is a value, it is not case sensitive, can be binary,
218
+ # and can include whitespace.
219
+ # - Discard all keys but the first.
220
+ # - Discard a string that aren't formatting accorded to these rules.
221
+ def self.parse_strings(strings)
222
+ h = {}
223
+
224
+ strings.each do |kv|
225
+ if kv.match( /^([\x20-\x3c\x3f-\x7e]+)(?:=(.*))?$/ )
226
+ key = $1.downcase
227
+ value = $2
228
+ next if h.has_key? key
229
+ h[key] = value
230
+ end
231
+ end
232
+
233
+ h
234
+ end
235
+ end
236
+
237
+ end
238
+ end
239
+ end
240
+