/mandos/release

To get this branch, use:
bzr branch http://bzr.recompile.se/loggerhead/mandos/release

« back to all changes in this revision

Viewing changes to mandos

merge

Show diffs side-by-side

added added

removed removed

Lines of Context:
11
11
# and some lines in "main".
12
12
13
13
# Everything else is
14
 
# Copyright © 2008 Teddy Hogeborn
15
 
# Copyright © 2008 Björn Påhlsson
 
14
# Copyright © 2008,2009 Teddy Hogeborn
 
15
# Copyright © 2008,2009 Björn Påhlsson
16
16
17
17
# This program is free software: you can redistribute it and/or modify
18
18
# it under the terms of the GNU General Public License as published by
35
35
 
36
36
import SocketServer
37
37
import socket
38
 
from optparse import OptionParser
 
38
import optparse
39
39
import datetime
40
40
import errno
41
41
import gnutls.crypto
66
66
import ctypes
67
67
import ctypes.util
68
68
 
69
 
version = "1.0.2"
 
69
version = "1.0.8"
70
70
 
71
71
logger = logging.Logger('mandos')
72
72
syslogger = (logging.handlers.SysLogHandler
73
73
             (facility = logging.handlers.SysLogHandler.LOG_DAEMON,
74
74
              address = "/dev/log"))
75
75
syslogger.setFormatter(logging.Formatter
76
 
                       ('Mandos: %(levelname)s: %(message)s'))
 
76
                       ('Mandos [%(process)d]: %(levelname)s:'
 
77
                        ' %(message)s'))
77
78
logger.addHandler(syslogger)
78
79
 
79
80
console = logging.StreamHandler()
80
 
console.setFormatter(logging.Formatter('%(name)s: %(levelname)s:'
81
 
                                       ' %(message)s'))
 
81
console.setFormatter(logging.Formatter('%(name)s [%(process)d]:'
 
82
                                       ' %(levelname)s: %(message)s'))
82
83
logger.addHandler(console)
83
84
 
84
85
class AvahiError(Exception):
113
114
    """
114
115
    def __init__(self, interface = avahi.IF_UNSPEC, name = None,
115
116
                 servicetype = None, port = None, TXT = None,
116
 
                 domain = "", host = "", max_renames = 32768):
 
117
                 domain = "", host = "", max_renames = 32768,
 
118
                 protocol = avahi.PROTO_UNSPEC):
117
119
        self.interface = interface
118
120
        self.name = name
119
121
        self.type = servicetype
123
125
        self.host = host
124
126
        self.rename_count = 0
125
127
        self.max_renames = max_renames
 
128
        self.protocol = protocol
126
129
    def rename(self):
127
130
        """Derived from the Avahi example code"""
128
131
        if self.rename_count >= self.max_renames:
134
137
        logger.info(u"Changing Zeroconf service name to %r ...",
135
138
                    str(self.name))
136
139
        syslogger.setFormatter(logging.Formatter
137
 
                               ('Mandos (%s): %%(levelname)s:'
138
 
                                ' %%(message)s' % self.name))
 
140
                               ('Mandos (%s) [%%(process)d]:'
 
141
                                ' %%(levelname)s: %%(message)s'
 
142
                                % self.name))
139
143
        self.remove()
140
144
        self.add()
141
145
        self.rename_count += 1
157
161
                     service.name, service.type)
158
162
        group.AddService(
159
163
                self.interface,         # interface
160
 
                avahi.PROTO_INET6,      # protocol
 
164
                self.protocol,          # protocol
161
165
                dbus.UInt32(0),         # flags
162
166
                self.name, self.type,
163
167
                self.domain, self.host,
175
179
    return dbus.String(dt.isoformat(), variant_level=variant_level)
176
180
 
177
181
 
178
 
class Client(dbus.service.Object):
 
182
class Client(object):
179
183
    """A representation of a client host served by this server.
180
184
    Attributes:
181
 
    name:       string; from the config file, used in log messages
 
185
    name:       string; from the config file, used in log messages and
 
186
                        D-Bus identifiers
182
187
    fingerprint: string (40 or 32 hexadecimal digits); used to
183
188
                 uniquely identify the client
184
189
    secret:     bytestring; sent verbatim (over TLS) to client
201
206
                     client lives.  %() expansions are done at
202
207
                     runtime with vars(self) as dict, so that for
203
208
                     instance %(name)s can be used in the command.
204
 
    use_dbus: bool(); Whether to provide D-Bus interface and signals
205
 
    dbus_object_path: dbus.ObjectPath ; only set if self.use_dbus
 
209
    current_checker_command: string; current running checker_command
206
210
    """
207
211
    def timeout_milliseconds(self):
208
212
        "Return the 'timeout' attribute in milliseconds"
216
220
                + (self.interval.seconds * 1000)
217
221
                + (self.interval.microseconds // 1000))
218
222
    
219
 
    def __init__(self, name = None, disable_hook=None, config=None,
220
 
                 use_dbus=True):
 
223
    def __init__(self, name = None, disable_hook=None, config=None):
221
224
        """Note: the 'checker' key in 'config' sets the
222
225
        'checker_command' attribute and *not* the 'checker'
223
226
        attribute."""
225
228
        if config is None:
226
229
            config = {}
227
230
        logger.debug(u"Creating client %r", self.name)
228
 
        self.use_dbus = use_dbus
229
 
        if self.use_dbus:
230
 
            self.dbus_object_path = (dbus.ObjectPath
231
 
                                     ("/Mandos/clients/"
232
 
                                      + self.name.replace(".", "_")))
233
 
            dbus.service.Object.__init__(self, bus,
234
 
                                         self.dbus_object_path)
235
231
        # Uppercase and remove spaces from fingerprint for later
236
232
        # comparison purposes with return value from the fingerprint()
237
233
        # function
261
257
        self.disable_initiator_tag = None
262
258
        self.checker_callback_tag = None
263
259
        self.checker_command = config["checker"]
 
260
        self.current_checker_command = None
 
261
        self.last_connect = None
264
262
    
265
263
    def enable(self):
266
264
        """Start this client's checker and timeout hooks"""
277
275
                                   (self.timeout_milliseconds(),
278
276
                                    self.disable))
279
277
        self.enabled = True
280
 
        if self.use_dbus:
281
 
            # Emit D-Bus signals
282
 
            self.PropertyChanged(dbus.String(u"enabled"),
283
 
                                 dbus.Boolean(True, variant_level=1))
284
 
            self.PropertyChanged(dbus.String(u"last_enabled"),
285
 
                                 (_datetime_to_dbus(self.last_enabled,
286
 
                                                    variant_level=1)))
287
278
    
288
279
    def disable(self):
289
280
        """Disable this client."""
300
291
        if self.disable_hook:
301
292
            self.disable_hook(self)
302
293
        self.enabled = False
303
 
        if self.use_dbus:
304
 
            # Emit D-Bus signal
305
 
            self.PropertyChanged(dbus.String(u"enabled"),
306
 
                                 dbus.Boolean(False, variant_level=1))
307
294
        # Do not run this again if called by a gobject.timeout_add
308
295
        return False
309
296
    
315
302
        """The checker has completed, so take appropriate actions."""
316
303
        self.checker_callback_tag = None
317
304
        self.checker = None
318
 
        if self.use_dbus:
319
 
            # Emit D-Bus signal
320
 
            self.PropertyChanged(dbus.String(u"checker_running"),
321
 
                                 dbus.Boolean(False, variant_level=1))
322
 
        if (os.WIFEXITED(condition)
323
 
            and (os.WEXITSTATUS(condition) == 0)):
324
 
            logger.info(u"Checker for %(name)s succeeded",
325
 
                        vars(self))
326
 
            if self.use_dbus:
327
 
                # Emit D-Bus signal
328
 
                self.CheckerCompleted(dbus.Boolean(True),
329
 
                                      dbus.UInt16(condition),
330
 
                                      dbus.String(command))
331
 
            self.bump_timeout()
332
 
        elif not os.WIFEXITED(condition):
 
305
        if os.WIFEXITED(condition):
 
306
            exitstatus = os.WEXITSTATUS(condition)
 
307
            if exitstatus == 0:
 
308
                logger.info(u"Checker for %(name)s succeeded",
 
309
                            vars(self))
 
310
                self.checked_ok()
 
311
            else:
 
312
                logger.info(u"Checker for %(name)s failed",
 
313
                            vars(self))
 
314
        else:
333
315
            logger.warning(u"Checker for %(name)s crashed?",
334
316
                           vars(self))
335
 
            if self.use_dbus:
336
 
                # Emit D-Bus signal
337
 
                self.CheckerCompleted(dbus.Boolean(False),
338
 
                                      dbus.UInt16(condition),
339
 
                                      dbus.String(command))
340
 
        else:
341
 
            logger.info(u"Checker for %(name)s failed",
342
 
                        vars(self))
343
 
            if self.use_dbus:
344
 
                # Emit D-Bus signal
345
 
                self.CheckerCompleted(dbus.Boolean(False),
346
 
                                      dbus.UInt16(condition),
347
 
                                      dbus.String(command))
348
317
    
349
 
    def bump_timeout(self):
 
318
    def checked_ok(self):
350
319
        """Bump up the timeout for this client.
351
320
        This should only be called when the client has been seen,
352
321
        alive and well.
356
325
        self.disable_initiator_tag = (gobject.timeout_add
357
326
                                      (self.timeout_milliseconds(),
358
327
                                       self.disable))
359
 
        if self.use_dbus:
360
 
            # Emit D-Bus signal
361
 
            self.PropertyChanged(
362
 
                dbus.String(u"last_checked_ok"),
363
 
                (_datetime_to_dbus(self.last_checked_ok,
364
 
                                   variant_level=1)))
365
328
    
366
329
    def start_checker(self):
367
330
        """Start a new checker subprocess if one is not running.
375
338
        # checkers alone, the checker would have to take more time
376
339
        # than 'timeout' for the client to be declared invalid, which
377
340
        # is as it should be.
 
341
        
 
342
        # If a checker exists, make sure it is not a zombie
 
343
        if self.checker is not None:
 
344
            pid, status = os.waitpid(self.checker.pid, os.WNOHANG)
 
345
            if pid:
 
346
                logger.warning("Checker was a zombie")
 
347
                gobject.source_remove(self.checker_callback_tag)
 
348
                self.checker_callback(pid, status,
 
349
                                      self.current_checker_command)
 
350
        # Start a new checker if needed
378
351
        if self.checker is None:
379
352
            try:
380
353
                # In case checker_command has exactly one % operator
390
363
                    logger.error(u'Could not format string "%s":'
391
364
                                 u' %s', self.checker_command, error)
392
365
                    return True # Try again later
 
366
            self.current_checker_command = command
393
367
            try:
394
368
                logger.info(u"Starting checker %r for %s",
395
369
                            command, self.name)
400
374
                self.checker = subprocess.Popen(command,
401
375
                                                close_fds=True,
402
376
                                                shell=True, cwd="/")
403
 
                if self.use_dbus:
404
 
                    # Emit D-Bus signal
405
 
                    self.CheckerStarted(command)
406
 
                    self.PropertyChanged(
407
 
                        dbus.String("checker_running"),
408
 
                        dbus.Boolean(True, variant_level=1))
409
377
                self.checker_callback_tag = (gobject.child_watch_add
410
378
                                             (self.checker.pid,
411
379
                                              self.checker_callback,
412
380
                                              data=command))
 
381
                # The checker may have completed before the gobject
 
382
                # watch was added.  Check for this.
 
383
                pid, status = os.waitpid(self.checker.pid, os.WNOHANG)
 
384
                if pid:
 
385
                    gobject.source_remove(self.checker_callback_tag)
 
386
                    self.checker_callback(pid, status, command)
413
387
            except OSError, error:
414
388
                logger.error(u"Failed to start subprocess: %s",
415
389
                             error)
433
407
            if error.errno != errno.ESRCH: # No such process
434
408
                raise
435
409
        self.checker = None
436
 
        if self.use_dbus:
437
 
            self.PropertyChanged(dbus.String(u"checker_running"),
438
 
                                 dbus.Boolean(False, variant_level=1))
439
410
    
440
411
    def still_valid(self):
441
412
        """Has the timeout not yet passed for this client?"""
446
417
            return now < (self.created + self.timeout)
447
418
        else:
448
419
            return now < (self.last_checked_ok + self.timeout)
 
420
 
 
421
 
 
422
class ClientDBus(Client, dbus.service.Object):
 
423
    """A Client class using D-Bus
 
424
    Attributes:
 
425
    dbus_object_path: dbus.ObjectPath ; only set if self.use_dbus
 
426
    """
 
427
    # dbus.service.Object doesn't use super(), so we can't either.
 
428
    
 
429
    def __init__(self, *args, **kwargs):
 
430
        Client.__init__(self, *args, **kwargs)
 
431
        # Only now, when this client is initialized, can it show up on
 
432
        # the D-Bus
 
433
        self.dbus_object_path = (dbus.ObjectPath
 
434
                                 ("/clients/"
 
435
                                  + self.name.replace(".", "_")))
 
436
        dbus.service.Object.__init__(self, bus,
 
437
                                     self.dbus_object_path)
 
438
    def enable(self):
 
439
        oldstate = getattr(self, "enabled", False)
 
440
        r = Client.enable(self)
 
441
        if oldstate != self.enabled:
 
442
            # Emit D-Bus signals
 
443
            self.PropertyChanged(dbus.String(u"enabled"),
 
444
                                 dbus.Boolean(True, variant_level=1))
 
445
            self.PropertyChanged(dbus.String(u"last_enabled"),
 
446
                                 (_datetime_to_dbus(self.last_enabled,
 
447
                                                    variant_level=1)))
 
448
        return r
 
449
    
 
450
    def disable(self, signal = True):
 
451
        oldstate = getattr(self, "enabled", False)
 
452
        r = Client.disable(self)
 
453
        if signal and oldstate != self.enabled:
 
454
            # Emit D-Bus signal
 
455
            self.PropertyChanged(dbus.String(u"enabled"),
 
456
                                 dbus.Boolean(False, variant_level=1))
 
457
        return r
 
458
    
 
459
    def __del__(self, *args, **kwargs):
 
460
        try:
 
461
            self.remove_from_connection()
 
462
        except org.freedesktop.DBus.Python.LookupError:
 
463
            pass
 
464
        dbus.service.Object.__del__(self, *args, **kwargs)
 
465
        Client.__del__(self, *args, **kwargs)
 
466
    
 
467
    def checker_callback(self, pid, condition, command,
 
468
                         *args, **kwargs):
 
469
        self.checker_callback_tag = None
 
470
        self.checker = None
 
471
        # Emit D-Bus signal
 
472
        self.PropertyChanged(dbus.String(u"checker_running"),
 
473
                             dbus.Boolean(False, variant_level=1))
 
474
        if os.WIFEXITED(condition):
 
475
            exitstatus = os.WEXITSTATUS(condition)
 
476
            # Emit D-Bus signal
 
477
            self.CheckerCompleted(dbus.Int16(exitstatus),
 
478
                                  dbus.Int64(condition),
 
479
                                  dbus.String(command))
 
480
        else:
 
481
            # Emit D-Bus signal
 
482
            self.CheckerCompleted(dbus.Int16(-1),
 
483
                                  dbus.Int64(condition),
 
484
                                  dbus.String(command))
 
485
        
 
486
        return Client.checker_callback(self, pid, condition, command,
 
487
                                       *args, **kwargs)
 
488
    
 
489
    def checked_ok(self, *args, **kwargs):
 
490
        r = Client.checked_ok(self, *args, **kwargs)
 
491
        # Emit D-Bus signal
 
492
        self.PropertyChanged(
 
493
            dbus.String(u"last_checked_ok"),
 
494
            (_datetime_to_dbus(self.last_checked_ok,
 
495
                               variant_level=1)))
 
496
        return r
 
497
    
 
498
    def start_checker(self, *args, **kwargs):
 
499
        old_checker = self.checker
 
500
        if self.checker is not None:
 
501
            old_checker_pid = self.checker.pid
 
502
        else:
 
503
            old_checker_pid = None
 
504
        r = Client.start_checker(self, *args, **kwargs)
 
505
        # Only emit D-Bus signal if new checker process was started
 
506
        if ((self.checker is not None)
 
507
            and not (old_checker is not None
 
508
                     and old_checker_pid == self.checker.pid)):
 
509
            self.CheckerStarted(self.current_checker_command)
 
510
            self.PropertyChanged(
 
511
                dbus.String("checker_running"),
 
512
                dbus.Boolean(True, variant_level=1))
 
513
        return r
 
514
    
 
515
    def stop_checker(self, *args, **kwargs):
 
516
        old_checker = getattr(self, "checker", None)
 
517
        r = Client.stop_checker(self, *args, **kwargs)
 
518
        if (old_checker is not None
 
519
            and getattr(self, "checker", None) is None):
 
520
            self.PropertyChanged(dbus.String(u"checker_running"),
 
521
                                 dbus.Boolean(False, variant_level=1))
 
522
        return r
449
523
    
450
524
    ## D-Bus methods & signals
451
 
    _interface = u"org.mandos_system.Mandos.Client"
 
525
    _interface = u"se.bsnet.fukt.Mandos.Client"
452
526
    
453
 
    # BumpTimeout - method
454
 
    BumpTimeout = dbus.service.method(_interface)(bump_timeout)
455
 
    BumpTimeout.__name__ = "BumpTimeout"
 
527
    # CheckedOK - method
 
528
    CheckedOK = dbus.service.method(_interface)(checked_ok)
 
529
    CheckedOK.__name__ = "CheckedOK"
456
530
    
457
531
    # CheckerCompleted - signal
458
 
    @dbus.service.signal(_interface, signature="bqs")
459
 
    def CheckerCompleted(self, success, condition, command):
 
532
    @dbus.service.signal(_interface, signature="nxs")
 
533
    def CheckerCompleted(self, exitcode, waitstatus, command):
460
534
        "D-Bus signal"
461
535
        pass
462
536
    
503
577
                dbus.String("checker_running"):
504
578
                    dbus.Boolean(self.checker is not None,
505
579
                                 variant_level=1),
 
580
                dbus.String("object_path"):
 
581
                    dbus.ObjectPath(self.dbus_object_path,
 
582
                                    variant_level=1)
506
583
                }, signature="sv")
507
584
    
508
585
    # IsStillValid - method
509
 
    IsStillValid = (dbus.service.method(_interface, out_signature="b")
510
 
                    (still_valid))
511
 
    IsStillValid.__name__ = "IsStillValid"
 
586
    @dbus.service.method(_interface, out_signature="b")
 
587
    def IsStillValid(self):
 
588
        return self.still_valid()
512
589
    
513
590
    # PropertyChanged - signal
514
591
    @dbus.service.signal(_interface, signature="sv")
516
593
        "D-Bus signal"
517
594
        pass
518
595
    
 
596
    # ReceivedSecret - signal
 
597
    @dbus.service.signal(_interface)
 
598
    def ReceivedSecret(self):
 
599
        "D-Bus signal"
 
600
        pass
 
601
    
 
602
    # Rejected - signal
 
603
    @dbus.service.signal(_interface)
 
604
    def Rejected(self):
 
605
        "D-Bus signal"
 
606
        pass
 
607
    
519
608
    # SetChecker - method
520
609
    @dbus.service.method(_interface, in_signature="s")
521
610
    def SetChecker(self, checker):
591
680
        != gnutls.library.constants.GNUTLS_CRT_OPENPGP):
592
681
        # ...do the normal thing
593
682
        return session.peer_certificate
594
 
    list_size = ctypes.c_uint()
 
683
    list_size = ctypes.c_uint(1)
595
684
    cert_list = (gnutls.library.functions
596
685
                 .gnutls_certificate_get_peers
597
686
                 (session._c_object, ctypes.byref(list_size)))
 
687
    if not bool(cert_list) and list_size.value != 0:
 
688
        raise gnutls.errors.GNUTLSError("error getting peer"
 
689
                                        " certificate")
598
690
    if list_size.value == 0:
599
691
        return None
600
692
    cert = cert_list[0]
649
741
    def handle(self):
650
742
        logger.info(u"TCP connection from: %s",
651
743
                    unicode(self.client_address))
652
 
        session = (gnutls.connection
653
 
                   .ClientSession(self.request,
654
 
                                  gnutls.connection
655
 
                                  .X509Credentials()))
656
 
        
657
 
        line = self.request.makefile().readline()
658
 
        logger.debug(u"Protocol version: %r", line)
659
 
        try:
660
 
            if int(line.strip().split()[0]) > 1:
661
 
                raise RuntimeError
662
 
        except (ValueError, IndexError, RuntimeError), error:
663
 
            logger.error(u"Unknown protocol version: %s", error)
664
 
            return
665
 
        
666
 
        # Note: gnutls.connection.X509Credentials is really a generic
667
 
        # GnuTLS certificate credentials object so long as no X.509
668
 
        # keys are added to it.  Therefore, we can use it here despite
669
 
        # using OpenPGP certificates.
670
 
        
671
 
        #priority = ':'.join(("NONE", "+VERS-TLS1.1", "+AES-256-CBC",
672
 
        #                "+SHA1", "+COMP-NULL", "+CTYPE-OPENPGP",
673
 
        #                "+DHE-DSS"))
674
 
        # Use a fallback default, since this MUST be set.
675
 
        priority = self.server.settings.get("priority", "NORMAL")
676
 
        (gnutls.library.functions
677
 
         .gnutls_priority_set_direct(session._c_object,
678
 
                                     priority, None))
679
 
        
680
 
        try:
681
 
            session.handshake()
682
 
        except gnutls.errors.GNUTLSError, error:
683
 
            logger.warning(u"Handshake failed: %s", error)
684
 
            # Do not run session.bye() here: the session is not
685
 
            # established.  Just abandon the request.
686
 
            return
687
 
        try:
688
 
            fpr = fingerprint(peer_certificate(session))
689
 
        except (TypeError, gnutls.errors.GNUTLSError), error:
690
 
            logger.warning(u"Bad certificate: %s", error)
691
 
            session.bye()
692
 
            return
693
 
        logger.debug(u"Fingerprint: %s", fpr)
694
 
        for c in self.server.clients:
695
 
            if c.fingerprint == fpr:
696
 
                client = c
697
 
                break
698
 
        else:
699
 
            logger.warning(u"Client not found for fingerprint: %s",
700
 
                           fpr)
701
 
            session.bye()
702
 
            return
703
 
        # Have to check if client.still_valid(), since it is possible
704
 
        # that the client timed out while establishing the GnuTLS
705
 
        # session.
706
 
        if not client.still_valid():
707
 
            logger.warning(u"Client %(name)s is invalid",
708
 
                           vars(client))
709
 
            session.bye()
710
 
            return
711
 
        ## This won't work here, since we're in a fork.
712
 
        # client.bump_timeout()
713
 
        sent_size = 0
714
 
        while sent_size < len(client.secret):
715
 
            sent = session.send(client.secret[sent_size:])
716
 
            logger.debug(u"Sent: %d, remaining: %d",
717
 
                         sent, len(client.secret)
718
 
                         - (sent_size + sent))
719
 
            sent_size += sent
720
 
        session.bye()
721
 
 
722
 
 
723
 
class IPv6_TCPServer(SocketServer.ForkingMixIn,
 
744
        logger.debug(u"IPC Pipe FD: %d", self.server.pipe[1])
 
745
        # Open IPC pipe to parent process
 
746
        with closing(os.fdopen(self.server.pipe[1], "w", 1)) as ipc:
 
747
            session = (gnutls.connection
 
748
                       .ClientSession(self.request,
 
749
                                      gnutls.connection
 
750
                                      .X509Credentials()))
 
751
            
 
752
            line = self.request.makefile().readline()
 
753
            logger.debug(u"Protocol version: %r", line)
 
754
            try:
 
755
                if int(line.strip().split()[0]) > 1:
 
756
                    raise RuntimeError
 
757
            except (ValueError, IndexError, RuntimeError), error:
 
758
                logger.error(u"Unknown protocol version: %s", error)
 
759
                return
 
760
            
 
761
            # Note: gnutls.connection.X509Credentials is really a
 
762
            # generic GnuTLS certificate credentials object so long as
 
763
            # no X.509 keys are added to it.  Therefore, we can use it
 
764
            # here despite using OpenPGP certificates.
 
765
            
 
766
            #priority = ':'.join(("NONE", "+VERS-TLS1.1",
 
767
            #                     "+AES-256-CBC", "+SHA1",
 
768
            #                     "+COMP-NULL", "+CTYPE-OPENPGP",
 
769
            #                     "+DHE-DSS"))
 
770
            # Use a fallback default, since this MUST be set.
 
771
            priority = self.server.settings.get("priority", "NORMAL")
 
772
            (gnutls.library.functions
 
773
             .gnutls_priority_set_direct(session._c_object,
 
774
                                         priority, None))
 
775
            
 
776
            try:
 
777
                session.handshake()
 
778
            except gnutls.errors.GNUTLSError, error:
 
779
                logger.warning(u"Handshake failed: %s", error)
 
780
                # Do not run session.bye() here: the session is not
 
781
                # established.  Just abandon the request.
 
782
                return
 
783
            logger.debug(u"Handshake succeeded")
 
784
            try:
 
785
                fpr = fingerprint(peer_certificate(session))
 
786
            except (TypeError, gnutls.errors.GNUTLSError), error:
 
787
                logger.warning(u"Bad certificate: %s", error)
 
788
                session.bye()
 
789
                return
 
790
            logger.debug(u"Fingerprint: %s", fpr)
 
791
            
 
792
            for c in self.server.clients:
 
793
                if c.fingerprint == fpr:
 
794
                    client = c
 
795
                    break
 
796
            else:
 
797
                logger.warning(u"Client not found for fingerprint: %s",
 
798
                               fpr)
 
799
                ipc.write("NOTFOUND %s\n" % fpr)
 
800
                session.bye()
 
801
                return
 
802
            # Have to check if client.still_valid(), since it is
 
803
            # possible that the client timed out while establishing
 
804
            # the GnuTLS session.
 
805
            if not client.still_valid():
 
806
                logger.warning(u"Client %(name)s is invalid",
 
807
                               vars(client))
 
808
                ipc.write("INVALID %s\n" % client.name)
 
809
                session.bye()
 
810
                return
 
811
            ipc.write("SENDING %s\n" % client.name)
 
812
            sent_size = 0
 
813
            while sent_size < len(client.secret):
 
814
                sent = session.send(client.secret[sent_size:])
 
815
                logger.debug(u"Sent: %d, remaining: %d",
 
816
                             sent, len(client.secret)
 
817
                             - (sent_size + sent))
 
818
                sent_size += sent
 
819
            session.bye()
 
820
 
 
821
 
 
822
class ForkingMixInWithPipe(SocketServer.ForkingMixIn, object):
 
823
    """Like SocketServer.ForkingMixIn, but also pass a pipe.
 
824
    Assumes a gobject.MainLoop event loop.
 
825
    """
 
826
    def process_request(self, request, client_address):
 
827
        """This overrides and wraps the original process_request().
 
828
        This function creates a new pipe in self.pipe 
 
829
        """
 
830
        self.pipe = os.pipe()
 
831
        super(ForkingMixInWithPipe,
 
832
              self).process_request(request, client_address)
 
833
        os.close(self.pipe[1])  # close write end
 
834
        # Call "handle_ipc" for both data and EOF events
 
835
        gobject.io_add_watch(self.pipe[0],
 
836
                             gobject.IO_IN | gobject.IO_HUP,
 
837
                             self.handle_ipc)
 
838
    def handle_ipc(source, condition):
 
839
        """Dummy function; override as necessary"""
 
840
        os.close(source)
 
841
        return False
 
842
 
 
843
 
 
844
class IPv6_TCPServer(ForkingMixInWithPipe,
724
845
                     SocketServer.TCPServer, object):
725
 
    """IPv6 TCP server.  Accepts 'None' as address and/or port.
 
846
    """IPv6-capable TCP server.  Accepts 'None' as address and/or port
726
847
    Attributes:
727
848
        settings:       Server settings
728
849
        clients:        Set() of Client objects
736
857
        if "clients" in kwargs:
737
858
            self.clients = kwargs["clients"]
738
859
            del kwargs["clients"]
 
860
        if "use_ipv6" in kwargs:
 
861
            if not kwargs["use_ipv6"]:
 
862
                self.address_family = socket.AF_INET
 
863
            del kwargs["use_ipv6"]
739
864
        self.enabled = False
740
865
        super(IPv6_TCPServer, self).__init__(*args, **kwargs)
741
866
    def server_bind(self):
755
880
                                 u" bind to interface %s",
756
881
                                 self.settings["interface"])
757
882
                else:
758
 
                    raise error
 
883
                    raise
759
884
        # Only bind(2) the socket if we really need to.
760
885
        if self.server_address[0] or self.server_address[1]:
761
886
            if not self.server_address[0]:
762
 
                in6addr_any = "::"
763
 
                self.server_address = (in6addr_any,
 
887
                if self.address_family == socket.AF_INET6:
 
888
                    any_address = "::" # in6addr_any
 
889
                else:
 
890
                    any_address = socket.INADDR_ANY
 
891
                self.server_address = (any_address,
764
892
                                       self.server_address[1])
765
893
            elif not self.server_address[1]:
766
894
                self.server_address = (self.server_address[0],
778
906
            return super(IPv6_TCPServer, self).server_activate()
779
907
    def enable(self):
780
908
        self.enabled = True
 
909
    def handle_ipc(self, source, condition, file_objects={}):
 
910
        condition_names = {
 
911
            gobject.IO_IN: "IN", # There is data to read.
 
912
            gobject.IO_OUT: "OUT", # Data can be written (without
 
913
                                   # blocking).
 
914
            gobject.IO_PRI: "PRI", # There is urgent data to read.
 
915
            gobject.IO_ERR: "ERR", # Error condition.
 
916
            gobject.IO_HUP: "HUP"  # Hung up (the connection has been
 
917
                                   # broken, usually for pipes and
 
918
                                   # sockets).
 
919
            }
 
920
        conditions_string = ' | '.join(name
 
921
                                       for cond, name in
 
922
                                       condition_names.iteritems()
 
923
                                       if cond & condition)
 
924
        logger.debug("Handling IPC: FD = %d, condition = %s", source,
 
925
                     conditions_string)
 
926
        
 
927
        # Turn the pipe file descriptor into a Python file object
 
928
        if source not in file_objects:
 
929
            file_objects[source] = os.fdopen(source, "r", 1)
 
930
        
 
931
        # Read a line from the file object
 
932
        cmdline = file_objects[source].readline()
 
933
        if not cmdline:             # Empty line means end of file
 
934
            # close the IPC pipe
 
935
            file_objects[source].close()
 
936
            del file_objects[source]
 
937
            
 
938
            # Stop calling this function
 
939
            return False
 
940
        
 
941
        logger.debug("IPC command: %r\n" % cmdline)
 
942
        
 
943
        # Parse and act on command
 
944
        cmd, args = cmdline.split(None, 1)
 
945
        if cmd == "NOTFOUND":
 
946
            if self.settings["use_dbus"]:
 
947
                # Emit D-Bus signal
 
948
                mandos_dbus_service.ClientNotFound(args)
 
949
        elif cmd == "INVALID":
 
950
            if self.settings["use_dbus"]:
 
951
                for client in self.clients:
 
952
                    if client.name == args:
 
953
                        # Emit D-Bus signal
 
954
                        client.Rejected()
 
955
                        break
 
956
        elif cmd == "SENDING":
 
957
            for client in self.clients:
 
958
                if client.name == args:
 
959
                    client.checked_ok()
 
960
                    if self.settings["use_dbus"]:
 
961
                        # Emit D-Bus signal
 
962
                        client.ReceivedSecret()
 
963
                    break
 
964
        else:
 
965
            logger.error("Unknown IPC command: %r", cmdline)
 
966
        
 
967
        # Keep calling this function
 
968
        return True
781
969
 
782
970
 
783
971
def string_to_delta(interval):
784
972
    """Parse a string and return a datetime.timedelta
785
 
 
 
973
    
786
974
    >>> string_to_delta('7d')
787
975
    datetime.timedelta(7)
788
976
    >>> string_to_delta('60s')
889
1077
 
890
1078
 
891
1079
def main():
892
 
    parser = OptionParser(version = "%%prog %s" % version)
 
1080
    
 
1081
    ######################################################################
 
1082
    # Parsing of options, both command line and config file
 
1083
    
 
1084
    parser = optparse.OptionParser(version = "%%prog %s" % version)
893
1085
    parser.add_option("-i", "--interface", type="string",
894
1086
                      metavar="IF", help="Bind to interface IF")
895
1087
    parser.add_option("-a", "--address", type="string",
913
1105
                      dest="use_dbus",
914
1106
                      help="Do not provide D-Bus system bus"
915
1107
                      " interface")
 
1108
    parser.add_option("--no-ipv6", action="store_false",
 
1109
                      dest="use_ipv6", help="Do not use IPv6")
916
1110
    options = parser.parse_args()[0]
917
1111
    
918
1112
    if options.check:
929
1123
                        "SECURE256:!CTYPE-X.509:+CTYPE-OPENPGP",
930
1124
                        "servicename": "Mandos",
931
1125
                        "use_dbus": "True",
 
1126
                        "use_ipv6": "True",
932
1127
                        }
933
1128
    
934
1129
    # Parse config file for server-global settings
937
1132
    server_config.read(os.path.join(options.configdir, "mandos.conf"))
938
1133
    # Convert the SafeConfigParser object to a dict
939
1134
    server_settings = server_config.defaults()
940
 
    # Use getboolean on the boolean config options
941
 
    server_settings["debug"] = (server_config.getboolean
942
 
                                ("DEFAULT", "debug"))
943
 
    server_settings["use_dbus"] = (server_config.getboolean
944
 
                                   ("DEFAULT", "use_dbus"))
 
1135
    # Use the appropriate methods on the non-string config options
 
1136
    server_settings["debug"] = server_config.getboolean("DEFAULT",
 
1137
                                                        "debug")
 
1138
    server_settings["use_dbus"] = server_config.getboolean("DEFAULT",
 
1139
                                                           "use_dbus")
 
1140
    server_settings["use_ipv6"] = server_config.getboolean("DEFAULT",
 
1141
                                                           "use_ipv6")
 
1142
    if server_settings["port"]:
 
1143
        server_settings["port"] = server_config.getint("DEFAULT",
 
1144
                                                       "port")
945
1145
    del server_config
946
1146
    
947
1147
    # Override the settings from the config file with command line
948
1148
    # options, if set.
949
1149
    for option in ("interface", "address", "port", "debug",
950
1150
                   "priority", "servicename", "configdir",
951
 
                   "use_dbus"):
 
1151
                   "use_dbus", "use_ipv6"):
952
1152
        value = getattr(options, option)
953
1153
        if value is not None:
954
1154
            server_settings[option] = value
955
1155
    del options
956
1156
    # Now we have our good server settings in "server_settings"
957
1157
    
 
1158
    ##################################################################
 
1159
    
958
1160
    # For convenience
959
1161
    debug = server_settings["debug"]
960
1162
    use_dbus = server_settings["use_dbus"]
 
1163
    use_ipv6 = server_settings["use_ipv6"]
961
1164
    
962
1165
    if not debug:
963
1166
        syslogger.setLevel(logging.WARNING)
965
1168
    
966
1169
    if server_settings["servicename"] != "Mandos":
967
1170
        syslogger.setFormatter(logging.Formatter
968
 
                               ('Mandos (%s): %%(levelname)s:'
969
 
                                ' %%(message)s'
 
1171
                               ('Mandos (%s) [%%(process)d]:'
 
1172
                                ' %%(levelname)s: %%(message)s'
970
1173
                                % server_settings["servicename"]))
971
1174
    
972
1175
    # Parse config file with clients
973
1176
    client_defaults = { "timeout": "1h",
974
1177
                        "interval": "5m",
975
 
                        "checker": "fping -q -- %(host)s",
 
1178
                        "checker": "fping -q -- %%(host)s",
976
1179
                        "host": "",
977
1180
                        }
978
1181
    client_config = ConfigParser.SafeConfigParser(client_defaults)
979
1182
    client_config.read(os.path.join(server_settings["configdir"],
980
1183
                                    "clients.conf"))
 
1184
 
 
1185
    global mandos_dbus_service
 
1186
    mandos_dbus_service = None
981
1187
    
982
1188
    clients = Set()
983
1189
    tcp_server = IPv6_TCPServer((server_settings["address"],
984
1190
                                 server_settings["port"]),
985
1191
                                TCP_handler,
986
1192
                                settings=server_settings,
987
 
                                clients=clients)
 
1193
                                clients=clients, use_ipv6=use_ipv6)
988
1194
    pidfilename = "/var/run/mandos.pid"
989
1195
    try:
990
1196
        pidfile = open(pidfilename, "w")
991
 
    except IOError, error:
 
1197
    except IOError:
992
1198
        logger.error("Could not open file %r", pidfilename)
993
1199
    
994
1200
    try:
995
1201
        uid = pwd.getpwnam("_mandos").pw_uid
 
1202
        gid = pwd.getpwnam("_mandos").pw_gid
996
1203
    except KeyError:
997
1204
        try:
998
1205
            uid = pwd.getpwnam("mandos").pw_uid
 
1206
            gid = pwd.getpwnam("mandos").pw_gid
999
1207
        except KeyError:
1000
1208
            try:
1001
1209
                uid = pwd.getpwnam("nobody").pw_uid
 
1210
                gid = pwd.getpwnam("nogroup").pw_gid
1002
1211
            except KeyError:
1003
1212
                uid = 65534
1004
 
    try:
1005
 
        gid = pwd.getpwnam("_mandos").pw_gid
1006
 
    except KeyError:
1007
 
        try:
1008
 
            gid = pwd.getpwnam("mandos").pw_gid
1009
 
        except KeyError:
1010
 
            try:
1011
 
                gid = pwd.getpwnam("nogroup").pw_gid
1012
 
            except KeyError:
1013
1213
                gid = 65534
1014
1214
    try:
 
1215
        os.setgid(gid)
1015
1216
        os.setuid(uid)
1016
 
        os.setgid(gid)
1017
1217
    except OSError, error:
1018
1218
        if error[0] != errno.EPERM:
1019
1219
            raise error
1020
1220
    
 
1221
    # Enable all possible GnuTLS debugging
 
1222
    if debug:
 
1223
        # "Use a log level over 10 to enable all debugging options."
 
1224
        # - GnuTLS manual
 
1225
        gnutls.library.functions.gnutls_global_set_log_level(11)
 
1226
        
 
1227
        @gnutls.library.types.gnutls_log_func
 
1228
        def debug_gnutls(level, string):
 
1229
            logger.debug("GnuTLS: %s", string[:-1])
 
1230
        
 
1231
        (gnutls.library.functions
 
1232
         .gnutls_global_set_log_function(debug_gnutls))
 
1233
    
1021
1234
    global service
 
1235
    protocol = avahi.PROTO_INET6 if use_ipv6 else avahi.PROTO_INET
1022
1236
    service = AvahiService(name = server_settings["servicename"],
1023
 
                           servicetype = "_mandos._tcp", )
 
1237
                           servicetype = "_mandos._tcp",
 
1238
                           protocol = protocol)
1024
1239
    if server_settings["interface"]:
1025
1240
        service.interface = (if_nametoindex
1026
1241
                             (server_settings["interface"]))
1037
1252
                            avahi.DBUS_INTERFACE_SERVER)
1038
1253
    # End of Avahi example code
1039
1254
    if use_dbus:
1040
 
        bus_name = dbus.service.BusName(u"org.mandos-system.Mandos",
1041
 
                                        bus)
 
1255
        bus_name = dbus.service.BusName(u"se.bsnet.fukt.Mandos", bus)
1042
1256
    
1043
 
    clients.update(Set(Client(name = section,
1044
 
                              config
1045
 
                              = dict(client_config.items(section)),
1046
 
                              use_dbus = use_dbus)
1047
 
                       for section in client_config.sections()))
 
1257
    client_class = Client
 
1258
    if use_dbus:
 
1259
        client_class = ClientDBus
 
1260
    clients.update(Set(
 
1261
            client_class(name = section,
 
1262
                         config= dict(client_config.items(section)))
 
1263
            for section in client_config.sections()))
1048
1264
    if not clients:
1049
 
        logger.critical(u"No clients defined")
1050
 
        sys.exit(1)
 
1265
        logger.warning(u"No clients defined")
1051
1266
    
1052
1267
    if debug:
1053
1268
        # Redirect stdin so all checkers get /dev/null
1062
1277
        daemon()
1063
1278
    
1064
1279
    try:
1065
 
        pid = os.getpid()
1066
 
        pidfile.write(str(pid) + "\n")
1067
 
        pidfile.close()
 
1280
        with closing(pidfile):
 
1281
            pid = os.getpid()
 
1282
            pidfile.write(str(pid) + "\n")
1068
1283
        del pidfile
1069
1284
    except IOError:
1070
1285
        logger.error(u"Could not write to file %r with PID %d",
1096
1311
    signal.signal(signal.SIGTERM, lambda signum, frame: sys.exit())
1097
1312
    
1098
1313
    if use_dbus:
1099
 
        class MandosServer(dbus.service.Object):
 
1314
        class MandosDBusService(dbus.service.Object):
1100
1315
            """A D-Bus proxy object"""
1101
1316
            def __init__(self):
1102
 
                dbus.service.Object.__init__(self, bus,
1103
 
                                             "/Mandos")
1104
 
            _interface = u"org.mandos_system.Mandos"
1105
 
 
 
1317
                dbus.service.Object.__init__(self, bus, "/")
 
1318
            _interface = u"se.bsnet.fukt.Mandos"
 
1319
            
1106
1320
            @dbus.service.signal(_interface, signature="oa{sv}")
1107
1321
            def ClientAdded(self, objpath, properties):
1108
1322
                "D-Bus signal"
1109
1323
                pass
1110
 
 
1111
 
            @dbus.service.signal(_interface, signature="o")
1112
 
            def ClientRemoved(self, objpath):
1113
 
                "D-Bus signal"
1114
 
                pass
1115
 
 
 
1324
            
 
1325
            @dbus.service.signal(_interface, signature="s")
 
1326
            def ClientNotFound(self, fingerprint):
 
1327
                "D-Bus signal"
 
1328
                pass
 
1329
            
 
1330
            @dbus.service.signal(_interface, signature="os")
 
1331
            def ClientRemoved(self, objpath, name):
 
1332
                "D-Bus signal"
 
1333
                pass
 
1334
            
1116
1335
            @dbus.service.method(_interface, out_signature="ao")
1117
1336
            def GetAllClients(self):
 
1337
                "D-Bus method"
1118
1338
                return dbus.Array(c.dbus_object_path for c in clients)
1119
 
 
 
1339
            
1120
1340
            @dbus.service.method(_interface, out_signature="a{oa{sv}}")
1121
1341
            def GetAllClientsWithProperties(self):
 
1342
                "D-Bus method"
1122
1343
                return dbus.Dictionary(
1123
1344
                    ((c.dbus_object_path, c.GetAllProperties())
1124
1345
                     for c in clients),
1125
1346
                    signature="oa{sv}")
1126
 
 
 
1347
            
1127
1348
            @dbus.service.method(_interface, in_signature="o")
1128
1349
            def RemoveClient(self, object_path):
 
1350
                "D-Bus method"
1129
1351
                for c in clients:
1130
1352
                    if c.dbus_object_path == object_path:
1131
1353
                        clients.remove(c)
 
1354
                        c.remove_from_connection()
1132
1355
                        # Don't signal anything except ClientRemoved
1133
 
                        c.use_dbus = False
1134
 
                        c.disable()
 
1356
                        c.disable(signal=False)
1135
1357
                        # Emit D-Bus signal
1136
 
                        self.ClientRemoved(object_path)
 
1358
                        self.ClientRemoved(object_path, c.name)
1137
1359
                        return
1138
1360
                raise KeyError
1139
 
 
 
1361
            
1140
1362
            del _interface
1141
 
    
1142
 
        mandos_server = MandosServer()
 
1363
        
 
1364
        mandos_dbus_service = MandosDBusService()
1143
1365
    
1144
1366
    for client in clients:
1145
1367
        if use_dbus:
1146
1368
            # Emit D-Bus signal
1147
 
            mandos_server.ClientAdded(client.dbus_object_path,
1148
 
                                      client.GetAllProperties())
 
1369
            mandos_dbus_service.ClientAdded(client.dbus_object_path,
 
1370
                                            client.GetAllProperties())
1149
1371
        client.enable()
1150
1372
    
1151
1373
    tcp_server.enable()
1153
1375
    
1154
1376
    # Find out what port we got
1155
1377
    service.port = tcp_server.socket.getsockname()[1]
1156
 
    logger.info(u"Now listening on address %r, port %d, flowinfo %d,"
1157
 
                u" scope_id %d" % tcp_server.socket.getsockname())
 
1378
    if use_ipv6:
 
1379
        logger.info(u"Now listening on address %r, port %d,"
 
1380
                    " flowinfo %d, scope_id %d"
 
1381
                    % tcp_server.socket.getsockname())
 
1382
    else:                       # IPv4
 
1383
        logger.info(u"Now listening on address %r, port %d"
 
1384
                    % tcp_server.socket.getsockname())
1158
1385
    
1159
1386
    #service.interface = tcp_server.socket.getsockname()[3]
1160
1387
    
1180
1407
        sys.exit(1)
1181
1408
    except KeyboardInterrupt:
1182
1409
        if debug:
1183
 
            print
 
1410
            print >> sys.stderr
 
1411
        logger.debug("Server received KeyboardInterrupt")
 
1412
    logger.debug("Server exiting")
1184
1413
 
1185
1414
if __name__ == '__main__':
1186
1415
    main()