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.
- data/.gitignore +1 -0
- data/CHANGELOG.md +0 -0
- data/LICENSE +20 -0
- data/README.md +97 -0
- data/Rakefile +24 -0
- data/bin/wamupd +314 -0
- data/lib/wamupd/action.rb +40 -0
- data/lib/wamupd/avahi_model.rb +210 -0
- data/lib/wamupd/avahi_service.rb +112 -0
- data/lib/wamupd/avahi_service_file.rb +160 -0
- data/lib/wamupd/dns_avahi_controller.rb +230 -0
- data/lib/wamupd/dns_ip_controller.rb +95 -0
- data/lib/wamupd/dns_update.rb +154 -0
- data/lib/wamupd/lease_update.rb +29 -0
- data/lib/wamupd/main_settings.rb +202 -0
- data/lib/wamupd/signals.rb +52 -0
- data/lib/wamupd.rb +10 -0
- data/test/data/config.yaml +9 -0
- data/test/data/simple.service +16 -0
- data/test/data/ssh.service +12 -0
- data/test/test.rb +36 -0
- data/test/test_action.rb +28 -0
- data/test/test_avahi_model.rb +29 -0
- data/test/test_avahi_service.rb +50 -0
- data/test/test_avahi_service_file.rb +78 -0
- data/test/test_dns_avahi_controller.rb +69 -0
- data/test/test_dns_ip_controller.rb +30 -0
- data/test/test_main_settings.rb +43 -0
- data/test/test_signals.rb +58 -0
- data/wamupd.gemspec +29 -0
- metadata +165 -0
@@ -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
|
+
|