/mandos/trunk

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

« back to all changes in this revision

Viewing changes to mandos

  • Committer: Teddy Hogeborn
  • Date: 2009-04-01 03:37:45 UTC
  • Revision ID: teddy@fukt.bsnet.se-20090401033745-c89k6bij5opdm1rk
* mandos (ClientDBus.__del__): Bug fix: Correct mispasted code, and do
                               not try to call
                               dbus.service.Object.__del__() if it
                               does not exist.
  (ClientDBus.start_checker): Simplify logic.

Show diffs side-by-side

added added

removed removed

Lines of Context:
66
66
import ctypes
67
67
import ctypes.util
68
68
 
69
 
version = "1.0.4"
 
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 LookupError:
 
463
            pass
 
464
        if hasattr(dbus.service.Object, "__del__"):
 
465
            dbus.service.Object.__del__(self, *args, **kwargs)
 
466
        Client.__del__(self, *args, **kwargs)
 
467
    
 
468
    def checker_callback(self, pid, condition, command,
 
469
                         *args, **kwargs):
 
470
        self.checker_callback_tag = None
 
471
        self.checker = None
 
472
        # Emit D-Bus signal
 
473
        self.PropertyChanged(dbus.String(u"checker_running"),
 
474
                             dbus.Boolean(False, variant_level=1))
 
475
        if os.WIFEXITED(condition):
 
476
            exitstatus = os.WEXITSTATUS(condition)
 
477
            # Emit D-Bus signal
 
478
            self.CheckerCompleted(dbus.Int16(exitstatus),
 
479
                                  dbus.Int64(condition),
 
480
                                  dbus.String(command))
 
481
        else:
 
482
            # Emit D-Bus signal
 
483
            self.CheckerCompleted(dbus.Int16(-1),
 
484
                                  dbus.Int64(condition),
 
485
                                  dbus.String(command))
 
486
        
 
487
        return Client.checker_callback(self, pid, condition, command,
 
488
                                       *args, **kwargs)
 
489
    
 
490
    def checked_ok(self, *args, **kwargs):
 
491
        r = Client.checked_ok(self, *args, **kwargs)
 
492
        # Emit D-Bus signal
 
493
        self.PropertyChanged(
 
494
            dbus.String(u"last_checked_ok"),
 
495
            (_datetime_to_dbus(self.last_checked_ok,
 
496
                               variant_level=1)))
 
497
        return r
 
498
    
 
499
    def start_checker(self, *args, **kwargs):
 
500
        old_checker = self.checker
 
501
        if self.checker is not None:
 
502
            old_checker_pid = self.checker.pid
 
503
        else:
 
504
            old_checker_pid = None
 
505
        r = Client.start_checker(self, *args, **kwargs)
 
506
        # Only if new checker process was started
 
507
        if (self.checker is not None
 
508
            and old_checker_pid != self.checker.pid):
 
509
            # Emit D-Bus signal
 
510
            self.CheckerStarted(self.current_checker_command)
 
511
            self.PropertyChanged(
 
512
                dbus.String("checker_running"),
 
513
                dbus.Boolean(True, variant_level=1))
 
514
        return r
 
515
    
 
516
    def stop_checker(self, *args, **kwargs):
 
517
        old_checker = getattr(self, "checker", None)
 
518
        r = Client.stop_checker(self, *args, **kwargs)
 
519
        if (old_checker is not None
 
520
            and getattr(self, "checker", None) is None):
 
521
            self.PropertyChanged(dbus.String(u"checker_running"),
 
522
                                 dbus.Boolean(False, variant_level=1))
 
523
        return r
449
524
    
450
525
    ## D-Bus methods & signals
451
 
    _interface = u"org.mandos_system.Mandos.Client"
 
526
    _interface = u"se.bsnet.fukt.Mandos.Client"
452
527
    
453
 
    # BumpTimeout - method
454
 
    BumpTimeout = dbus.service.method(_interface)(bump_timeout)
455
 
    BumpTimeout.__name__ = "BumpTimeout"
 
528
    # CheckedOK - method
 
529
    CheckedOK = dbus.service.method(_interface)(checked_ok)
 
530
    CheckedOK.__name__ = "CheckedOK"
456
531
    
457
532
    # CheckerCompleted - signal
458
 
    @dbus.service.signal(_interface, signature="bqs")
459
 
    def CheckerCompleted(self, success, condition, command):
 
533
    @dbus.service.signal(_interface, signature="nxs")
 
534
    def CheckerCompleted(self, exitcode, waitstatus, command):
460
535
        "D-Bus signal"
461
536
        pass
462
537
    
503
578
                dbus.String("checker_running"):
504
579
                    dbus.Boolean(self.checker is not None,
505
580
                                 variant_level=1),
 
581
                dbus.String("object_path"):
 
582
                    dbus.ObjectPath(self.dbus_object_path,
 
583
                                    variant_level=1)
506
584
                }, signature="sv")
507
585
    
508
586
    # IsStillValid - method
509
 
    IsStillValid = (dbus.service.method(_interface, out_signature="b")
510
 
                    (still_valid))
511
 
    IsStillValid.__name__ = "IsStillValid"
 
587
    @dbus.service.method(_interface, out_signature="b")
 
588
    def IsStillValid(self):
 
589
        return self.still_valid()
512
590
    
513
591
    # PropertyChanged - signal
514
592
    @dbus.service.signal(_interface, signature="sv")
516
594
        "D-Bus signal"
517
595
        pass
518
596
    
 
597
    # ReceivedSecret - signal
 
598
    @dbus.service.signal(_interface)
 
599
    def ReceivedSecret(self):
 
600
        "D-Bus signal"
 
601
        pass
 
602
    
 
603
    # Rejected - signal
 
604
    @dbus.service.signal(_interface)
 
605
    def Rejected(self):
 
606
        "D-Bus signal"
 
607
        pass
 
608
    
519
609
    # SetChecker - method
520
610
    @dbus.service.method(_interface, in_signature="s")
521
611
    def SetChecker(self, checker):
591
681
        != gnutls.library.constants.GNUTLS_CRT_OPENPGP):
592
682
        # ...do the normal thing
593
683
        return session.peer_certificate
594
 
    list_size = ctypes.c_uint()
 
684
    list_size = ctypes.c_uint(1)
595
685
    cert_list = (gnutls.library.functions
596
686
                 .gnutls_certificate_get_peers
597
687
                 (session._c_object, ctypes.byref(list_size)))
 
688
    if not bool(cert_list) and list_size.value != 0:
 
689
        raise gnutls.errors.GNUTLSError("error getting peer"
 
690
                                        " certificate")
598
691
    if list_size.value == 0:
599
692
        return None
600
693
    cert = cert_list[0]
649
742
    def handle(self):
650
743
        logger.info(u"TCP connection from: %s",
651
744
                    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,
 
745
        logger.debug(u"IPC Pipe FD: %d", self.server.pipe[1])
 
746
        # Open IPC pipe to parent process
 
747
        with closing(os.fdopen(self.server.pipe[1], "w", 1)) as ipc:
 
748
            session = (gnutls.connection
 
749
                       .ClientSession(self.request,
 
750
                                      gnutls.connection
 
751
                                      .X509Credentials()))
 
752
            
 
753
            line = self.request.makefile().readline()
 
754
            logger.debug(u"Protocol version: %r", line)
 
755
            try:
 
756
                if int(line.strip().split()[0]) > 1:
 
757
                    raise RuntimeError
 
758
            except (ValueError, IndexError, RuntimeError), error:
 
759
                logger.error(u"Unknown protocol version: %s", error)
 
760
                return
 
761
            
 
762
            # Note: gnutls.connection.X509Credentials is really a
 
763
            # generic GnuTLS certificate credentials object so long as
 
764
            # no X.509 keys are added to it.  Therefore, we can use it
 
765
            # here despite using OpenPGP certificates.
 
766
            
 
767
            #priority = ':'.join(("NONE", "+VERS-TLS1.1",
 
768
            #                     "+AES-256-CBC", "+SHA1",
 
769
            #                     "+COMP-NULL", "+CTYPE-OPENPGP",
 
770
            #                     "+DHE-DSS"))
 
771
            # Use a fallback default, since this MUST be set.
 
772
            priority = self.server.settings.get("priority", "NORMAL")
 
773
            (gnutls.library.functions
 
774
             .gnutls_priority_set_direct(session._c_object,
 
775
                                         priority, None))
 
776
            
 
777
            try:
 
778
                session.handshake()
 
779
            except gnutls.errors.GNUTLSError, error:
 
780
                logger.warning(u"Handshake failed: %s", error)
 
781
                # Do not run session.bye() here: the session is not
 
782
                # established.  Just abandon the request.
 
783
                return
 
784
            logger.debug(u"Handshake succeeded")
 
785
            try:
 
786
                fpr = fingerprint(peer_certificate(session))
 
787
            except (TypeError, gnutls.errors.GNUTLSError), error:
 
788
                logger.warning(u"Bad certificate: %s", error)
 
789
                session.bye()
 
790
                return
 
791
            logger.debug(u"Fingerprint: %s", fpr)
 
792
            
 
793
            for c in self.server.clients:
 
794
                if c.fingerprint == fpr:
 
795
                    client = c
 
796
                    break
 
797
            else:
 
798
                logger.warning(u"Client not found for fingerprint: %s",
 
799
                               fpr)
 
800
                ipc.write("NOTFOUND %s\n" % fpr)
 
801
                session.bye()
 
802
                return
 
803
            # Have to check if client.still_valid(), since it is
 
804
            # possible that the client timed out while establishing
 
805
            # the GnuTLS session.
 
806
            if not client.still_valid():
 
807
                logger.warning(u"Client %(name)s is invalid",
 
808
                               vars(client))
 
809
                ipc.write("INVALID %s\n" % client.name)
 
810
                session.bye()
 
811
                return
 
812
            ipc.write("SENDING %s\n" % client.name)
 
813
            sent_size = 0
 
814
            while sent_size < len(client.secret):
 
815
                sent = session.send(client.secret[sent_size:])
 
816
                logger.debug(u"Sent: %d, remaining: %d",
 
817
                             sent, len(client.secret)
 
818
                             - (sent_size + sent))
 
819
                sent_size += sent
 
820
            session.bye()
 
821
 
 
822
 
 
823
class ForkingMixInWithPipe(SocketServer.ForkingMixIn, object):
 
824
    """Like SocketServer.ForkingMixIn, but also pass a pipe.
 
825
    Assumes a gobject.MainLoop event loop.
 
826
    """
 
827
    def process_request(self, request, client_address):
 
828
        """This overrides and wraps the original process_request().
 
829
        This function creates a new pipe in self.pipe 
 
830
        """
 
831
        self.pipe = os.pipe()
 
832
        super(ForkingMixInWithPipe,
 
833
              self).process_request(request, client_address)
 
834
        os.close(self.pipe[1])  # close write end
 
835
        # Call "handle_ipc" for both data and EOF events
 
836
        gobject.io_add_watch(self.pipe[0],
 
837
                             gobject.IO_IN | gobject.IO_HUP,
 
838
                             self.handle_ipc)
 
839
    def handle_ipc(source, condition):
 
840
        """Dummy function; override as necessary"""
 
841
        os.close(source)
 
842
        return False
 
843
 
 
844
 
 
845
class IPv6_TCPServer(ForkingMixInWithPipe,
724
846
                     SocketServer.TCPServer, object):
725
 
    """IPv6 TCP server.  Accepts 'None' as address and/or port.
 
847
    """IPv6-capable TCP server.  Accepts 'None' as address and/or port
726
848
    Attributes:
727
849
        settings:       Server settings
728
850
        clients:        Set() of Client objects
736
858
        if "clients" in kwargs:
737
859
            self.clients = kwargs["clients"]
738
860
            del kwargs["clients"]
 
861
        if "use_ipv6" in kwargs:
 
862
            if not kwargs["use_ipv6"]:
 
863
                self.address_family = socket.AF_INET
 
864
            del kwargs["use_ipv6"]
739
865
        self.enabled = False
740
866
        super(IPv6_TCPServer, self).__init__(*args, **kwargs)
741
867
    def server_bind(self):
755
881
                                 u" bind to interface %s",
756
882
                                 self.settings["interface"])
757
883
                else:
758
 
                    raise error
 
884
                    raise
759
885
        # Only bind(2) the socket if we really need to.
760
886
        if self.server_address[0] or self.server_address[1]:
761
887
            if not self.server_address[0]:
762
 
                in6addr_any = "::"
763
 
                self.server_address = (in6addr_any,
 
888
                if self.address_family == socket.AF_INET6:
 
889
                    any_address = "::" # in6addr_any
 
890
                else:
 
891
                    any_address = socket.INADDR_ANY
 
892
                self.server_address = (any_address,
764
893
                                       self.server_address[1])
765
894
            elif not self.server_address[1]:
766
895
                self.server_address = (self.server_address[0],
778
907
            return super(IPv6_TCPServer, self).server_activate()
779
908
    def enable(self):
780
909
        self.enabled = True
 
910
    def handle_ipc(self, source, condition, file_objects={}):
 
911
        condition_names = {
 
912
            gobject.IO_IN: "IN", # There is data to read.
 
913
            gobject.IO_OUT: "OUT", # Data can be written (without
 
914
                                   # blocking).
 
915
            gobject.IO_PRI: "PRI", # There is urgent data to read.
 
916
            gobject.IO_ERR: "ERR", # Error condition.
 
917
            gobject.IO_HUP: "HUP"  # Hung up (the connection has been
 
918
                                   # broken, usually for pipes and
 
919
                                   # sockets).
 
920
            }
 
921
        conditions_string = ' | '.join(name
 
922
                                       for cond, name in
 
923
                                       condition_names.iteritems()
 
924
                                       if cond & condition)
 
925
        logger.debug("Handling IPC: FD = %d, condition = %s", source,
 
926
                     conditions_string)
 
927
        
 
928
        # Turn the pipe file descriptor into a Python file object
 
929
        if source not in file_objects:
 
930
            file_objects[source] = os.fdopen(source, "r", 1)
 
931
        
 
932
        # Read a line from the file object
 
933
        cmdline = file_objects[source].readline()
 
934
        if not cmdline:             # Empty line means end of file
 
935
            # close the IPC pipe
 
936
            file_objects[source].close()
 
937
            del file_objects[source]
 
938
            
 
939
            # Stop calling this function
 
940
            return False
 
941
        
 
942
        logger.debug("IPC command: %r\n" % cmdline)
 
943
        
 
944
        # Parse and act on command
 
945
        cmd, args = cmdline.split(None, 1)
 
946
        if cmd == "NOTFOUND":
 
947
            if self.settings["use_dbus"]:
 
948
                # Emit D-Bus signal
 
949
                mandos_dbus_service.ClientNotFound(args)
 
950
        elif cmd == "INVALID":
 
951
            if self.settings["use_dbus"]:
 
952
                for client in self.clients:
 
953
                    if client.name == args:
 
954
                        # Emit D-Bus signal
 
955
                        client.Rejected()
 
956
                        break
 
957
        elif cmd == "SENDING":
 
958
            for client in self.clients:
 
959
                if client.name == args:
 
960
                    client.checked_ok()
 
961
                    if self.settings["use_dbus"]:
 
962
                        # Emit D-Bus signal
 
963
                        client.ReceivedSecret()
 
964
                    break
 
965
        else:
 
966
            logger.error("Unknown IPC command: %r", cmdline)
 
967
        
 
968
        # Keep calling this function
 
969
        return True
781
970
 
782
971
 
783
972
def string_to_delta(interval):
784
973
    """Parse a string and return a datetime.timedelta
785
 
 
 
974
    
786
975
    >>> string_to_delta('7d')
787
976
    datetime.timedelta(7)
788
977
    >>> string_to_delta('60s')
889
1078
 
890
1079
 
891
1080
def main():
 
1081
    
 
1082
    ######################################################################
 
1083
    # Parsing of options, both command line and config file
 
1084
    
892
1085
    parser = optparse.OptionParser(version = "%%prog %s" % version)
893
1086
    parser.add_option("-i", "--interface", type="string",
894
1087
                      metavar="IF", help="Bind to interface IF")
913
1106
                      dest="use_dbus",
914
1107
                      help="Do not provide D-Bus system bus"
915
1108
                      " interface")
 
1109
    parser.add_option("--no-ipv6", action="store_false",
 
1110
                      dest="use_ipv6", help="Do not use IPv6")
916
1111
    options = parser.parse_args()[0]
917
1112
    
918
1113
    if options.check:
929
1124
                        "SECURE256:!CTYPE-X.509:+CTYPE-OPENPGP",
930
1125
                        "servicename": "Mandos",
931
1126
                        "use_dbus": "True",
 
1127
                        "use_ipv6": "True",
932
1128
                        }
933
1129
    
934
1130
    # Parse config file for server-global settings
937
1133
    server_config.read(os.path.join(options.configdir, "mandos.conf"))
938
1134
    # Convert the SafeConfigParser object to a dict
939
1135
    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"))
 
1136
    # Use the appropriate methods on the non-string config options
 
1137
    server_settings["debug"] = server_config.getboolean("DEFAULT",
 
1138
                                                        "debug")
 
1139
    server_settings["use_dbus"] = server_config.getboolean("DEFAULT",
 
1140
                                                           "use_dbus")
 
1141
    server_settings["use_ipv6"] = server_config.getboolean("DEFAULT",
 
1142
                                                           "use_ipv6")
 
1143
    if server_settings["port"]:
 
1144
        server_settings["port"] = server_config.getint("DEFAULT",
 
1145
                                                       "port")
945
1146
    del server_config
946
1147
    
947
1148
    # Override the settings from the config file with command line
948
1149
    # options, if set.
949
1150
    for option in ("interface", "address", "port", "debug",
950
1151
                   "priority", "servicename", "configdir",
951
 
                   "use_dbus"):
 
1152
                   "use_dbus", "use_ipv6"):
952
1153
        value = getattr(options, option)
953
1154
        if value is not None:
954
1155
            server_settings[option] = value
955
1156
    del options
956
1157
    # Now we have our good server settings in "server_settings"
957
1158
    
 
1159
    ##################################################################
 
1160
    
958
1161
    # For convenience
959
1162
    debug = server_settings["debug"]
960
1163
    use_dbus = server_settings["use_dbus"]
 
1164
    use_ipv6 = server_settings["use_ipv6"]
961
1165
    
962
1166
    if not debug:
963
1167
        syslogger.setLevel(logging.WARNING)
965
1169
    
966
1170
    if server_settings["servicename"] != "Mandos":
967
1171
        syslogger.setFormatter(logging.Formatter
968
 
                               ('Mandos (%s): %%(levelname)s:'
969
 
                                ' %%(message)s'
 
1172
                               ('Mandos (%s) [%%(process)d]:'
 
1173
                                ' %%(levelname)s: %%(message)s'
970
1174
                                % server_settings["servicename"]))
971
1175
    
972
1176
    # Parse config file with clients
978
1182
    client_config = ConfigParser.SafeConfigParser(client_defaults)
979
1183
    client_config.read(os.path.join(server_settings["configdir"],
980
1184
                                    "clients.conf"))
 
1185
 
 
1186
    global mandos_dbus_service
 
1187
    mandos_dbus_service = None
981
1188
    
982
1189
    clients = Set()
983
1190
    tcp_server = IPv6_TCPServer((server_settings["address"],
984
1191
                                 server_settings["port"]),
985
1192
                                TCP_handler,
986
1193
                                settings=server_settings,
987
 
                                clients=clients)
 
1194
                                clients=clients, use_ipv6=use_ipv6)
988
1195
    pidfilename = "/var/run/mandos.pid"
989
1196
    try:
990
1197
        pidfile = open(pidfilename, "w")
991
 
    except IOError, error:
 
1198
    except IOError:
992
1199
        logger.error("Could not open file %r", pidfilename)
993
1200
    
994
1201
    try:
1006
1213
                uid = 65534
1007
1214
                gid = 65534
1008
1215
    try:
 
1216
        os.setgid(gid)
1009
1217
        os.setuid(uid)
1010
 
        os.setgid(gid)
1011
1218
    except OSError, error:
1012
1219
        if error[0] != errno.EPERM:
1013
1220
            raise error
1014
1221
    
 
1222
    # Enable all possible GnuTLS debugging
 
1223
    if debug:
 
1224
        # "Use a log level over 10 to enable all debugging options."
 
1225
        # - GnuTLS manual
 
1226
        gnutls.library.functions.gnutls_global_set_log_level(11)
 
1227
        
 
1228
        @gnutls.library.types.gnutls_log_func
 
1229
        def debug_gnutls(level, string):
 
1230
            logger.debug("GnuTLS: %s", string[:-1])
 
1231
        
 
1232
        (gnutls.library.functions
 
1233
         .gnutls_global_set_log_function(debug_gnutls))
 
1234
    
1015
1235
    global service
 
1236
    protocol = avahi.PROTO_INET6 if use_ipv6 else avahi.PROTO_INET
1016
1237
    service = AvahiService(name = server_settings["servicename"],
1017
 
                           servicetype = "_mandos._tcp", )
 
1238
                           servicetype = "_mandos._tcp",
 
1239
                           protocol = protocol)
1018
1240
    if server_settings["interface"]:
1019
1241
        service.interface = (if_nametoindex
1020
1242
                             (server_settings["interface"]))
1031
1253
                            avahi.DBUS_INTERFACE_SERVER)
1032
1254
    # End of Avahi example code
1033
1255
    if use_dbus:
1034
 
        bus_name = dbus.service.BusName(u"org.mandos-system.Mandos",
1035
 
                                        bus)
 
1256
        bus_name = dbus.service.BusName(u"se.bsnet.fukt.Mandos", bus)
1036
1257
    
1037
 
    clients.update(Set(Client(name = section,
1038
 
                              config
1039
 
                              = dict(client_config.items(section)),
1040
 
                              use_dbus = use_dbus)
1041
 
                       for section in client_config.sections()))
 
1258
    client_class = Client
 
1259
    if use_dbus:
 
1260
        client_class = ClientDBus
 
1261
    clients.update(Set(
 
1262
            client_class(name = section,
 
1263
                         config= dict(client_config.items(section)))
 
1264
            for section in client_config.sections()))
1042
1265
    if not clients:
1043
1266
        logger.warning(u"No clients defined")
1044
1267
    
1055
1278
        daemon()
1056
1279
    
1057
1280
    try:
1058
 
        pid = os.getpid()
1059
 
        pidfile.write(str(pid) + "\n")
1060
 
        pidfile.close()
 
1281
        with closing(pidfile):
 
1282
            pid = os.getpid()
 
1283
            pidfile.write(str(pid) + "\n")
1061
1284
        del pidfile
1062
1285
    except IOError:
1063
1286
        logger.error(u"Could not write to file %r with PID %d",
1089
1312
    signal.signal(signal.SIGTERM, lambda signum, frame: sys.exit())
1090
1313
    
1091
1314
    if use_dbus:
1092
 
        class MandosServer(dbus.service.Object):
 
1315
        class MandosDBusService(dbus.service.Object):
1093
1316
            """A D-Bus proxy object"""
1094
1317
            def __init__(self):
1095
 
                dbus.service.Object.__init__(self, bus,
1096
 
                                             "/Mandos")
1097
 
            _interface = u"org.mandos_system.Mandos"
1098
 
 
 
1318
                dbus.service.Object.__init__(self, bus, "/")
 
1319
            _interface = u"se.bsnet.fukt.Mandos"
 
1320
            
1099
1321
            @dbus.service.signal(_interface, signature="oa{sv}")
1100
1322
            def ClientAdded(self, objpath, properties):
1101
1323
                "D-Bus signal"
1102
1324
                pass
1103
 
 
1104
 
            @dbus.service.signal(_interface, signature="o")
1105
 
            def ClientRemoved(self, objpath):
1106
 
                "D-Bus signal"
1107
 
                pass
1108
 
 
 
1325
            
 
1326
            @dbus.service.signal(_interface, signature="s")
 
1327
            def ClientNotFound(self, fingerprint):
 
1328
                "D-Bus signal"
 
1329
                pass
 
1330
            
 
1331
            @dbus.service.signal(_interface, signature="os")
 
1332
            def ClientRemoved(self, objpath, name):
 
1333
                "D-Bus signal"
 
1334
                pass
 
1335
            
1109
1336
            @dbus.service.method(_interface, out_signature="ao")
1110
1337
            def GetAllClients(self):
 
1338
                "D-Bus method"
1111
1339
                return dbus.Array(c.dbus_object_path for c in clients)
1112
 
 
 
1340
            
1113
1341
            @dbus.service.method(_interface, out_signature="a{oa{sv}}")
1114
1342
            def GetAllClientsWithProperties(self):
 
1343
                "D-Bus method"
1115
1344
                return dbus.Dictionary(
1116
1345
                    ((c.dbus_object_path, c.GetAllProperties())
1117
1346
                     for c in clients),
1118
1347
                    signature="oa{sv}")
1119
 
 
 
1348
            
1120
1349
            @dbus.service.method(_interface, in_signature="o")
1121
1350
            def RemoveClient(self, object_path):
 
1351
                "D-Bus method"
1122
1352
                for c in clients:
1123
1353
                    if c.dbus_object_path == object_path:
1124
1354
                        clients.remove(c)
 
1355
                        c.remove_from_connection()
1125
1356
                        # Don't signal anything except ClientRemoved
1126
 
                        c.use_dbus = False
1127
 
                        c.disable()
 
1357
                        c.disable(signal=False)
1128
1358
                        # Emit D-Bus signal
1129
 
                        self.ClientRemoved(object_path)
 
1359
                        self.ClientRemoved(object_path, c.name)
1130
1360
                        return
1131
1361
                raise KeyError
1132
 
            @dbus.service.method(_interface)
1133
 
            def Quit(self):
1134
 
                main_loop.quit()
1135
 
 
 
1362
            
1136
1363
            del _interface
1137
 
    
1138
 
        mandos_server = MandosServer()
 
1364
        
 
1365
        mandos_dbus_service = MandosDBusService()
1139
1366
    
1140
1367
    for client in clients:
1141
1368
        if use_dbus:
1142
1369
            # Emit D-Bus signal
1143
 
            mandos_server.ClientAdded(client.dbus_object_path,
1144
 
                                      client.GetAllProperties())
 
1370
            mandos_dbus_service.ClientAdded(client.dbus_object_path,
 
1371
                                            client.GetAllProperties())
1145
1372
        client.enable()
1146
1373
    
1147
1374
    tcp_server.enable()
1149
1376
    
1150
1377
    # Find out what port we got
1151
1378
    service.port = tcp_server.socket.getsockname()[1]
1152
 
    logger.info(u"Now listening on address %r, port %d, flowinfo %d,"
1153
 
                u" scope_id %d" % tcp_server.socket.getsockname())
 
1379
    if use_ipv6:
 
1380
        logger.info(u"Now listening on address %r, port %d,"
 
1381
                    " flowinfo %d, scope_id %d"
 
1382
                    % tcp_server.socket.getsockname())
 
1383
    else:                       # IPv4
 
1384
        logger.info(u"Now listening on address %r, port %d"
 
1385
                    % tcp_server.socket.getsockname())
1154
1386
    
1155
1387
    #service.interface = tcp_server.socket.getsockname()[3]
1156
1388
    
1176
1408
        sys.exit(1)
1177
1409
    except KeyboardInterrupt:
1178
1410
        if debug:
1179
 
            print
 
1411
            print >> sys.stderr
 
1412
        logger.debug("Server received KeyboardInterrupt")
 
1413
    logger.debug("Server exiting")
1180
1414
 
1181
1415
if __name__ == '__main__':
1182
1416
    main()