wamupd 1.1.1

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,210 @@
1
+ #!/usr/bin/ruby
2
+ # Copyright (C) 2009-2010 James Brown <roguelazer@roguelazer.com>.
3
+ #
4
+ # This file is part of wamupd.
5
+ #
6
+ # wamupd is free software: you can redistribute it and/or modify
7
+ # it under the terms of the GNU General Public License as published by
8
+ # the Free Software Foundation, either version 3 of the License, or
9
+ # (at your option) any later version.
10
+ #
11
+ # wamupd is distributed in the hope that it will be useful,
12
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
13
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14
+ # GNU General Public License for more details.
15
+ #
16
+ # You should have received a copy of the GNU General Public License
17
+ # along with wamupd. If not, see <http://www.gnu.org/licenses/>.
18
+
19
+ require "wamupd/avahi_service"
20
+ require "wamupd/signals"
21
+
22
+ require "dbus"
23
+ require "set"
24
+
25
+ # Wamupd is a module that is used to namespace all of the wamupd code.
26
+ module Wamupd
27
+ # Model for Avahi's registered services. Uses D-BUS to talk to the
28
+ # system Avahi daemon and pull down service information. It's kind of a
29
+ # hack because Avahi has a really (really!) horrible D-BUS interface.
30
+ #
31
+ # ==Signals
32
+ #
33
+ # [:added]
34
+ # A service was added from the system Avahi. Includes the service as
35
+ # a parameter.
36
+ #
37
+ # [:removed]
38
+ # A service was removed from the system Avahi. Includes the service
39
+ # as a paramter.
40
+ #
41
+ # [:quit]
42
+ # The model is quitting.
43
+ class AvahiModel
44
+ include Signals
45
+
46
+ # Constructor. Boring.
47
+ def initialize
48
+ @bus = DBus::SystemBus.instance
49
+ @service = @bus.service("org.freedesktop.Avahi")
50
+ @service.introspect
51
+ @server = @service.object("/")
52
+ @server.introspect
53
+ @server = @server["org.freedesktop.Avahi.Server"]
54
+
55
+ @service_types = Set.new
56
+ @count = 0
57
+
58
+ @known_services = Hash.new
59
+ @handlers = Hash.new
60
+ end
61
+
62
+ # Actually starts listening.
63
+ def start_listen
64
+ stb = @server.ServiceTypeBrowserNew(-1,-1,"",0)
65
+ mr = DBus::MatchRule.new
66
+ mr.type = "signal"
67
+ mr.interface = "org.freedesktop.Avahi.ServiceTypeBrowser"
68
+ mr.path = stb.first
69
+ mr.member = "ItemNew"
70
+ @bus.add_match(mr) do |msg, first_param|
71
+ type = msg.params[2]
72
+ if (not @service_types.member?(type))
73
+ @service_types.add(msg.params[2])
74
+ add_type_listener(msg.params[2])
75
+ end
76
+ end
77
+ end
78
+
79
+ # Add a D-BUS listener for an individual service type
80
+ def add_type_listener(service_type)
81
+ @server.ServiceBrowserNew(-1,-1,service_type,"",0) { |sb, first_param|
82
+ sb=sb.params.first
83
+ mr = DBus::MatchRule.new
84
+ mr.type = "signal"
85
+ mr.interface = "org.freedesktop.Avahi.ServiceBrowser"
86
+ mr.path = sb
87
+ @bus.add_match(mr) do |item_msg, first_param|
88
+ if (item_msg.member == "ItemNew")
89
+ # From avahi-common/defs.h:
90
+ #
91
+ # typedef enum {
92
+ # AVAHI_LOOKUP_RESULT_CACHED = 1, This response originates from the cache
93
+ # AVAHI_LOOKUP_RESULT_WIDE_AREA = 2, This response originates from wide area DNS
94
+ # AVAHI_LOOKUP_RESULT_MULTICAST = 4, This response originates from multicast DNS
95
+ # AVAHI_LOOKUP_RESULT_LOCAL = 8, This record/service resides on and was announced by the local host. Only available in service and record browsers and only on AVAHI_BROWSER_NEW.
96
+ # AVAHI_LOOKUP_RESULT_OUR_OWN = 16, This service belongs to the same local client as the browser object. Only available in avahi-client, and only for service browsers and only on AVAHI_BROWSER_NEW.
97
+ # AVAHI_LOOKUP_RESULT_STATIC = 32 The returned data has been defined statically by some configuration option *
98
+ # } AvahiLookupResultFlags;
99
+ #
100
+ # AND the flags result (param 5) to limit ourselves to
101
+ # local services
102
+ if ((item_msg.params[5] & 8) != 0)
103
+ # Then use an async serviceresolver to look up the
104
+ # service. Has to be async, because otherwise
105
+ # there's a recursive call that causes us to run out
106
+ # of stack space. Woo broken libraries.
107
+ @server.ServiceResolverNew(-1,-1,item_msg.params[2],
108
+ item_msg.params[3],
109
+ item_msg.params[4],
110
+ -1,0) do |srb,fp|
111
+ srb = srb.params.first
112
+ mrs = DBus::MatchRule.new
113
+ mrs.type = "signal"
114
+ mrs.interface = "org.freedesktop.Avahi.ServiceResolver"
115
+ mrs.path = srb
116
+ @bus.add_match(mrs) do |msg,fp|
117
+ if (msg.member == "Found")
118
+ name = msg.params[2]
119
+ type = msg.params[3]
120
+ host = msg.params[5]
121
+ address = msg.params[7]
122
+ port = msg.params[8]
123
+ txt = Wamupd::AvahiModel.pack_txt_param(msg.params[9])
124
+ add_service_record(name, type, host, port, txt)
125
+ end
126
+ end
127
+ end
128
+ end
129
+ elsif (item_msg.member == "ItemRemove")
130
+ if ((item_msg.params[5] & 8) != 0)
131
+ name = msg.params[2]
132
+ type = msg.params[3]
133
+ host = msg.params[5]
134
+ address = msg.params[7]
135
+ port = msg.params[8]
136
+ txt = Wamupd::AvahiModel.pack_txt_param(msg.params[9])
137
+ remove_service_record(name, type, host, port, txt)
138
+ end
139
+ end
140
+ end
141
+ }
142
+ end
143
+
144
+ # Pack an array of TXT parameters into a single TXT record
145
+ # appropriately
146
+ #
147
+ # Note: The actual TXT record should consist of name-value pairs,
148
+ # with each pair delimited by its length.
149
+ #
150
+ # As of 2010-04-21, Dnsruby internally converts double-quoted,
151
+ # space-separted strings into the appropriate format. Most
152
+ # annoyingly, if you provide it input already in the right format,
153
+ # it sticks a completely unnecessary byte on the front and screws it
154
+ # up.
155
+ #
156
+ # Aren't you happy you know that now?
157
+ def self.pack_txt_param(strs)
158
+ val = ""
159
+ strs.each { |c|
160
+ # Dnsruby uses multi-byte lengths if any of your records are
161
+ # over 255 characters, though. This makes mDNSResponder get
162
+ # amusingly confused.
163
+ if (c.length > 255)
164
+ next
165
+ end
166
+ val += "\"#{c.pack("c*")}\" "
167
+ }
168
+ return val.chop
169
+ end
170
+
171
+ # Construct an AvahiService from the given parameters
172
+ def add_service_record(name, type, host, port, txt)
173
+ # Replace the .local that Avahi sticks at the end of the host (why
174
+ # doesn't it just use the domain field? who knows?)
175
+ host.sub!(/\.local$/, "")
176
+ a = AvahiService.new(name, {:type=>type, :hostname=>host, :port=>port, :txt=>txt})
177
+ @known_services[a.identifier] = a
178
+ signal(:added, a)
179
+ end
180
+
181
+ # Remove an AvahiService using the given parameters.
182
+ def remove_service_record(name, type, host, port, txt)
183
+ a = AvahiService.new(name, {:type=>type, :hostname=>host, :port=>port, :txt=>txt})
184
+ if (@known_services.has_key?(a.identifier))
185
+ @known_services.delete(a.identifier)
186
+ end
187
+ signal(:removed, a)
188
+ end
189
+
190
+ # Exit the listener
191
+ def exit
192
+ signal(:quit)
193
+ end
194
+
195
+ # Run the listener. This function doesn't return until
196
+ # exit is called.
197
+ def run
198
+ start_listen
199
+
200
+ @main_loop = DBus::Main.new
201
+ self.on(:quit) {
202
+ @main_loop.quit
203
+ }
204
+ @main_loop << @bus
205
+ @main_loop.run
206
+ end
207
+
208
+ private :add_type_listener
209
+ end
210
+ end
@@ -0,0 +1,112 @@
1
+ # Copyright (C) 2009-2010 James Brown <roguelazer@roguelazer.com>.
2
+ #
3
+ # This file is part of wamupd.
4
+ #
5
+ # wamupd is free software: you can redistribute it and/or modify
6
+ # it under the terms of the GNU General Public License as published by
7
+ # the Free Software Foundation, either version 3 of the License, or
8
+ # (at your option) any later version.
9
+ #
10
+ # wamupd is distributed in the hope that it will be useful,
11
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
12
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
+ # GNU General Public License for more details.
14
+ #
15
+ # You should have received a copy of the GNU General Public License
16
+ # along with wamupd. If not, see <http://www.gnu.org/licenses/>.
17
+
18
+ # Wamupd is a module that is used to namespace all of the wamupd code.
19
+ module Wamupd
20
+ # A single <service> entry from a service record. A given service file
21
+ # (representated by an AvahiServiceFile) may contain many AvahiServices.
22
+ class AvahiService
23
+ attr_reader :type
24
+ attr_reader :subtype
25
+ attr_reader :hostname
26
+ attr_reader :port
27
+ attr_reader :domainname
28
+ attr_reader :name
29
+
30
+ # Get the subtype as Apple displays it
31
+ def subtype_display
32
+ "#{@type},#{@subtype}"
33
+ end
34
+
35
+ # The type and name in this zone. Name of the SRV and TXT records
36
+ def type_in_zone_with_name
37
+ sa = MainSettings.instance
38
+ return sa.hostname + "." + @type + "."+ sa.zone
39
+ end
40
+
41
+ # The full type in this zone. Goes in the PTR
42
+ def type_in_zone
43
+ sa = MainSettings.instance
44
+ return @type + "." + sa.zone
45
+ end
46
+
47
+ # A key that can be used to identify this service.
48
+ # Is the subtype-type followed by a - followed by the port
49
+ def identifier
50
+ retval = ""
51
+ if (@subtype)
52
+ retval += @subtype
53
+ retval += "."
54
+ end
55
+ if (@type)
56
+ retval += @type
57
+ retval += "-"
58
+ end
59
+ retval += @port.to_s
60
+ return retval
61
+ end
62
+
63
+ # The target of this service
64
+ def target
65
+ t = ""
66
+ sa = MainSettings.instance
67
+ if (@hostname.nil?)
68
+ t += sa.hostname
69
+ else
70
+ t += @hostname
71
+ end
72
+ t += "."
73
+ if (@domainname.nil?)
74
+ t += sa.zone
75
+ else
76
+ t += @domainname
77
+ end
78
+ end
79
+
80
+ # TXT record
81
+ def txt
82
+ return (@txt.nil? || @txt == "") ? false : @txt
83
+ end
84
+
85
+ # Initialize
86
+ #
87
+ # Argument:
88
+ # Either an XML node or a hash with some useful subset of the
89
+ # parameters :type, :subtype, :hostname, :port, :txt, and
90
+ # :domainname
91
+ def initialize(name, param)
92
+ @name = name
93
+ mapping = {:type=>:@type,
94
+ :subtype=>:@subtype,
95
+ :hostname=>:@hostname,
96
+ :port=>:@port,
97
+ :txt=>:@txt,
98
+ :domainname=>:@domainname
99
+ }
100
+ mapping.each {|k,v|
101
+ if (param.has_key?(k))
102
+ self.instance_variable_set(v, param[k])
103
+ end
104
+ }
105
+ end
106
+
107
+ # String coercer
108
+ def to_s
109
+ "<AvahiService name='#{@name}' type=#{@type} txt=#{self.txt}>"
110
+ end
111
+ end
112
+ end
@@ -0,0 +1,160 @@
1
+ # Copyright (C) 2009-2010 James Brown <roguelazer@roguelazer.com>.
2
+ #
3
+ # This file is part of wamupd.
4
+ #
5
+ # wamupd is free software: you can redistribute it and/or modify
6
+ # it under the terms of the GNU General Public License as published by
7
+ # the Free Software Foundation, either version 3 of the License, or
8
+ # (at your option) any later version.
9
+ #
10
+ # wamupd is distributed in the hope that it will be useful,
11
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
12
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
+ # GNU General Public License for more details.
14
+ #
15
+ # You should have received a copy of the GNU General Public License
16
+ # along with wamupd. If not, see <http://www.gnu.org/licenses/>.
17
+ # Copyright (C) 2009-2010 James Brown <roguelazer@roguelazer.com>.
18
+ #
19
+ # This file is part of wamupd.
20
+ #
21
+ # wamupd is free software: you can redistribute it and/or modify
22
+ # it under the terms of the GNU General Public License as published by
23
+ # the Free Software Foundation, either version 3 of the License, or
24
+ # (at your option) any later version.
25
+ #
26
+ # wamupd is distributed in the hope that it will be useful,
27
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
28
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
29
+ # GNU General Public License for more details.
30
+ #
31
+ # You should have received a copy of the GNU General Public License
32
+ # along with wamupd. If not, see <http://www.gnu.org/licenses/>.
33
+
34
+ require "xml"
35
+
36
+ require "wamupd/avahi_service"
37
+
38
+ # Wamupd is a module that is used to namespace all of the wamupd code.
39
+ module Wamupd
40
+ # AvahiServiceFile is a class to abstract Avahi's .service files. It is
41
+ # capable of loading, parsing, and validating these files into a group
42
+ # of AvahiService objects.
43
+ class AvahiServiceFile
44
+ include Enumerable
45
+
46
+ SERVICE_DTD_PATH = "/usr/local/share/avahi/avahi-service.dtd";
47
+
48
+ # Get the name of this service
49
+ def name
50
+ if (@replace_wildcards)
51
+ return AvahiServiceFile.replace_wildcards(@name)
52
+ end
53
+ return @name
54
+ end
55
+
56
+ # Access each service in turn
57
+ def each(&block)
58
+ @services.each { |c|
59
+ yield c
60
+ }
61
+ end
62
+
63
+ # The number of services in this definition
64
+ def size
65
+ return @services.count
66
+ end
67
+
68
+ # The first service defined
69
+ def first
70
+ if (self.size > 0)
71
+ return @services[0]
72
+ else
73
+ return nil
74
+ end
75
+ end
76
+
77
+ # Replace the wildcards in a string using the Avahi
78
+ # rules
79
+ def self.replace_wildcards(string)
80
+ return string.sub("%h", MainSettings.instance.hostname)
81
+ end
82
+
83
+ # Has this service file entry been validated against the DTD?
84
+ def valid?
85
+ return @valid
86
+ end
87
+
88
+ # Initialize the service from a service file
89
+ def initialize(filename)
90
+ @valid = false
91
+ @services = []
92
+ @replace_wildcards = false
93
+ load_from_file(filename)
94
+ end
95
+
96
+ # Load this AvahiService entry from a .service defintion file
97
+ #
98
+ # Arguments:
99
+ # filename:: The file to load
100
+ def load_from_file(filename)
101
+ d = XML::Document.file(filename)
102
+ if (not File.exists?(SERVICE_DTD_PATH))
103
+ $stderr.puts "Could not find service DTD at #{SERVICE_DTD_PATH}"
104
+ exit(1)
105
+ end
106
+ dtd = File.open(SERVICE_DTD_PATH, "r") { |f|
107
+ lines = f.readlines().join("")
108
+ }
109
+ validator = XML::Dtd.new(dtd)
110
+ @valid = d.validate(validator)
111
+ if (not @valid)
112
+ $stderr.puts "Service file #{filename} failed validation"
113
+ exit(1)
114
+ end
115
+ sg = d.root
116
+ sg.children.each { |c|
117
+ case c.name
118
+ when "name"
119
+ @name = c.content
120
+ if ((not c["replace-wildcards"].nil?) and (c["replace-wildcards"] == "yes"))
121
+ @replace_wildcards = true
122
+ end
123
+ when "service"
124
+ node = c
125
+ params = {}
126
+ node.children.each { |c|
127
+ case c.name
128
+ when "type"
129
+ params[:type] = c.content
130
+ when "subtype"
131
+ params[:subtype] = c.content
132
+ when "host-name"
133
+ params[:hostname] = c.content
134
+ when "port"
135
+ params[:port] = c.content.to_i
136
+ when "txt-record"
137
+ params[:txt] = c.content
138
+ when "domain-name"
139
+ params[:domainname] = c.content
140
+ end
141
+ }
142
+ @services.push(AvahiService.new(self.name, params))
143
+ end
144
+ }
145
+ end
146
+
147
+ # Load all of the service definitions in a directory
148
+ #
149
+ # Returns:
150
+ # an array of AvahiService objects
151
+ def self.load_from_directory(dir)
152
+ retval = []
153
+ Dir.glob(File.join(dir, "*.service")).each { |f|
154
+ retval.push(AvahiServiceFile.new(f))
155
+ }
156
+ return retval
157
+ end
158
+ end
159
+ end
160
+