/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: 2015-05-22 20:23:46 UTC
  • Revision ID: teddy@recompile.se-20150522202346-taccq232srbszyd9
mandos-keygen: Bug fix: Only use one SSH key from ssh-keyscan

If ssh-keyscan found keys of more than one type, the generated output
would be incorrect.  Restrict the output to one type of key.

Show diffs side-by-side

added added

removed removed

Lines of Context:
36
36
 
37
37
from future_builtins import *
38
38
 
39
 
try:
40
 
    import SocketServer as socketserver
41
 
except ImportError:
42
 
    import socketserver
 
39
import SocketServer as socketserver
43
40
import socket
44
41
import argparse
45
42
import datetime
50
47
import gnutls.library.functions
51
48
import gnutls.library.constants
52
49
import gnutls.library.types
53
 
try:
54
 
    import ConfigParser as configparser
55
 
except ImportError:
56
 
    import configparser
 
50
import ConfigParser as configparser
57
51
import sys
58
52
import re
59
53
import os
68
62
import struct
69
63
import fcntl
70
64
import functools
71
 
try:
72
 
    import cPickle as pickle
73
 
except ImportError:
74
 
    import pickle
 
65
import cPickle as pickle
75
66
import multiprocessing
76
67
import types
77
68
import binascii
78
69
import tempfile
79
70
import itertools
80
71
import collections
81
 
import codecs
82
72
 
83
73
import dbus
84
74
import dbus.service
85
 
try:
86
 
    import gobject
87
 
except ImportError:
88
 
    from gi.repository import GObject as gobject
 
75
import gobject
89
76
import avahi
90
77
from dbus.mainloop.glib import DBusGMainLoop
91
78
import ctypes
424
411
            .format(self.name)))
425
412
        return ret
426
413
 
427
 
def call_pipe(connection,       # : multiprocessing.Connection
428
 
              func, *args, **kwargs):
429
 
    """This function is meant to be called by multiprocessing.Process
430
 
    
431
 
    This function runs func(*args, **kwargs), and writes the resulting
432
 
    return value on the provided multiprocessing.Connection.
433
 
    """
434
 
    connection.send(func(*args, **kwargs))
435
 
    connection.close()
436
414
 
437
415
class Client(object):
438
416
    """A representation of a client host served by this server.
465
443
    last_checker_status: integer between 0 and 255 reflecting exit
466
444
                         status of last checker. -1 reflects crashed
467
445
                         checker, -2 means no checker completed yet.
468
 
    last_checker_signal: The signal which killed the last checker, if
469
 
                         last_checker_status is -1
470
446
    last_enabled: datetime.datetime(); (UTC) or None
471
447
    name:       string; from the config file, used in log messages and
472
448
                        D-Bus identifiers
646
622
        # Also start a new checker *right now*.
647
623
        self.start_checker()
648
624
    
649
 
    def checker_callback(self, source, condition, connection,
650
 
                         command):
 
625
    def checker_callback(self, pid, condition, command):
651
626
        """The checker has completed, so take appropriate actions."""
652
627
        self.checker_callback_tag = None
653
628
        self.checker = None
654
 
        # Read return code from connection (see call_pipe)
655
 
        returncode = connection.recv()
656
 
        connection.close()
657
 
        
658
 
        if returncode >= 0:
659
 
            self.last_checker_status = returncode
660
 
            self.last_checker_signal = None
 
629
        if os.WIFEXITED(condition):
 
630
            self.last_checker_status = os.WEXITSTATUS(condition)
661
631
            if self.last_checker_status == 0:
662
632
                logger.info("Checker for %(name)s succeeded",
663
633
                            vars(self))
666
636
                logger.info("Checker for %(name)s failed", vars(self))
667
637
        else:
668
638
            self.last_checker_status = -1
669
 
            self.last_checker_signal = -returncode
670
639
            logger.warning("Checker for %(name)s crashed?",
671
640
                           vars(self))
672
 
        return False
673
641
    
674
642
    def checked_ok(self):
675
643
        """Assert that the client has been seen, alive and well."""
676
644
        self.last_checked_ok = datetime.datetime.utcnow()
677
645
        self.last_checker_status = 0
678
 
        self.last_checker_signal = None
679
646
        self.bump_timeout()
680
647
    
681
648
    def bump_timeout(self, timeout=None):
707
674
        # than 'timeout' for the client to be disabled, which is as it
708
675
        # should be.
709
676
        
710
 
        if self.checker is not None and not self.checker.is_alive():
711
 
            logger.warning("Checker was not alive; joining")
712
 
            self.checker.join()
713
 
            self.checker = None
 
677
        # If a checker exists, make sure it is not a zombie
 
678
        try:
 
679
            pid, status = os.waitpid(self.checker.pid, os.WNOHANG)
 
680
        except AttributeError:
 
681
            pass
 
682
        except OSError as error:
 
683
            if error.errno != errno.ECHILD:
 
684
                raise
 
685
        else:
 
686
            if pid:
 
687
                logger.warning("Checker was a zombie")
 
688
                gobject.source_remove(self.checker_callback_tag)
 
689
                self.checker_callback(pid, status,
 
690
                                      self.current_checker_command)
714
691
        # Start a new checker if needed
715
692
        if self.checker is None:
716
693
            # Escape attributes for the shell
725
702
                             exc_info=error)
726
703
                return True     # Try again later
727
704
            self.current_checker_command = command
728
 
            logger.info("Starting checker %r for %s", command,
729
 
                        self.name)
730
 
            # We don't need to redirect stdout and stderr, since
731
 
            # in normal mode, that is already done by daemon(),
732
 
            # and in debug mode we don't want to.  (Stdin is
733
 
            # always replaced by /dev/null.)
734
 
            # The exception is when not debugging but nevertheless
735
 
            # running in the foreground; use the previously
736
 
            # created wnull.
737
 
            popen_args = { "close_fds": True,
738
 
                           "shell": True,
739
 
                           "cwd": "/" }
740
 
            if (not self.server_settings["debug"]
741
 
                and self.server_settings["foreground"]):
742
 
                popen_args.update({"stdout": wnull,
743
 
                                   "stderr": wnull })
744
 
            pipe = multiprocessing.Pipe(duplex = False)
745
 
            self.checker = multiprocessing.Process(
746
 
                target = call_pipe,
747
 
                args = (pipe[1], subprocess.call, command),
748
 
                kwargs = popen_args)
749
 
            self.checker.start()
750
 
            self.checker_callback_tag = gobject.io_add_watch(
751
 
                pipe[0].fileno(), gobject.IO_IN,
752
 
                self.checker_callback, pipe[0], command)
 
705
            try:
 
706
                logger.info("Starting checker %r for %s", command,
 
707
                            self.name)
 
708
                # We don't need to redirect stdout and stderr, since
 
709
                # in normal mode, that is already done by daemon(),
 
710
                # and in debug mode we don't want to.  (Stdin is
 
711
                # always replaced by /dev/null.)
 
712
                # The exception is when not debugging but nevertheless
 
713
                # running in the foreground; use the previously
 
714
                # created wnull.
 
715
                popen_args = {}
 
716
                if (not self.server_settings["debug"]
 
717
                    and self.server_settings["foreground"]):
 
718
                    popen_args.update({"stdout": wnull,
 
719
                                       "stderr": wnull })
 
720
                self.checker = subprocess.Popen(command,
 
721
                                                close_fds=True,
 
722
                                                shell=True,
 
723
                                                cwd="/",
 
724
                                                **popen_args)
 
725
            except OSError as error:
 
726
                logger.error("Failed to start subprocess",
 
727
                             exc_info=error)
 
728
                return True
 
729
            self.checker_callback_tag = gobject.child_watch_add(
 
730
                self.checker.pid, self.checker_callback, data=command)
 
731
            # The checker may have completed before the gobject
 
732
            # watch was added.  Check for this.
 
733
            try:
 
734
                pid, status = os.waitpid(self.checker.pid, os.WNOHANG)
 
735
            except OSError as error:
 
736
                if error.errno == errno.ECHILD:
 
737
                    # This should never happen
 
738
                    logger.error("Child process vanished",
 
739
                                 exc_info=error)
 
740
                    return True
 
741
                raise
 
742
            if pid:
 
743
                gobject.source_remove(self.checker_callback_tag)
 
744
                self.checker_callback(pid, status, command)
753
745
        # Re-run this periodically if run by gobject.timeout_add
754
746
        return True
755
747
    
761
753
        if getattr(self, "checker", None) is None:
762
754
            return
763
755
        logger.debug("Stopping checker for %(name)s", vars(self))
764
 
        self.checker.terminate()
 
756
        try:
 
757
            self.checker.terminate()
 
758
            #time.sleep(0.5)
 
759
            #if self.checker.poll() is None:
 
760
            #    self.checker.kill()
 
761
        except OSError as error:
 
762
            if error.errno != errno.ESRCH: # No such process
 
763
                raise
765
764
        self.checker = None
766
765
 
767
766
 
1099
1098
                interface_names.add(alt_interface)
1100
1099
                # Is this a D-Bus signal?
1101
1100
                if getattr(attribute, "_dbus_is_signal", False):
1102
 
                    if sys.version_info.major == 2:
1103
 
                        # Extract the original non-method undecorated
1104
 
                        # function by black magic
1105
 
                        nonmethod_func = (dict(
1106
 
                            zip(attribute.func_code.co_freevars,
1107
 
                                attribute.__closure__))
1108
 
                                          ["func"].cell_contents)
1109
 
                    else:
1110
 
                        nonmethod_func = attribute
 
1101
                    # Extract the original non-method undecorated
 
1102
                    # function by black magic
 
1103
                    nonmethod_func = (dict(
 
1104
                        zip(attribute.func_code.co_freevars,
 
1105
                            attribute.__closure__))
 
1106
                                      ["func"].cell_contents)
1111
1107
                    # Create a new, but exactly alike, function
1112
1108
                    # object, and decorate it to be a new D-Bus signal
1113
1109
                    # with the alternate D-Bus interface name
1114
 
                    if sys.version_info.major == 2:
1115
 
                        new_function = types.FunctionType(
1116
 
                            nonmethod_func.func_code,
1117
 
                            nonmethod_func.func_globals,
1118
 
                            nonmethod_func.func_name,
1119
 
                            nonmethod_func.func_defaults,
1120
 
                            nonmethod_func.func_closure)
1121
 
                    else:
1122
 
                        new_function = types.FunctionType(
1123
 
                            nonmethod_func.__code__,
1124
 
                            nonmethod_func.__globals__,
1125
 
                            nonmethod_func.__name__,
1126
 
                            nonmethod_func.__defaults__,
1127
 
                            nonmethod_func.__closure__)
1128
1110
                    new_function = (dbus.service.signal(
1129
 
                        alt_interface,
1130
 
                        attribute._dbus_signature)(new_function))
 
1111
                        alt_interface, attribute._dbus_signature)
 
1112
                                    (types.FunctionType(
 
1113
                                        nonmethod_func.func_code,
 
1114
                                        nonmethod_func.func_globals,
 
1115
                                        nonmethod_func.func_name,
 
1116
                                        nonmethod_func.func_defaults,
 
1117
                                        nonmethod_func.func_closure)))
1131
1118
                    # Copy annotations, if any
1132
1119
                    try:
1133
1120
                        new_function._dbus_annotations = dict(
1356
1343
            DBusObjectWithProperties.__del__(self, *args, **kwargs)
1357
1344
        Client.__del__(self, *args, **kwargs)
1358
1345
    
1359
 
    def checker_callback(self, source, condition,
1360
 
                         connection, command, *args, **kwargs):
1361
 
        ret = Client.checker_callback(self, source, condition,
1362
 
                                      connection, command, *args,
1363
 
                                      **kwargs)
1364
 
        exitstatus = self.last_checker_status
1365
 
        if exitstatus >= 0:
 
1346
    def checker_callback(self, pid, condition, command,
 
1347
                         *args, **kwargs):
 
1348
        self.checker_callback_tag = None
 
1349
        self.checker = None
 
1350
        if os.WIFEXITED(condition):
 
1351
            exitstatus = os.WEXITSTATUS(condition)
1366
1352
            # Emit D-Bus signal
1367
1353
            self.CheckerCompleted(dbus.Int16(exitstatus),
1368
 
                                  dbus.Int64(0),
 
1354
                                  dbus.Int64(condition),
1369
1355
                                  dbus.String(command))
1370
1356
        else:
1371
1357
            # Emit D-Bus signal
1372
1358
            self.CheckerCompleted(dbus.Int16(-1),
1373
 
                                  dbus.Int64(
1374
 
                                      self.last_checker_signal),
 
1359
                                  dbus.Int64(condition),
1375
1360
                                  dbus.String(command))
1376
 
        return ret
 
1361
        
 
1362
        return Client.checker_callback(self, pid, condition, command,
 
1363
                                       *args, **kwargs)
1377
1364
    
1378
1365
    def start_checker(self, *args, **kwargs):
1379
1366
        old_checker_pid = getattr(self.checker, "pid", None)
1672
1659
        self._pipe = child_pipe
1673
1660
        self._pipe.send(('init', fpr, address))
1674
1661
        if not self._pipe.recv():
1675
 
            raise KeyError(fpr)
 
1662
            raise KeyError()
1676
1663
    
1677
1664
    def __getattribute__(self, name):
1678
1665
        if name == '_pipe':
2141
2128
        
2142
2129
        if command == 'getattr':
2143
2130
            attrname = request[1]
2144
 
            if isinstance(client_object.__getattribute__(attrname),
2145
 
                          collections.Callable):
 
2131
            if callable(client_object.__getattribute__(attrname)):
2146
2132
                parent_pipe.send(('function', ))
2147
2133
            else:
2148
2134
                parent_pipe.send((
2183
2169
    # avoid excessive use of external libraries.
2184
2170
    
2185
2171
    # New type for defining tokens, syntax, and semantics all-in-one
 
2172
    Token = collections.namedtuple("Token",
 
2173
                                   ("regexp", # To match token; if
 
2174
                                              # "value" is not None,
 
2175
                                              # must have a "group"
 
2176
                                              # containing digits
 
2177
                                    "value",  # datetime.timedelta or
 
2178
                                              # None
 
2179
                                    "followers")) # Tokens valid after
 
2180
                                                  # this token
2186
2181
    Token = collections.namedtuple("Token", (
2187
2182
        "regexp",  # To match token; if "value" is not None, must have
2188
2183
                   # a "group" containing digits
2223
2218
    # Define starting values
2224
2219
    value = datetime.timedelta() # Value so far
2225
2220
    found_token = None
2226
 
    followers = frozenset((token_duration, )) # Following valid tokens
 
2221
    followers = frozenset((token_duration,)) # Following valid tokens
2227
2222
    s = duration                # String left to parse
2228
2223
    # Loop until end token is found
2229
2224
    while found_token is not token_end:
2246
2241
                break
2247
2242
        else:
2248
2243
            # No currently valid tokens were found
2249
 
            raise ValueError("Invalid RFC 3339 duration: {!r}"
2250
 
                             .format(duration))
 
2244
            raise ValueError("Invalid RFC 3339 duration")
2251
2245
    # End token found
2252
2246
    return value
2253
2247
 
2504
2498
            pidfilename = "/var/run/mandos.pid"
2505
2499
        pidfile = None
2506
2500
        try:
2507
 
            pidfile = codecs.open(pidfilename, "w", encoding="utf-8")
 
2501
            pidfile = open(pidfilename, "w")
2508
2502
        except IOError as e:
2509
2503
            logger.error("Could not open file %r", pidfilename,
2510
2504
                         exc_info=e)
2569
2563
            old_bus_name = dbus.service.BusName(
2570
2564
                "se.bsnet.fukt.Mandos", bus,
2571
2565
                do_not_queue=True)
2572
 
        except dbus.exceptions.DBusException as e:
 
2566
        except dbus.exceptions.NameExistsException as e:
2573
2567
            logger.error("Disabling D-Bus:", exc_info=e)
2574
2568
            use_dbus = False
2575
2569
            server_settings["use_dbus"] = False
2648
2642
                    pass
2649
2643
            
2650
2644
            # Clients who has passed its expire date can still be
2651
 
            # enabled if its last checker was successful.  A Client
 
2645
            # enabled if its last checker was successful.  Clients
2652
2646
            # whose checker succeeded before we stored its state is
2653
2647
            # assumed to have successfully run all checkers during
2654
2648
            # downtime.
2706
2700
    
2707
2701
    if not foreground:
2708
2702
        if pidfile is not None:
2709
 
            pid = os.getpid()
2710
2703
            try:
2711
2704
                with pidfile:
2712
 
                    print(pid, file=pidfile)
 
2705
                    pid = os.getpid()
 
2706
                    pidfile.write("{}\n".format(pid).encode("utf-8"))
2713
2707
            except IOError:
2714
2708
                logger.error("Could not write to file %r with PID %d",
2715
2709
                             pidfilename, pid)