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,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
+