111
98
class AvahiService(object):
112
"""An Avahi (Zeroconf) service.
115
100
interface: integer; avahi.IF_UNSPEC or an interface index.
116
101
Used to optionally bind to the specified interface.
117
name: string; Example: u'Mandos'
118
type: string; Example: u'_mandos._tcp'.
119
See <http://www.dns-sd.org/ServiceTypes.html>
120
port: integer; what port to announce
121
TXT: list of strings; TXT record for the service
122
domain: string; Domain to publish on, default to .local if empty.
123
host: string; Host to publish records for, default is localhost
124
max_renames: integer; maximum number of renames
125
rename_count: integer; counter so we only rename after collisions
126
a sensible number of times
127
group: D-Bus Entry Group
129
bus: dbus.SystemBus()
102
name = string; Example: "Mandos"
103
type = string; Example: "_mandos._tcp".
104
See <http://www.dns-sd.org/ServiceTypes.html>
105
port = integer; what port to announce
106
TXT = list of strings; TXT record for the service
107
domain = string; Domain to publish on, default to .local if empty.
108
host = string; Host to publish records for, default to localhost
110
max_renames = integer; maximum number of renames
111
rename_count = integer; counter so we only rename after collisions
112
a sensible number of times
131
114
def __init__(self, interface = avahi.IF_UNSPEC, name = None,
132
servicetype = None, port = None, TXT = None,
133
domain = u"", host = u"", max_renames = 32768,
134
protocol = avahi.PROTO_UNSPEC, bus = None):
115
type = None, port = None, TXT = None, domain = "",
116
host = "", max_renames = 12):
117
"""An Avahi (Zeroconf) service. """
135
118
self.interface = interface
137
self.type = servicetype
139
self.TXT = TXT if TXT is not None else []
140
126
self.domain = domain
142
128
self.rename_count = 0
143
self.max_renames = max_renames
144
self.protocol = protocol
145
self.group = None # our entry group
148
129
def rename(self):
149
130
"""Derived from the Avahi example code"""
150
131
if self.rename_count >= self.max_renames:
151
logger.critical(u"No suitable Zeroconf service name found"
152
u" after %i retries, exiting.",
154
raise AvahiServiceError(u"Too many renames")
155
self.name = self.server.GetAlternativeServiceName(self.name)
156
logger.info(u"Changing Zeroconf service name to %r ...",
158
syslogger.setFormatter(logging.Formatter
159
(u'Mandos (%s) [%%(process)d]:'
160
u' %%(levelname)s: %%(message)s'
132
logger.critical(u"No suitable service name found after %i"
133
u" retries, exiting.", rename_count)
134
raise AvahiServiceError("Too many renames")
135
name = server.GetAlternativeServiceName(name)
136
logger.notice(u"Changing name to %r ...", name)
164
139
self.rename_count += 1
165
140
def remove(self):
166
141
"""Derived from the Avahi example code"""
167
if self.group is not None:
142
if group is not None:
170
145
"""Derived from the Avahi example code"""
171
if self.group is None:
172
self.group = dbus.Interface(
173
self.bus.get_object(avahi.DBUS_NAME,
174
self.server.EntryGroupNew()),
175
avahi.DBUS_INTERFACE_ENTRY_GROUP)
176
self.group.connect_to_signal('StateChanged',
177
self.entry_group_state_changed)
178
logger.debug(u"Adding Zeroconf service '%s' of type '%s' ...",
179
self.name, self.type)
180
self.group.AddService(
183
dbus.UInt32(0), # flags
184
self.name, self.type,
185
self.domain, self.host,
186
dbus.UInt16(self.port),
187
avahi.string_array_to_txt_array(self.TXT))
189
def entry_group_state_changed(self, state, error):
190
"""Derived from the Avahi example code"""
191
logger.debug(u"Avahi state change: %i", state)
193
if state == avahi.ENTRY_GROUP_ESTABLISHED:
194
logger.debug(u"Zeroconf service established.")
195
elif state == avahi.ENTRY_GROUP_COLLISION:
196
logger.warning(u"Zeroconf service name collision.")
198
elif state == avahi.ENTRY_GROUP_FAILURE:
199
logger.critical(u"Avahi: Error in group state changed %s",
201
raise AvahiGroupError(u"State changed: %s"
204
"""Derived from the Avahi example code"""
205
if self.group is not None:
208
def server_state_changed(self, state):
209
"""Derived from the Avahi example code"""
210
if state == avahi.SERVER_COLLISION:
211
logger.error(u"Zeroconf server name collision")
213
elif state == avahi.SERVER_RUNNING:
216
"""Derived from the Avahi example code"""
217
if self.server is None:
218
self.server = dbus.Interface(
219
self.bus.get_object(avahi.DBUS_NAME,
220
avahi.DBUS_PATH_SERVER),
221
avahi.DBUS_INTERFACE_SERVER)
222
self.server.connect_to_signal(u"StateChanged",
223
self.server_state_changed)
224
self.server_state_changed(self.server.GetState())
148
group = dbus.Interface\
149
(bus.get_object(avahi.DBUS_NAME,
150
server.EntryGroupNew()),
151
avahi.DBUS_INTERFACE_ENTRY_GROUP)
152
group.connect_to_signal('StateChanged',
153
entry_group_state_changed)
154
logger.debug(u"Adding service '%s' of type '%s' ...",
155
service.name, service.type)
157
self.interface, # interface
158
avahi.PROTO_INET6, # protocol
159
dbus.UInt32(0), # flags
160
self.name, self.type,
161
self.domain, self.host,
162
dbus.UInt16(self.port),
163
avahi.string_array_to_txt_array(self.TXT))
166
# From the Avahi example code:
167
group = None # our entry group
168
# End of Avahi example code
227
171
class Client(object):
228
172
"""A representation of a client host served by this server.
231
name: string; from the config file, used in log messages and
174
name: string; from the config file, used in log messages
233
175
fingerprint: string (40 or 32 hexadecimal digits); used to
234
176
uniquely identify the client
235
secret: bytestring; sent verbatim (over TLS) to client
236
host: string; available for use by the checker command
237
created: datetime.datetime(); (UTC) object creation
238
last_enabled: datetime.datetime(); (UTC)
240
last_checked_ok: datetime.datetime(); (UTC) or None
241
timeout: datetime.timedelta(); How long from last_checked_ok
242
until this client is invalid
243
interval: datetime.timedelta(); How often to start a new checker
244
disable_hook: If set, called by disable() as disable_hook(self)
245
checker: subprocess.Popen(); a running checker process used
246
to see if the client lives.
247
'None' if no process is running.
177
secret: bytestring; sent verbatim (over TLS) to client
178
fqdn: string (FQDN); available for use by the checker command
179
created: datetime.datetime(); object creation, not client host
180
last_checked_ok: datetime.datetime() or None if not yet checked OK
181
timeout: datetime.timedelta(); How long from last_checked_ok
182
until this client is invalid
183
interval: datetime.timedelta(); How often to start a new checker
184
stop_hook: If set, called by stop() as stop_hook(self)
185
checker: subprocess.Popen(); a running checker process used
186
to see if the client lives.
187
'None' if no process is running.
248
188
checker_initiator_tag: a gobject event source tag, or None
249
disable_initiator_tag: - '' -
189
stop_initiator_tag: - '' -
250
190
checker_callback_tag: - '' -
251
191
checker_command: string; External command which is run to check if
252
192
client lives. %() expansions are done at
253
193
runtime with vars(self) as dict, so that for
254
194
instance %(name)s can be used in the command.
255
current_checker_command: string; current running checker_command
196
_timeout: Real variable for 'timeout'
197
_interval: Real variable for 'interval'
198
_timeout_milliseconds: Used when calling gobject.timeout_add()
199
_interval_milliseconds: - '' -
259
def _datetime_to_milliseconds(dt):
260
"Convert a datetime.datetime() to milliseconds"
261
return ((dt.days * 24 * 60 * 60 * 1000)
262
+ (dt.seconds * 1000)
263
+ (dt.microseconds // 1000))
265
def timeout_milliseconds(self):
266
"Return the 'timeout' attribute in milliseconds"
267
return self._datetime_to_milliseconds(self.timeout)
269
def interval_milliseconds(self):
270
"Return the 'interval' attribute in milliseconds"
271
return self._datetime_to_milliseconds(self.interval)
273
def __init__(self, name = None, disable_hook=None, config=None):
274
"""Note: the 'checker' key in 'config' sets the
275
'checker_command' attribute and *not* the 'checker'
201
def _set_timeout(self, timeout):
202
"Setter function for 'timeout' attribute"
203
self._timeout = timeout
204
self._timeout_milliseconds = ((self.timeout.days
205
* 24 * 60 * 60 * 1000)
206
+ (self.timeout.seconds * 1000)
207
+ (self.timeout.microseconds
209
timeout = property(lambda self: self._timeout,
212
def _set_interval(self, interval):
213
"Setter function for 'interval' attribute"
214
self._interval = interval
215
self._interval_milliseconds = ((self.interval.days
216
* 24 * 60 * 60 * 1000)
217
+ (self.interval.seconds
219
+ (self.interval.microseconds
221
interval = property(lambda self: self._interval,
224
def __init__(self, name=None, stop_hook=None, fingerprint=None,
225
secret=None, secfile=None, fqdn=None, timeout=None,
226
interval=-1, checker=None):
227
"""Note: the 'checker' argument sets the 'checker_command'
228
attribute and not the 'checker' attribute.."""
280
230
logger.debug(u"Creating client %r", self.name)
281
# Uppercase and remove spaces from fingerprint for later
282
# comparison purposes with return value from the fingerprint()
284
self.fingerprint = (config[u"fingerprint"].upper()
231
# Uppercase and remove spaces from fingerprint
232
# for later comparison purposes with return value of
233
# the fingerprint() function
234
self.fingerprint = fingerprint.upper().replace(u" ", u"")
286
235
logger.debug(u" Fingerprint: %s", self.fingerprint)
287
if u"secret" in config:
288
self.secret = config[u"secret"].decode(u"base64")
289
elif u"secfile" in config:
290
with closing(open(os.path.expanduser
292
(config[u"secfile"])))) as secfile:
293
self.secret = secfile.read()
237
self.secret = secret.decode(u"base64")
240
self.secret = sf.read()
295
243
raise TypeError(u"No secret or secfile for client %s"
297
self.host = config.get(u"host", u"")
298
self.created = datetime.datetime.utcnow()
300
self.last_enabled = None
246
self.created = datetime.datetime.now()
301
247
self.last_checked_ok = None
302
self.timeout = string_to_delta(config[u"timeout"])
303
self.interval = string_to_delta(config[u"interval"])
304
self.disable_hook = disable_hook
248
self.timeout = string_to_delta(timeout)
249
self.interval = string_to_delta(interval)
250
self.stop_hook = stop_hook
305
251
self.checker = None
306
252
self.checker_initiator_tag = None
307
self.disable_initiator_tag = None
253
self.stop_initiator_tag = None
308
254
self.checker_callback_tag = None
309
self.checker_command = config[u"checker"]
310
self.current_checker_command = None
311
self.last_connect = None
255
self.check_command = checker
314
257
"""Start this client's checker and timeout hooks"""
315
self.last_enabled = datetime.datetime.utcnow()
316
258
# Schedule a new checker to be started an 'interval' from now,
317
259
# and every interval from then on.
318
self.checker_initiator_tag = (gobject.timeout_add
319
(self.interval_milliseconds(),
260
self.checker_initiator_tag = gobject.timeout_add\
261
(self._interval_milliseconds,
321
263
# Also start a new checker *right now*.
322
264
self.start_checker()
323
# Schedule a disable() when 'timeout' has passed
324
self.disable_initiator_tag = (gobject.timeout_add
325
(self.timeout_milliseconds(),
330
"""Disable this client."""
331
if not getattr(self, "enabled", False):
265
# Schedule a stop() when 'timeout' has passed
266
self.stop_initiator_tag = gobject.timeout_add\
267
(self._timeout_milliseconds,
271
The possibility that a client might be restarted is left open,
272
but not currently used."""
273
# If this client doesn't have a secret, it is already stopped.
275
logger.debug(u"Stopping client %s", self.name)
333
logger.info(u"Disabling client %s", self.name)
334
if getattr(self, u"disable_initiator_tag", False):
335
gobject.source_remove(self.disable_initiator_tag)
336
self.disable_initiator_tag = None
337
if getattr(self, u"checker_initiator_tag", False):
279
if getattr(self, "stop_initiator_tag", False):
280
gobject.source_remove(self.stop_initiator_tag)
281
self.stop_initiator_tag = None
282
if getattr(self, "checker_initiator_tag", False):
338
283
gobject.source_remove(self.checker_initiator_tag)
339
284
self.checker_initiator_tag = None
340
285
self.stop_checker()
341
if self.disable_hook:
342
self.disable_hook(self)
344
288
# Do not run this again if called by a gobject.timeout_add
347
290
def __del__(self):
348
self.disable_hook = None
351
def checker_callback(self, pid, condition, command):
291
self.stop_hook = None
293
def checker_callback(self, pid, condition):
352
294
"""The checker has completed, so take appropriate actions."""
295
now = datetime.datetime.now()
353
296
self.checker_callback_tag = None
354
297
self.checker = None
355
if os.WIFEXITED(condition):
356
exitstatus = os.WEXITSTATUS(condition)
358
logger.info(u"Checker for %(name)s succeeded",
362
logger.info(u"Checker for %(name)s failed",
298
if os.WIFEXITED(condition) \
299
and (os.WEXITSTATUS(condition) == 0):
300
logger.debug(u"Checker for %(name)s succeeded",
302
self.last_checked_ok = now
303
gobject.source_remove(self.stop_initiator_tag)
304
self.stop_initiator_tag = gobject.timeout_add\
305
(self._timeout_milliseconds,
307
elif not os.WIFEXITED(condition):
365
308
logger.warning(u"Checker for %(name)s crashed?",
368
def checked_ok(self):
369
"""Bump up the timeout for this client.
371
This should only be called when the client has been seen,
374
self.last_checked_ok = datetime.datetime.utcnow()
375
gobject.source_remove(self.disable_initiator_tag)
376
self.disable_initiator_tag = (gobject.timeout_add
377
(self.timeout_milliseconds(),
311
logger.debug(u"Checker for %(name)s failed",
380
313
def start_checker(self):
381
314
"""Start a new checker subprocess if one is not running.
383
315
If a checker already exists, leave it running and do
385
317
# The reason for not killing a running checker is that if we
462
368
if error.errno != errno.ESRCH: # No such process
464
370
self.checker = None
466
371
def still_valid(self):
467
372
"""Has the timeout not yet passed for this client?"""
468
if not getattr(self, u"enabled", False):
470
now = datetime.datetime.utcnow()
373
now = datetime.datetime.now()
471
374
if self.last_checked_ok is None:
472
375
return now < (self.created + self.timeout)
474
377
return now < (self.last_checked_ok + self.timeout)
477
class ClientDBus(Client, dbus.service.Object):
478
"""A Client class using D-Bus
481
dbus_object_path: dbus.ObjectPath
482
bus: dbus.SystemBus()
484
# dbus.service.Object doesn't use super(), so we can't either.
486
def __init__(self, bus = None, *args, **kwargs):
488
Client.__init__(self, *args, **kwargs)
489
# Only now, when this client is initialized, can it show up on
491
self.dbus_object_path = (dbus.ObjectPath
493
+ self.name.replace(u".", u"_")))
494
dbus.service.Object.__init__(self, self.bus,
495
self.dbus_object_path)
498
def _datetime_to_dbus(dt, variant_level=0):
499
"""Convert a UTC datetime.datetime() to a D-Bus type."""
500
return dbus.String(dt.isoformat(),
501
variant_level=variant_level)
504
oldstate = getattr(self, u"enabled", False)
505
r = Client.enable(self)
506
if oldstate != self.enabled:
508
self.PropertyChanged(dbus.String(u"enabled"),
509
dbus.Boolean(True, variant_level=1))
510
self.PropertyChanged(
511
dbus.String(u"last_enabled"),
512
self._datetime_to_dbus(self.last_enabled,
516
def disable(self, signal = True):
517
oldstate = getattr(self, u"enabled", False)
518
r = Client.disable(self)
519
if signal and oldstate != self.enabled:
521
self.PropertyChanged(dbus.String(u"enabled"),
522
dbus.Boolean(False, variant_level=1))
525
def __del__(self, *args, **kwargs):
527
self.remove_from_connection()
530
if hasattr(dbus.service.Object, u"__del__"):
531
dbus.service.Object.__del__(self, *args, **kwargs)
532
Client.__del__(self, *args, **kwargs)
534
def checker_callback(self, pid, condition, command,
536
self.checker_callback_tag = None
539
self.PropertyChanged(dbus.String(u"checker_running"),
540
dbus.Boolean(False, variant_level=1))
541
if os.WIFEXITED(condition):
542
exitstatus = os.WEXITSTATUS(condition)
544
self.CheckerCompleted(dbus.Int16(exitstatus),
545
dbus.Int64(condition),
546
dbus.String(command))
549
self.CheckerCompleted(dbus.Int16(-1),
550
dbus.Int64(condition),
551
dbus.String(command))
553
return Client.checker_callback(self, pid, condition, command,
556
def checked_ok(self, *args, **kwargs):
557
r = Client.checked_ok(self, *args, **kwargs)
559
self.PropertyChanged(
560
dbus.String(u"last_checked_ok"),
561
(self._datetime_to_dbus(self.last_checked_ok,
565
def start_checker(self, *args, **kwargs):
566
old_checker = self.checker
567
if self.checker is not None:
568
old_checker_pid = self.checker.pid
570
old_checker_pid = None
571
r = Client.start_checker(self, *args, **kwargs)
572
# Only if new checker process was started
573
if (self.checker is not None
574
and old_checker_pid != self.checker.pid):
576
self.CheckerStarted(self.current_checker_command)
577
self.PropertyChanged(
578
dbus.String(u"checker_running"),
579
dbus.Boolean(True, variant_level=1))
582
def stop_checker(self, *args, **kwargs):
583
old_checker = getattr(self, u"checker", None)
584
r = Client.stop_checker(self, *args, **kwargs)
585
if (old_checker is not None
586
and getattr(self, u"checker", None) is None):
587
self.PropertyChanged(dbus.String(u"checker_running"),
588
dbus.Boolean(False, variant_level=1))
591
## D-Bus methods & signals
592
_interface = u"se.bsnet.fukt.Mandos.Client"
595
@dbus.service.method(_interface)
597
return self.checked_ok()
599
# CheckerCompleted - signal
600
@dbus.service.signal(_interface, signature=u"nxs")
601
def CheckerCompleted(self, exitcode, waitstatus, command):
605
# CheckerStarted - signal
606
@dbus.service.signal(_interface, signature=u"s")
607
def CheckerStarted(self, command):
611
# GetAllProperties - method
612
@dbus.service.method(_interface, out_signature=u"a{sv}")
613
def GetAllProperties(self):
615
return dbus.Dictionary({
616
dbus.String(u"name"):
617
dbus.String(self.name, variant_level=1),
618
dbus.String(u"fingerprint"):
619
dbus.String(self.fingerprint, variant_level=1),
620
dbus.String(u"host"):
621
dbus.String(self.host, variant_level=1),
622
dbus.String(u"created"):
623
self._datetime_to_dbus(self.created,
625
dbus.String(u"last_enabled"):
626
(self._datetime_to_dbus(self.last_enabled,
628
if self.last_enabled is not None
629
else dbus.Boolean(False, variant_level=1)),
630
dbus.String(u"enabled"):
631
dbus.Boolean(self.enabled, variant_level=1),
632
dbus.String(u"last_checked_ok"):
633
(self._datetime_to_dbus(self.last_checked_ok,
635
if self.last_checked_ok is not None
636
else dbus.Boolean (False, variant_level=1)),
637
dbus.String(u"timeout"):
638
dbus.UInt64(self.timeout_milliseconds(),
640
dbus.String(u"interval"):
641
dbus.UInt64(self.interval_milliseconds(),
643
dbus.String(u"checker"):
644
dbus.String(self.checker_command,
646
dbus.String(u"checker_running"):
647
dbus.Boolean(self.checker is not None,
649
dbus.String(u"object_path"):
650
dbus.ObjectPath(self.dbus_object_path,
654
# IsStillValid - method
655
@dbus.service.method(_interface, out_signature=u"b")
656
def IsStillValid(self):
657
return self.still_valid()
659
# PropertyChanged - signal
660
@dbus.service.signal(_interface, signature=u"sv")
661
def PropertyChanged(self, property, value):
665
# ReceivedSecret - signal
666
@dbus.service.signal(_interface)
667
def ReceivedSecret(self):
672
@dbus.service.signal(_interface)
677
# SetChecker - method
678
@dbus.service.method(_interface, in_signature=u"s")
679
def SetChecker(self, checker):
680
"D-Bus setter method"
681
self.checker_command = checker
683
self.PropertyChanged(dbus.String(u"checker"),
684
dbus.String(self.checker_command,
688
@dbus.service.method(_interface, in_signature=u"s")
689
def SetHost(self, host):
690
"D-Bus setter method"
693
self.PropertyChanged(dbus.String(u"host"),
694
dbus.String(self.host, variant_level=1))
696
# SetInterval - method
697
@dbus.service.method(_interface, in_signature=u"t")
698
def SetInterval(self, milliseconds):
699
self.interval = datetime.timedelta(0, 0, 0, milliseconds)
701
self.PropertyChanged(dbus.String(u"interval"),
702
(dbus.UInt64(self.interval_milliseconds(),
706
@dbus.service.method(_interface, in_signature=u"ay",
708
def SetSecret(self, secret):
709
"D-Bus setter method"
710
self.secret = str(secret)
712
# SetTimeout - method
713
@dbus.service.method(_interface, in_signature=u"t")
714
def SetTimeout(self, milliseconds):
715
self.timeout = datetime.timedelta(0, 0, 0, milliseconds)
717
self.PropertyChanged(dbus.String(u"timeout"),
718
(dbus.UInt64(self.timeout_milliseconds(),
722
@dbus.service.method(_interface)
727
# StartChecker - method
728
@dbus.service.method(_interface)
729
def StartChecker(self):
734
@dbus.service.method(_interface)
739
# StopChecker - method
740
@dbus.service.method(_interface)
741
def StopChecker(self):
747
class ClientHandler(socketserver.BaseRequestHandler, object):
748
"""A class to handle client connections.
750
Instantiated once for each connection to handle it.
380
def peer_certificate(session):
381
"Return the peer's OpenPGP certificate as a bytestring"
382
# If not an OpenPGP certificate...
383
if gnutls.library.functions.gnutls_certificate_type_get\
384
(session._c_object) \
385
!= gnutls.library.constants.GNUTLS_CRT_OPENPGP:
386
# ...do the normal thing
387
return session.peer_certificate
388
list_size = ctypes.c_uint()
389
cert_list = gnutls.library.functions.gnutls_certificate_get_peers\
390
(session._c_object, ctypes.byref(list_size))
391
if list_size.value == 0:
394
return ctypes.string_at(cert.data, cert.size)
397
def fingerprint(openpgp):
398
"Convert an OpenPGP bytestring to a hexdigit fingerprint string"
399
# New empty GnuTLS certificate
400
crt = gnutls.library.types.gnutls_openpgp_crt_t()
401
gnutls.library.functions.gnutls_openpgp_crt_init\
403
# New GnuTLS "datum" with the OpenPGP public key
404
datum = gnutls.library.types.gnutls_datum_t\
405
(ctypes.cast(ctypes.c_char_p(openpgp),
406
ctypes.POINTER(ctypes.c_ubyte)),
407
ctypes.c_uint(len(openpgp)))
408
# Import the OpenPGP public key into the certificate
409
ret = gnutls.library.functions.gnutls_openpgp_crt_import\
412
gnutls.library.constants.GNUTLS_OPENPGP_FMT_RAW)
413
# New buffer for the fingerprint
414
buffer = ctypes.create_string_buffer(20)
415
buffer_length = ctypes.c_size_t()
416
# Get the fingerprint from the certificate into the buffer
417
gnutls.library.functions.gnutls_openpgp_crt_get_fingerprint\
418
(crt, ctypes.byref(buffer), ctypes.byref(buffer_length))
419
# Deinit the certificate
420
gnutls.library.functions.gnutls_openpgp_crt_deinit(crt)
421
# Convert the buffer to a Python bytestring
422
fpr = ctypes.string_at(buffer, buffer_length.value)
423
# Convert the bytestring to hexadecimal notation
424
hex_fpr = u''.join(u"%02X" % ord(char) for char in fpr)
428
class tcp_handler(SocketServer.BaseRequestHandler, object):
429
"""A TCP request handler class.
430
Instantiated by IPv6_TCPServer for each request to handle it.
751
431
Note: This will run in its own forked process."""
753
433
def handle(self):
754
logger.info(u"TCP connection from: %s",
755
unicode(self.client_address))
756
logger.debug(u"IPC Pipe FD: %d", self.server.pipe[1])
757
# Open IPC pipe to parent process
758
with closing(os.fdopen(self.server.pipe[1], u"w", 1)) as ipc:
759
session = (gnutls.connection
760
.ClientSession(self.request,
764
line = self.request.makefile().readline()
765
logger.debug(u"Protocol version: %r", line)
767
if int(line.strip().split()[0]) > 1:
769
except (ValueError, IndexError, RuntimeError), error:
770
logger.error(u"Unknown protocol version: %s", error)
773
# Note: gnutls.connection.X509Credentials is really a
774
# generic GnuTLS certificate credentials object so long as
775
# no X.509 keys are added to it. Therefore, we can use it
776
# here despite using OpenPGP certificates.
778
#priority = u':'.join((u"NONE", u"+VERS-TLS1.1",
779
# u"+AES-256-CBC", u"+SHA1",
780
# u"+COMP-NULL", u"+CTYPE-OPENPGP",
782
# Use a fallback default, since this MUST be set.
783
priority = self.server.gnutls_priority
786
(gnutls.library.functions
787
.gnutls_priority_set_direct(session._c_object,
792
except gnutls.errors.GNUTLSError, error:
793
logger.warning(u"Handshake failed: %s", error)
794
# Do not run session.bye() here: the session is not
795
# established. Just abandon the request.
797
logger.debug(u"Handshake succeeded")
799
fpr = self.fingerprint(self.peer_certificate(session))
800
except (TypeError, gnutls.errors.GNUTLSError), error:
801
logger.warning(u"Bad certificate: %s", error)
804
logger.debug(u"Fingerprint: %s", fpr)
806
for c in self.server.clients:
807
if c.fingerprint == fpr:
811
ipc.write(u"NOTFOUND %s\n" % fpr)
814
# Have to check if client.still_valid(), since it is
815
# possible that the client timed out while establishing
816
# the GnuTLS session.
817
if not client.still_valid():
818
ipc.write(u"INVALID %s\n" % client.name)
821
ipc.write(u"SENDING %s\n" % client.name)
823
while sent_size < len(client.secret):
824
sent = session.send(client.secret[sent_size:])
825
logger.debug(u"Sent: %d, remaining: %d",
826
sent, len(client.secret)
827
- (sent_size + sent))
832
def peer_certificate(session):
833
"Return the peer's OpenPGP certificate as a bytestring"
834
# If not an OpenPGP certificate...
835
if (gnutls.library.functions
836
.gnutls_certificate_type_get(session._c_object)
837
!= gnutls.library.constants.GNUTLS_CRT_OPENPGP):
838
# ...do the normal thing
839
return session.peer_certificate
840
list_size = ctypes.c_uint(1)
841
cert_list = (gnutls.library.functions
842
.gnutls_certificate_get_peers
843
(session._c_object, ctypes.byref(list_size)))
844
if not bool(cert_list) and list_size.value != 0:
845
raise gnutls.errors.GNUTLSError(u"error getting peer"
847
if list_size.value == 0:
850
return ctypes.string_at(cert.data, cert.size)
853
def fingerprint(openpgp):
854
"Convert an OpenPGP bytestring to a hexdigit fingerprint"
855
# New GnuTLS "datum" with the OpenPGP public key
856
datum = (gnutls.library.types
857
.gnutls_datum_t(ctypes.cast(ctypes.c_char_p(openpgp),
860
ctypes.c_uint(len(openpgp))))
861
# New empty GnuTLS certificate
862
crt = gnutls.library.types.gnutls_openpgp_crt_t()
863
(gnutls.library.functions
864
.gnutls_openpgp_crt_init(ctypes.byref(crt)))
865
# Import the OpenPGP public key into the certificate
866
(gnutls.library.functions
867
.gnutls_openpgp_crt_import(crt, ctypes.byref(datum),
868
gnutls.library.constants
869
.GNUTLS_OPENPGP_FMT_RAW))
870
# Verify the self signature in the key
871
crtverify = ctypes.c_uint()
872
(gnutls.library.functions
873
.gnutls_openpgp_crt_verify_self(crt, 0,
874
ctypes.byref(crtverify)))
875
if crtverify.value != 0:
876
gnutls.library.functions.gnutls_openpgp_crt_deinit(crt)
877
raise (gnutls.errors.CertificateSecurityError
879
# New buffer for the fingerprint
880
buf = ctypes.create_string_buffer(20)
881
buf_len = ctypes.c_size_t()
882
# Get the fingerprint from the certificate into the buffer
883
(gnutls.library.functions
884
.gnutls_openpgp_crt_get_fingerprint(crt, ctypes.byref(buf),
885
ctypes.byref(buf_len)))
886
# Deinit the certificate
887
gnutls.library.functions.gnutls_openpgp_crt_deinit(crt)
888
# Convert the buffer to a Python bytestring
889
fpr = ctypes.string_at(buf, buf_len.value)
890
# Convert the bytestring to hexadecimal notation
891
hex_fpr = u''.join(u"%02X" % ord(char) for char in fpr)
895
class ForkingMixInWithPipe(socketserver.ForkingMixIn, object):
896
"""Like socketserver.ForkingMixIn, but also pass a pipe."""
897
def process_request(self, request, client_address):
898
"""Overrides and wraps the original process_request().
900
This function creates a new pipe in self.pipe
902
self.pipe = os.pipe()
903
super(ForkingMixInWithPipe,
904
self).process_request(request, client_address)
905
os.close(self.pipe[1]) # close write end
906
self.add_pipe(self.pipe[0])
907
def add_pipe(self, pipe):
908
"""Dummy function; override as necessary"""
912
class IPv6_TCPServer(ForkingMixInWithPipe,
913
socketserver.TCPServer, object):
914
"""IPv6-capable TCP server. Accepts 'None' as address and/or port
434
logger.debug(u"TCP connection from: %s",
435
unicode(self.client_address))
436
session = gnutls.connection.ClientSession\
437
(self.request, gnutls.connection.X509Credentials())
439
line = self.request.makefile().readline()
440
logger.debug(u"Protocol version: %r", line)
442
if int(line.strip().split()[0]) > 1:
444
except (ValueError, IndexError, RuntimeError), error:
445
logger.error(u"Unknown protocol version: %s", error)
448
# Note: gnutls.connection.X509Credentials is really a generic
449
# GnuTLS certificate credentials object so long as no X.509
450
# keys are added to it. Therefore, we can use it here despite
451
# using OpenPGP certificates.
453
#priority = ':'.join(("NONE", "+VERS-TLS1.1", "+AES-256-CBC",
454
# "+SHA1", "+COMP-NULL", "+CTYPE-OPENPGP",
456
priority = "NORMAL" # Fallback default, since this
458
if self.server.settings["priority"]:
459
priority = self.server.settings["priority"]
460
gnutls.library.functions.gnutls_priority_set_direct\
461
(session._c_object, priority, None);
465
except gnutls.errors.GNUTLSError, error:
466
logger.debug(u"Handshake failed: %s", error)
467
# Do not run session.bye() here: the session is not
468
# established. Just abandon the request.
471
fpr = fingerprint(peer_certificate(session))
472
except (TypeError, gnutls.errors.GNUTLSError), error:
473
logger.debug(u"Bad certificate: %s", error)
476
logger.debug(u"Fingerprint: %s", fpr)
478
for c in self.server.clients:
479
if c.fingerprint == fpr:
483
logger.debug(u"Client not found for fingerprint: %s", fpr)
486
# Have to check if client.still_valid(), since it is possible
487
# that the client timed out while establishing the GnuTLS
489
if not client.still_valid():
490
logger.debug(u"Client %(name)s is invalid", vars(client))
494
while sent_size < len(client.secret):
495
sent = session.send(client.secret[sent_size:])
496
logger.debug(u"Sent: %d, remaining: %d",
497
sent, len(client.secret)
498
- (sent_size + sent))
503
class IPv6_TCPServer(SocketServer.ForkingTCPServer, object):
504
"""IPv6 TCP server. Accepts 'None' as address and/or port.
917
enabled: Boolean; whether this server is activated yet
918
interface: None or a network interface name (string)
919
use_ipv6: Boolean; to use IPv6 or not
506
settings: Server settings
507
clients: Set() of Client objects
921
def __init__(self, server_address, RequestHandlerClass,
922
interface=None, use_ipv6=True):
923
self.interface = interface
925
self.address_family = socket.AF_INET6
926
socketserver.TCPServer.__init__(self, server_address,
509
address_family = socket.AF_INET6
510
def __init__(self, *args, **kwargs):
511
if "settings" in kwargs:
512
self.settings = kwargs["settings"]
513
del kwargs["settings"]
514
if "clients" in kwargs:
515
self.clients = kwargs["clients"]
516
del kwargs["clients"]
517
return super(type(self), self).__init__(*args, **kwargs)
928
518
def server_bind(self):
929
519
"""This overrides the normal server_bind() function
930
520
to bind to an interface if one was specified, and also NOT to
931
521
bind to an address or port if they were not specified."""
932
if self.interface is not None:
933
if SO_BINDTODEVICE is None:
934
logger.error(u"SO_BINDTODEVICE does not exist;"
935
u" cannot bind to interface %s",
939
self.socket.setsockopt(socket.SOL_SOCKET,
943
except socket.error, error:
944
if error[0] == errno.EPERM:
945
logger.error(u"No permission to"
946
u" bind to interface %s",
948
elif error[0] == errno.ENOPROTOOPT:
949
logger.error(u"SO_BINDTODEVICE not available;"
950
u" cannot bind to interface %s",
522
if self.settings["interface"]:
523
# 25 is from /usr/include/asm-i486/socket.h
524
SO_BINDTODEVICE = getattr(socket, "SO_BINDTODEVICE", 25)
526
self.socket.setsockopt(socket.SOL_SOCKET,
528
self.settings["interface"])
529
except socket.error, error:
530
if error[0] == errno.EPERM:
531
logger.warning(u"No permission to"
532
u" bind to interface %s",
533
self.settings["interface"])
954
536
# Only bind(2) the socket if we really need to.
955
537
if self.server_address[0] or self.server_address[1]:
956
538
if not self.server_address[0]:
957
if self.address_family == socket.AF_INET6:
958
any_address = u"::" # in6addr_any
960
any_address = socket.INADDR_ANY
961
self.server_address = (any_address,
540
self.server_address = (in6addr_any,
962
541
self.server_address[1])
963
elif not self.server_address[1]:
542
elif self.server_address[1] is None:
964
543
self.server_address = (self.server_address[0],
967
# self.server_address = (self.server_address[0],
972
return socketserver.TCPServer.server_bind(self)
975
class MandosServer(IPv6_TCPServer):
979
clients: set of Client objects
980
gnutls_priority GnuTLS priority string
981
use_dbus: Boolean; to emit D-Bus signals or not
982
clients: set of Client objects
983
gnutls_priority GnuTLS priority string
984
use_dbus: Boolean; to emit D-Bus signals or not
986
Assumes a gobject.MainLoop event loop.
988
def __init__(self, server_address, RequestHandlerClass,
989
interface=None, use_ipv6=True, clients=None,
990
gnutls_priority=None, use_dbus=True):
992
self.clients = clients
993
self.use_dbus = use_dbus
994
self.gnutls_priority = gnutls_priority
995
IPv6_TCPServer.__init__(self, server_address,
997
interface = interface,
999
def server_activate(self):
1001
return socketserver.TCPServer.server_activate(self)
1004
def add_pipe(self, pipe):
1005
# Call "handle_ipc" for both data and EOF events
1006
gobject.io_add_watch(pipe, gobject.IO_IN | gobject.IO_HUP,
1008
def handle_ipc(self, source, condition, file_objects={}):
1010
gobject.IO_IN: u"IN", # There is data to read.
1011
gobject.IO_OUT: u"OUT", # Data can be written (without
1013
gobject.IO_PRI: u"PRI", # There is urgent data to read.
1014
gobject.IO_ERR: u"ERR", # Error condition.
1015
gobject.IO_HUP: u"HUP" # Hung up (the connection has been
1016
# broken, usually for pipes and
1019
conditions_string = ' | '.join(name
1021
condition_names.iteritems()
1022
if cond & condition)
1023
logger.debug(u"Handling IPC: FD = %d, condition = %s", source,
1026
# Turn the pipe file descriptor into a Python file object
1027
if source not in file_objects:
1028
file_objects[source] = os.fdopen(source, u"r", 1)
1030
# Read a line from the file object
1031
cmdline = file_objects[source].readline()
1032
if not cmdline: # Empty line means end of file
1033
# close the IPC pipe
1034
file_objects[source].close()
1035
del file_objects[source]
1037
# Stop calling this function
1040
logger.debug(u"IPC command: %r", cmdline)
1042
# Parse and act on command
1043
cmd, args = cmdline.rstrip(u"\r\n").split(None, 1)
1045
if cmd == u"NOTFOUND":
1046
logger.warning(u"Client not found for fingerprint: %s",
1050
mandos_dbus_service.ClientNotFound(args)
1051
elif cmd == u"INVALID":
1052
for client in self.clients:
1053
if client.name == args:
1054
logger.warning(u"Client %s is invalid", args)
1060
logger.error(u"Unknown client %s is invalid", args)
1061
elif cmd == u"SENDING":
1062
for client in self.clients:
1063
if client.name == args:
1064
logger.info(u"Sending secret to %s", client.name)
1068
client.ReceivedSecret()
1071
logger.error(u"Sending secret to unknown client %s",
1074
logger.error(u"Unknown IPC command: %r", cmdline)
1076
# Keep calling this function
545
return super(type(self), self).server_bind()
1080
548
def string_to_delta(interval):
1081
549
"""Parse a string and return a datetime.timedelta
1083
>>> string_to_delta(u'7d')
551
>>> string_to_delta('7d')
1084
552
datetime.timedelta(7)
1085
>>> string_to_delta(u'60s')
553
>>> string_to_delta('60s')
1086
554
datetime.timedelta(0, 60)
1087
>>> string_to_delta(u'60m')
555
>>> string_to_delta('60m')
1088
556
datetime.timedelta(0, 3600)
1089
>>> string_to_delta(u'24h')
557
>>> string_to_delta('24h')
1090
558
datetime.timedelta(1)
1091
559
>>> string_to_delta(u'1w')
1092
560
datetime.timedelta(7)
1093
>>> string_to_delta(u'5m 30s')
1094
datetime.timedelta(0, 330)
1096
timevalue = datetime.timedelta(0)
1097
for s in interval.split():
1099
suffix = unicode(s[-1])
1102
delta = datetime.timedelta(value)
1103
elif suffix == u"s":
1104
delta = datetime.timedelta(0, value)
1105
elif suffix == u"m":
1106
delta = datetime.timedelta(0, 0, 0, 0, value)
1107
elif suffix == u"h":
1108
delta = datetime.timedelta(0, 0, 0, 0, 0, value)
1109
elif suffix == u"w":
1110
delta = datetime.timedelta(0, 0, 0, 0, 0, 0, value)
1113
except (ValueError, IndexError):
563
suffix=unicode(interval[-1])
564
value=int(interval[:-1])
566
delta = datetime.timedelta(value)
568
delta = datetime.timedelta(0, value)
570
delta = datetime.timedelta(0, 0, 0, 0, value)
572
delta = datetime.timedelta(0, 0, 0, 0, 0, value)
574
delta = datetime.timedelta(0, 0, 0, 0, 0, 0, value)
1114
576
raise ValueError
577
except (ValueError, IndexError):
582
def server_state_changed(state):
583
"""Derived from the Avahi example code"""
584
if state == avahi.SERVER_COLLISION:
585
logger.warning(u"Server name collision")
587
elif state == avahi.SERVER_RUNNING:
591
def entry_group_state_changed(state, error):
592
"""Derived from the Avahi example code"""
593
logger.debug(u"state change: %i", state)
595
if state == avahi.ENTRY_GROUP_ESTABLISHED:
596
logger.debug(u"Service established.")
597
elif state == avahi.ENTRY_GROUP_COLLISION:
598
logger.warning(u"Service name collision.")
600
elif state == avahi.ENTRY_GROUP_FAILURE:
601
logger.critical(u"Error in group state changed %s",
603
raise AvahiGroupError("State changed: %s", str(error))
1119
605
def if_nametoindex(interface):
1120
"""Call the C function if_nametoindex(), or equivalent
1122
Note: This function cannot accept a unicode string."""
606
"""Call the C function if_nametoindex(), or equivalent"""
1123
607
global if_nametoindex
1125
if_nametoindex = (ctypes.cdll.LoadLibrary
1126
(ctypes.util.find_library(u"c"))
609
if "ctypes.util" not in sys.modules:
611
if_nametoindex = ctypes.cdll.LoadLibrary\
612
(ctypes.util.find_library("c")).if_nametoindex
1128
613
except (OSError, AttributeError):
1129
logger.warning(u"Doing if_nametoindex the hard way")
614
if "struct" not in sys.modules:
616
if "fcntl" not in sys.modules:
1130
618
def if_nametoindex(interface):
1131
619
"Get an interface index the hard way, i.e. using fcntl()"
1132
620
SIOCGIFINDEX = 0x8933 # From /usr/include/linux/sockios.h
1133
with closing(socket.socket()) as s:
1134
ifreq = fcntl.ioctl(s, SIOCGIFINDEX,
1135
struct.pack(str(u"16s16x"),
1137
interface_index = struct.unpack(str(u"I"),
622
ifreq = fcntl.ioctl(s, SIOCGIFINDEX,
623
struct.pack("16s16x", interface))
625
interface_index = struct.unpack("I", ifreq[16:20])[0]
1139
626
return interface_index
1140
627
return if_nametoindex(interface)
1143
def daemon(nochdir = False, noclose = False):
630
def daemon(nochdir, noclose):
1144
631
"""See daemon(3). Standard BSD Unix function.
1146
632
This should really exist as os.daemon, but it doesn't (yet)."""
1155
639
# Close all standard open file descriptors
1156
640
null = os.open(os.path.devnull, os.O_NOCTTY | os.O_RDWR)
1157
641
if not stat.S_ISCHR(os.fstat(null).st_mode):
1158
642
raise OSError(errno.ENODEV,
1159
u"/dev/null not a character device")
643
"/dev/null not a character device")
1160
644
os.dup2(null, sys.stdin.fileno())
1161
645
os.dup2(null, sys.stdout.fileno())
1162
646
os.dup2(null, sys.stderr.fileno())
1204
682
# Default values for config file for server-global settings
1205
server_defaults = { u"interface": u"",
1210
u"SECURE256:!CTYPE-X.509:+CTYPE-OPENPGP",
1211
u"servicename": u"Mandos",
1212
u"use_dbus": u"True",
1213
u"use_ipv6": u"True",
683
server_defaults = { "interface": "",
688
"SECURE256:!CTYPE-X.509:+CTYPE-OPENPGP",
689
"servicename": "Mandos",
1216
692
# Parse config file for server-global settings
1217
server_config = configparser.SafeConfigParser(server_defaults)
693
server_config = ConfigParser.SafeConfigParser(server_defaults)
1218
694
del server_defaults
1219
server_config.read(os.path.join(options.configdir,
695
server_config.read(os.path.join(options.configdir, "server.conf"))
696
server_section = "server"
1221
697
# Convert the SafeConfigParser object to a dict
1222
server_settings = server_config.defaults()
1223
# Use the appropriate methods on the non-string config options
1224
for option in (u"debug", u"use_dbus", u"use_ipv6"):
1225
server_settings[option] = server_config.getboolean(u"DEFAULT",
1227
if server_settings["port"]:
1228
server_settings["port"] = server_config.getint(u"DEFAULT",
698
server_settings = dict(server_config.items(server_section))
699
# Use getboolean on the boolean config option
700
server_settings["debug"] = server_config.getboolean\
701
(server_section, "debug")
1230
702
del server_config
1232
704
# Override the settings from the config file with command line
1233
705
# options, if set.
1234
for option in (u"interface", u"address", u"port", u"debug",
1235
u"priority", u"servicename", u"configdir",
1236
u"use_dbus", u"use_ipv6"):
706
for option in ("interface", "address", "port", "debug",
707
"priority", "servicename", "configdir"):
1237
708
value = getattr(options, option)
1238
709
if value is not None:
1239
710
server_settings[option] = value
1241
# Force all strings to be unicode
1242
for option in server_settings.keys():
1243
if type(server_settings[option]) is str:
1244
server_settings[option] = unicode(server_settings[option])
1245
712
# Now we have our good server settings in "server_settings"
1247
##################################################################
1250
debug = server_settings[u"debug"]
1251
use_dbus = server_settings[u"use_dbus"]
1252
use_ipv6 = server_settings[u"use_ipv6"]
1255
syslogger.setLevel(logging.WARNING)
1256
console.setLevel(logging.WARNING)
1258
if server_settings[u"servicename"] != u"Mandos":
1259
syslogger.setFormatter(logging.Formatter
1260
(u'Mandos (%s) [%%(process)d]:'
1261
u' %%(levelname)s: %%(message)s'
1262
% server_settings[u"servicename"]))
1264
714
# Parse config file with clients
1265
client_defaults = { u"timeout": u"1h",
1267
u"checker": u"fping -q -- %%(host)s",
715
client_defaults = { "timeout": "1h",
717
"checker": "fping -q -- %%(fqdn)s",
1270
client_config = configparser.SafeConfigParser(client_defaults)
1271
client_config.read(os.path.join(server_settings[u"configdir"],
1274
global mandos_dbus_service
1275
mandos_dbus_service = None
1278
tcp_server = MandosServer((server_settings[u"address"],
1279
server_settings[u"port"]),
1281
interface=server_settings[u"interface"],
1285
server_settings[u"priority"],
1287
pidfilename = u"/var/run/mandos.pid"
1289
pidfile = open(pidfilename, u"w")
1291
logger.error(u"Could not open file %r", pidfilename)
1294
uid = pwd.getpwnam(u"_mandos").pw_uid
1295
gid = pwd.getpwnam(u"_mandos").pw_gid
1298
uid = pwd.getpwnam(u"mandos").pw_uid
1299
gid = pwd.getpwnam(u"mandos").pw_gid
1302
uid = pwd.getpwnam(u"nobody").pw_uid
1303
gid = pwd.getpwnam(u"nobody").pw_gid
1310
except OSError, error:
1311
if error[0] != errno.EPERM:
1314
# Enable all possible GnuTLS debugging
1316
# "Use a log level over 10 to enable all debugging options."
1318
gnutls.library.functions.gnutls_global_set_log_level(11)
1320
@gnutls.library.types.gnutls_log_func
1321
def debug_gnutls(level, string):
1322
logger.debug(u"GnuTLS: %s", string[:-1])
1324
(gnutls.library.functions
1325
.gnutls_global_set_log_function(debug_gnutls))
719
client_config = ConfigParser.SafeConfigParser(client_defaults)
720
client_config.read(os.path.join(server_settings["configdir"],
724
service = AvahiService(name = server_settings["servicename"],
725
type = "_mandos._tcp", );
726
if server_settings["interface"]:
727
service.interface = if_nametoindex(server_settings["interface"])
1327
729
global main_loop
1328
732
# From the Avahi example code
1329
733
DBusGMainLoop(set_as_default=True )
1330
734
main_loop = gobject.MainLoop()
1331
735
bus = dbus.SystemBus()
736
server = dbus.Interface(
737
bus.get_object( avahi.DBUS_NAME, avahi.DBUS_PATH_SERVER ),
738
avahi.DBUS_INTERFACE_SERVER )
1332
739
# End of Avahi example code
1334
bus_name = dbus.service.BusName(u"se.bsnet.fukt.Mandos", bus)
1335
protocol = avahi.PROTO_INET6 if use_ipv6 else avahi.PROTO_INET
1336
service = AvahiService(name = server_settings[u"servicename"],
1337
servicetype = u"_mandos._tcp",
1338
protocol = protocol, bus = bus)
1339
if server_settings["interface"]:
1340
service.interface = (if_nametoindex
1341
(str(server_settings[u"interface"])))
1343
client_class = Client
1345
client_class = functools.partial(ClientDBus, bus = bus)
1347
client_class(name = section,
1348
config= dict(client_config.items(section)))
1349
for section in client_config.sections()))
1351
logger.warning(u"No clients defined")
741
debug = server_settings["debug"]
1354
# Redirect stdin so all checkers get /dev/null
1355
null = os.open(os.path.devnull, os.O_NOCTTY | os.O_RDWR)
1356
os.dup2(null, sys.stdin.fileno())
1360
# No console logging
1361
logger.removeHandler(console)
1362
# Close all input and output, do double fork, etc.
1366
with closing(pidfile):
1368
pidfile.write(str(pid) + "\n")
1371
logger.error(u"Could not write to file %r with PID %d",
1374
# "pidfile" was never created
744
console = logging.StreamHandler()
745
# console.setLevel(logging.DEBUG)
746
console.setFormatter(logging.Formatter\
747
('%(levelname)s: %(message)s'))
748
logger.addHandler(console)
752
def remove_from_clients(client):
753
clients.remove(client)
755
logger.debug(u"No clients left, exiting")
758
clients.update(Set(Client(name=section,
759
stop_hook = remove_from_clients,
760
**(dict(client_config\
762
for section in client_config.sections()))
1379
768
"Cleanup function; run on exit"
770
# From the Avahi example code
771
if not group is None:
774
# End of Avahi example code
1383
777
client = clients.pop()
1384
client.disable_hook = None
778
client.stop_hook = None
1387
781
atexit.register(cleanup)