/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:
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.3"
 
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():
892
 
    parser = OptionParser(version = "%%prog %s" % version)
 
1081
    
 
1082
    ######################################################################
 
1083
    # Parsing of options, both command line and config file
 
1084
    
 
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")
895
1088
    parser.add_option("-a", "--address", type="string",
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()