/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-12-25 23:13:47 UTC
  • Revision ID: teddy@fukt.bsnet.se-20091225231347-gg9u9ru0wj0f24hh
More consistent terminology: Clients are no longer "invalid" - they
are "disabled".  All code and documentation changed to reflect this.

D=Bus API change: The "properties" argument was removed from the
"ClientAdded" signal on interface "se.bsnet.fukt.Mandos".  All code in
both "mandos" and "mandos-monitor" changed to reflect this.

* mandos: Replaced "with closing(F)" with simply "with F" in all
          places where F is a file object.
  (Client.still_valid): Removed.  All callers changed to look at
                        "Client.enabled" instead.
  (dbus_service_property): Check for unsupported signatures with the
                           "byte_arrays" option.
  (DBusObjectWithProperties.Set): - '' -
  (ClientHandler.handle): Use the reverse pipe to receive the
                          "Client.enabled" attribute instead of the
                          now-removed "Client.still_valid()" method.
  (ForkingMixInWithPipe): Renamed to "ForkingMixInWithPipes" (all
                          users changed).  Now also create a reverse
                          pipe for sending data to the child process.
  (ForkingMixInWithPipes.add_pipe): Now takes two pipe fd's as
                                    arguments.  All callers changed.
  (IPv6_TCPServer.handle_ipc): Take an additional "reply_fd" argument
                               (all callers changed).  Close the reply
                               pipe when the child data pipe is
                               closed.  New "GETATTR" IPC method; will
                               pickle client attribute and send it
                               over the reply pipe FD.
  (MandosDBusService.ClientAdded): Removed "properties" argument.  All
                                   emitters changed.
* mandos-clients.conf.xml (DESCRIPTION, OPTIONS): Use
                                                  "enabled/disabled"
                                                  terminology.
* mandos-ctl: Option "--is-valid" renamed to "--is-enabled".
* mandos-monitor: Enable user locale.  Try to log exceptions.
  (MandosClientPropertyCache.__init__): Removed "properties" argument.
                                        All callers changed.
  (UserInterface.add_new_client): Remove "properties" argument.  All
                                  callers changed.  Supply "logger"
                                  argument to MandosClientWidget().
  (UserInterface.add_client): New "logger" argument.  All callers
                              changed.
* mandos.xml (BUGS, SECURITY/CLIENTS): Use "enabled/disabled"
                                       terminology.

Show diffs side-by-side

added added

removed removed

Lines of Context:
55
55
import logging
56
56
import logging.handlers
57
57
import pwd
58
 
from contextlib import closing
 
58
import contextlib
59
59
import struct
60
60
import fcntl
61
61
import functools
 
62
import cPickle as pickle
62
63
 
63
64
import dbus
64
65
import dbus.service
242
243
    enabled:    bool()
243
244
    last_checked_ok: datetime.datetime(); (UTC) or None
244
245
    timeout:    datetime.timedelta(); How long from last_checked_ok
245
 
                                      until this client is invalid
 
246
                                      until this client is disabled
246
247
    interval:   datetime.timedelta(); How often to start a new checker
247
248
    disable_hook:  If set, called by disable() as disable_hook(self)
248
249
    checker:    subprocess.Popen(); a running checker process used
290
291
        if u"secret" in config:
291
292
            self.secret = config[u"secret"].decode(u"base64")
292
293
        elif u"secfile" in config:
293
 
            with closing(open(os.path.expanduser
294
 
                              (os.path.expandvars
295
 
                               (config[u"secfile"])),
296
 
                              "rb")) as secfile:
 
294
            with open(os.path.expanduser(os.path.expandvars
 
295
                                         (config[u"secfile"])),
 
296
                      "rb") as secfile:
297
297
                self.secret = secfile.read()
298
298
        else:
299
299
            raise TypeError(u"No secret or secfile for client %s"
396
396
        # client would inevitably timeout, since no checker would get
397
397
        # a chance to run to completion.  If we instead leave running
398
398
        # checkers alone, the checker would have to take more time
399
 
        # than 'timeout' for the client to be declared invalid, which
400
 
        # is as it should be.
 
399
        # than 'timeout' for the client to be disabled, which is as it
 
400
        # should be.
401
401
        
402
402
        # If a checker exists, make sure it is not a zombie
403
403
        try:
475
475
            if error.errno != errno.ESRCH: # No such process
476
476
                raise
477
477
        self.checker = None
478
 
    
479
 
    def still_valid(self):
480
 
        """Has the timeout not yet passed for this client?"""
481
 
        if not getattr(self, u"enabled", False):
482
 
            return False
483
 
        now = datetime.datetime.utcnow()
484
 
        if self.last_checked_ok is None:
485
 
            return now < (self.created + self.timeout)
486
 
        else:
487
 
            return now < (self.last_checked_ok + self.timeout)
488
478
 
489
479
 
490
480
def dbus_service_property(dbus_interface, signature=u"v",
499
489
    dbus.service.method, except there is only "signature", since the
500
490
    type from Get() and the type sent to Set() is the same.
501
491
    """
 
492
    # Encoding deeply encoded byte arrays is not supported yet by the
 
493
    # "Set" method, so we fail early here:
 
494
    if byte_arrays and signature != u"ay":
 
495
        raise ValueError(u"Byte arrays not supported for non-'ay'"
 
496
                         u" signature %r" % signature)
502
497
    def decorator(func):
503
498
        func._dbus_is_property = True
504
499
        func._dbus_interface = dbus_interface
590
585
        if prop._dbus_access == u"read":
591
586
            raise DBusPropertyAccessException(property_name)
592
587
        if prop._dbus_get_args_options[u"byte_arrays"]:
 
588
            # The byte_arrays option is not supported yet on
 
589
            # signatures other than "ay".
 
590
            if prop._dbus_signature != u"ay":
 
591
                raise ValueError
593
592
            value = dbus.ByteArray(''.join(unichr(byte)
594
593
                                           for byte in value))
595
594
        prop(value)
990
989
    def handle(self):
991
990
        logger.info(u"TCP connection from: %s",
992
991
                    unicode(self.client_address))
993
 
        logger.debug(u"IPC Pipe FD: %d", self.server.pipe[1])
 
992
        logger.debug(u"IPC Pipe FD: %d", self.server.child_pipe[1])
994
993
        # Open IPC pipe to parent process
995
 
        with closing(os.fdopen(self.server.pipe[1], u"w", 1)) as ipc:
 
994
        with contextlib.nested(os.fdopen(self.server.child_pipe[1],
 
995
                                         u"w", 1),
 
996
                               os.fdopen(self.server.parent_pipe[0],
 
997
                                         u"r", 0)) as (ipc,
 
998
                                                       ipc_return):
996
999
            session = (gnutls.connection
997
1000
                       .ClientSession(self.request,
998
1001
                                      gnutls.connection
1033
1036
                return
1034
1037
            logger.debug(u"Handshake succeeded")
1035
1038
            try:
1036
 
                fpr = self.fingerprint(self.peer_certificate(session))
1037
 
            except (TypeError, gnutls.errors.GNUTLSError), error:
1038
 
                logger.warning(u"Bad certificate: %s", error)
1039
 
                session.bye()
1040
 
                return
1041
 
            logger.debug(u"Fingerprint: %s", fpr)
1042
 
            
1043
 
            for c in self.server.clients:
1044
 
                if c.fingerprint == fpr:
1045
 
                    client = c
1046
 
                    break
1047
 
            else:
1048
 
                ipc.write(u"NOTFOUND %s %s\n"
1049
 
                          % (fpr, unicode(self.client_address)))
1050
 
                session.bye()
1051
 
                return
1052
 
            # Have to check if client.still_valid(), since it is
1053
 
            # possible that the client timed out while establishing
1054
 
            # the GnuTLS session.
1055
 
            if not client.still_valid():
1056
 
                ipc.write(u"INVALID %s\n" % client.name)
1057
 
                session.bye()
1058
 
                return
1059
 
            ipc.write(u"SENDING %s\n" % client.name)
1060
 
            sent_size = 0
1061
 
            while sent_size < len(client.secret):
1062
 
                sent = session.send(client.secret[sent_size:])
1063
 
                logger.debug(u"Sent: %d, remaining: %d",
1064
 
                             sent, len(client.secret)
1065
 
                             - (sent_size + sent))
1066
 
                sent_size += sent
1067
 
            session.bye()
 
1039
                try:
 
1040
                    fpr = self.fingerprint(self.peer_certificate
 
1041
                                           (session))
 
1042
                except (TypeError, gnutls.errors.GNUTLSError), error:
 
1043
                    logger.warning(u"Bad certificate: %s", error)
 
1044
                    return
 
1045
                logger.debug(u"Fingerprint: %s", fpr)
 
1046
 
 
1047
                for c in self.server.clients:
 
1048
                    if c.fingerprint == fpr:
 
1049
                        client = c
 
1050
                        break
 
1051
                else:
 
1052
                    ipc.write(u"NOTFOUND %s %s\n"
 
1053
                              % (fpr, unicode(self.client_address)))
 
1054
                    return
 
1055
                # Have to check if client.enabled, since it is
 
1056
                # possible that the client was disabled since the
 
1057
                # GnuTLS session was established.
 
1058
                ipc.write(u"GETATTR enabled %s\n" % fpr)
 
1059
                enabled = pickle.load(ipc_return)
 
1060
                if not enabled:
 
1061
                    ipc.write(u"DISABLED %s\n" % client.name)
 
1062
                    return
 
1063
                ipc.write(u"SENDING %s\n" % client.name)
 
1064
                sent_size = 0
 
1065
                while sent_size < len(client.secret):
 
1066
                    sent = session.send(client.secret[sent_size:])
 
1067
                    logger.debug(u"Sent: %d, remaining: %d",
 
1068
                                 sent, len(client.secret)
 
1069
                                 - (sent_size + sent))
 
1070
                    sent_size += sent
 
1071
            finally:
 
1072
                session.bye()
1068
1073
    
1069
1074
    @staticmethod
1070
1075
    def peer_certificate(session):
1130
1135
        return hex_fpr
1131
1136
 
1132
1137
 
1133
 
class ForkingMixInWithPipe(socketserver.ForkingMixIn, object):
1134
 
    """Like socketserver.ForkingMixIn, but also pass a pipe."""
 
1138
class ForkingMixInWithPipes(socketserver.ForkingMixIn, object):
 
1139
    """Like socketserver.ForkingMixIn, but also pass a pipe pair."""
1135
1140
    def process_request(self, request, client_address):
1136
1141
        """Overrides and wraps the original process_request().
1137
1142
        
1138
1143
        This function creates a new pipe in self.pipe
1139
1144
        """
1140
 
        self.pipe = os.pipe()
1141
 
        super(ForkingMixInWithPipe,
 
1145
        self.child_pipe = os.pipe() # Child writes here
 
1146
        self.parent_pipe = os.pipe() # Parent writes here
 
1147
        super(ForkingMixInWithPipes,
1142
1148
              self).process_request(request, client_address)
1143
 
        os.close(self.pipe[1])  # close write end
1144
 
        self.add_pipe(self.pipe[0])
1145
 
    def add_pipe(self, pipe):
 
1149
        # Close unused ends for parent
 
1150
        os.close(self.parent_pipe[0]) # close read end
 
1151
        os.close(self.child_pipe[1])  # close write end
 
1152
        self.add_pipe_fds(self.child_pipe[0], self.parent_pipe[1])
 
1153
    def add_pipe_fds(self, child_pipe_fd, parent_pipe_fd):
1146
1154
        """Dummy function; override as necessary"""
1147
 
        os.close(pipe)
1148
 
 
1149
 
 
1150
 
class IPv6_TCPServer(ForkingMixInWithPipe,
 
1155
        os.close(child_pipe_fd)
 
1156
        os.close(parent_pipe_fd)
 
1157
 
 
1158
 
 
1159
class IPv6_TCPServer(ForkingMixInWithPipes,
1151
1160
                     socketserver.TCPServer, object):
1152
1161
    """IPv6-capable TCP server.  Accepts 'None' as address and/or port
1153
1162
    
1238
1247
            return socketserver.TCPServer.server_activate(self)
1239
1248
    def enable(self):
1240
1249
        self.enabled = True
1241
 
    def add_pipe(self, pipe):
 
1250
    def add_pipe_fds(self, child_pipe_fd, parent_pipe_fd):
1242
1251
        # Call "handle_ipc" for both data and EOF events
1243
 
        gobject.io_add_watch(pipe, gobject.IO_IN | gobject.IO_HUP,
1244
 
                             self.handle_ipc)
1245
 
    def handle_ipc(self, source, condition, file_objects={}):
 
1252
        gobject.io_add_watch(child_pipe_fd,
 
1253
                             gobject.IO_IN | gobject.IO_HUP,
 
1254
                             functools.partial(self.handle_ipc,
 
1255
                                               reply_fd
 
1256
                                               =parent_pipe_fd))
 
1257
    def handle_ipc(self, source, condition, reply_fd=None,
 
1258
                   file_objects={}):
1246
1259
        condition_names = {
1247
1260
            gobject.IO_IN: u"IN",   # There is data to read.
1248
1261
            gobject.IO_OUT: u"OUT", # Data can be written (without
1260
1273
        logger.debug(u"Handling IPC: FD = %d, condition = %s", source,
1261
1274
                     conditions_string)
1262
1275
        
1263
 
        # Turn the pipe file descriptor into a Python file object
 
1276
        # Turn the pipe file descriptors into Python file objects
1264
1277
        if source not in file_objects:
1265
1278
            file_objects[source] = os.fdopen(source, u"r", 1)
 
1279
        if reply_fd not in file_objects:
 
1280
            file_objects[reply_fd] = os.fdopen(reply_fd, u"w", 0)
1266
1281
        
1267
1282
        # Read a line from the file object
1268
1283
        cmdline = file_objects[source].readline()
1269
1284
        if not cmdline:             # Empty line means end of file
1270
 
            # close the IPC pipe
 
1285
            # close the IPC pipes
1271
1286
            file_objects[source].close()
1272
1287
            del file_objects[source]
 
1288
            file_objects[reply_fd].close()
 
1289
            del file_objects[reply_fd]
1273
1290
            
1274
1291
            # Stop calling this function
1275
1292
            return False
1286
1303
            if self.use_dbus:
1287
1304
                # Emit D-Bus signal
1288
1305
                mandos_dbus_service.ClientNotFound(fpr, address)
1289
 
        elif cmd == u"INVALID":
 
1306
        elif cmd == u"DISABLED":
1290
1307
            for client in self.clients:
1291
1308
                if client.name == args:
1292
 
                    logger.warning(u"Client %s is invalid", args)
 
1309
                    logger.warning(u"Client %s is disabled", args)
1293
1310
                    if self.use_dbus:
1294
1311
                        # Emit D-Bus signal
1295
1312
                        client.Rejected()
1296
1313
                    break
1297
1314
            else:
1298
 
                logger.error(u"Unknown client %s is invalid", args)
 
1315
                logger.error(u"Unknown client %s is disabled", args)
1299
1316
        elif cmd == u"SENDING":
1300
1317
            for client in self.clients:
1301
1318
                if client.name == args:
1308
1325
            else:
1309
1326
                logger.error(u"Sending secret to unknown client %s",
1310
1327
                             args)
 
1328
        elif cmd == u"GETATTR":
 
1329
            attr_name, fpr = args.split(None, 1)
 
1330
            for client in self.clients:
 
1331
                if client.fingerprint == fpr:
 
1332
                    attr_value = getattr(client, attr_name, None)
 
1333
                    logger.debug("IPC reply: %r", attr_value)
 
1334
                    pickle.dump(attr_value, file_objects[reply_fd])
 
1335
                    break
 
1336
            else:
 
1337
                logger.error(u"Client %s on address %s requesting "
 
1338
                             u"attribute %s not found", fpr, address,
 
1339
                             attr_name)
 
1340
                pickle.dump(None, file_objects[reply_fd])
1311
1341
        else:
1312
1342
            logger.error(u"Unknown IPC command: %r", cmdline)
1313
1343
        
1368
1398
        def if_nametoindex(interface):
1369
1399
            "Get an interface index the hard way, i.e. using fcntl()"
1370
1400
            SIOCGIFINDEX = 0x8933  # From /usr/include/linux/sockios.h
1371
 
            with closing(socket.socket()) as s:
 
1401
            with contextlib.closing(socket.socket()) as s:
1372
1402
                ifreq = fcntl.ioctl(s, SIOCGIFINDEX,
1373
1403
                                    struct.pack(str(u"16s16x"),
1374
1404
                                                interface))
1607
1637
        daemon()
1608
1638
    
1609
1639
    try:
1610
 
        with closing(pidfile):
 
1640
        with pidfile:
1611
1641
            pid = os.getpid()
1612
1642
            pidfile.write(str(pid) + "\n")
1613
1643
        del pidfile
1631
1661
                dbus.service.Object.__init__(self, bus, u"/")
1632
1662
            _interface = u"se.bsnet.fukt.Mandos"
1633
1663
            
1634
 
            @dbus.service.signal(_interface, signature=u"oa{sv}")
1635
 
            def ClientAdded(self, objpath, properties):
 
1664
            @dbus.service.signal(_interface, signature=u"o")
 
1665
            def ClientAdded(self, objpath):
1636
1666
                "D-Bus signal"
1637
1667
                pass
1638
1668
            
1700
1730
    for client in tcp_server.clients:
1701
1731
        if use_dbus:
1702
1732
            # Emit D-Bus signal
1703
 
            mandos_dbus_service.ClientAdded(client.dbus_object_path,
1704
 
                                            client.GetAll(u""))
 
1733
            mandos_dbus_service.ClientAdded(client.dbus_object_path)
1705
1734
        client.enable()
1706
1735
    
1707
1736
    tcp_server.enable()