/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-03-27 13:33:17 UTC
  • mfrom: (288.1.2 mandos-pipe-ipc)
  • Revision ID: teddy@fukt.bsnet.se-20090327133317-riwt5s5orrisozuj
Merge from pipe IPC branch.

* mandos (AvahiService.rename, main): Include PID in log messages when
                                      using a different service name.
  (Client.ReceivedSecret, Client.Rejected): New D-Bus signals.
  (TCP_handler.handle): Send IPC to parent process.
  (ForkingMixInWithPipe): New mixin class.
  (IPv6_TCPServer): Inherit from ForkingMixInWithPipe instead of
                    SocketServer.ForkingMixIn.
  (IPv6_TCPServer.handle_ipc): New method.
  (main/mandos_server): Renamed to "mandos_dbus_service" and made
                        global.
  (main/MandosServer): Renamed to "MandosDBusService".
  (main/MandosDBusService.ClientNotFound): New D-Bus signal.

Show diffs side-by-side

added added

removed removed

Lines of Context:
11
11
# and some lines in "main".
12
12
13
13
# Everything else is
14
 
# Copyright © 2007-2008 Teddy Hogeborn & Björn Påhlsson
 
14
# Copyright © 2008,2009 Teddy Hogeborn
 
15
# Copyright © 2008,2009 Björn Påhlsson
15
16
16
17
# This program is free software: you can redistribute it and/or modify
17
18
# it under the terms of the GNU General Public License as published by
24
25
#     GNU General Public License for more details.
25
26
26
27
# You should have received a copy of the GNU General Public License
27
 
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
28
# along with this program.  If not, see
 
29
# <http://www.gnu.org/licenses/>.
28
30
29
31
# Contact the authors at <mandos@fukt.bsnet.se>.
30
32
31
33
 
32
 
from __future__ import division
 
34
from __future__ import division, with_statement, absolute_import
33
35
 
34
36
import SocketServer
35
37
import socket
36
 
import select
37
 
from optparse import OptionParser
 
38
import optparse
38
39
import datetime
39
40
import errno
40
41
import gnutls.crypto
54
55
import stat
55
56
import logging
56
57
import logging.handlers
 
58
import pwd
 
59
from contextlib import closing
57
60
 
58
61
import dbus
 
62
import dbus.service
59
63
import gobject
60
64
import avahi
61
65
from dbus.mainloop.glib import DBusGMainLoop
62
66
import ctypes
 
67
import ctypes.util
63
68
 
64
 
version = "1.0"
 
69
version = "1.0.8"
65
70
 
66
71
logger = logging.Logger('mandos')
67
 
syslogger = logging.handlers.SysLogHandler\
68
 
            (facility = logging.handlers.SysLogHandler.LOG_DAEMON,
69
 
             address = "/dev/log")
70
 
syslogger.setFormatter(logging.Formatter\
71
 
                        ('Mandos: %(levelname)s: %(message)s'))
 
72
syslogger = (logging.handlers.SysLogHandler
 
73
             (facility = logging.handlers.SysLogHandler.LOG_DAEMON,
 
74
              address = "/dev/log"))
 
75
syslogger.setFormatter(logging.Formatter
 
76
                       ('Mandos [%(process)d]: %(levelname)s:'
 
77
                        ' %(message)s'))
72
78
logger.addHandler(syslogger)
73
79
 
74
80
console = logging.StreamHandler()
75
 
console.setFormatter(logging.Formatter('%(name)s: %(levelname)s:'
76
 
                                       ' %(message)s'))
 
81
console.setFormatter(logging.Formatter('%(name)s [%(process)d]:'
 
82
                                       ' %(levelname)s: %(message)s'))
77
83
logger.addHandler(console)
78
84
 
79
85
class AvahiError(Exception):
80
 
    def __init__(self, value):
 
86
    def __init__(self, value, *args, **kwargs):
81
87
        self.value = value
82
 
    def __str__(self):
83
 
        return repr(self.value)
 
88
        super(AvahiError, self).__init__(value, *args, **kwargs)
 
89
    def __unicode__(self):
 
90
        return unicode(repr(self.value))
84
91
 
85
92
class AvahiServiceError(AvahiError):
86
93
    pass
106
113
                  a sensible number of times
107
114
    """
108
115
    def __init__(self, interface = avahi.IF_UNSPEC, name = None,
109
 
                 type = None, port = None, TXT = None, domain = "",
110
 
                 host = "", max_renames = 32768):
 
116
                 servicetype = None, port = None, TXT = None,
 
117
                 domain = "", host = "", max_renames = 32768,
 
118
                 protocol = avahi.PROTO_UNSPEC):
111
119
        self.interface = interface
112
120
        self.name = name
113
 
        self.type = type
 
121
        self.type = servicetype
114
122
        self.port = port
115
 
        if TXT is None:
116
 
            self.TXT = []
117
 
        else:
118
 
            self.TXT = TXT
 
123
        self.TXT = TXT if TXT is not None else []
119
124
        self.domain = domain
120
125
        self.host = host
121
126
        self.rename_count = 0
122
127
        self.max_renames = max_renames
 
128
        self.protocol = protocol
123
129
    def rename(self):
124
130
        """Derived from the Avahi example code"""
125
131
        if self.rename_count >= self.max_renames:
126
 
            logger.critical(u"No suitable service name found after %i"
127
 
                            u" retries, exiting.", rename_count)
128
 
            raise AvahiServiceError("Too many renames")
 
132
            logger.critical(u"No suitable Zeroconf service name found"
 
133
                            u" after %i retries, exiting.",
 
134
                            self.rename_count)
 
135
            raise AvahiServiceError(u"Too many renames")
129
136
        self.name = server.GetAlternativeServiceName(self.name)
130
 
        logger.info(u"Changing name to %r ...", str(self.name))
131
 
        syslogger.setFormatter(logging.Formatter\
132
 
                               ('Mandos (%s): %%(levelname)s:'
133
 
                               ' %%(message)s' % self.name))
 
137
        logger.info(u"Changing Zeroconf service name to %r ...",
 
138
                    str(self.name))
 
139
        syslogger.setFormatter(logging.Formatter
 
140
                               ('Mandos (%s) [%%(process)d]:'
 
141
                                ' %%(levelname)s: %%(message)s'
 
142
                                % self.name))
134
143
        self.remove()
135
144
        self.add()
136
145
        self.rename_count += 1
142
151
        """Derived from the Avahi example code"""
143
152
        global group
144
153
        if group is None:
145
 
            group = dbus.Interface\
146
 
                    (bus.get_object(avahi.DBUS_NAME,
 
154
            group = dbus.Interface(bus.get_object
 
155
                                   (avahi.DBUS_NAME,
147
156
                                    server.EntryGroupNew()),
148
 
                     avahi.DBUS_INTERFACE_ENTRY_GROUP)
 
157
                                   avahi.DBUS_INTERFACE_ENTRY_GROUP)
149
158
            group.connect_to_signal('StateChanged',
150
159
                                    entry_group_state_changed)
151
 
        logger.debug(u"Adding service '%s' of type '%s' ...",
 
160
        logger.debug(u"Adding Zeroconf service '%s' of type '%s' ...",
152
161
                     service.name, service.type)
153
162
        group.AddService(
154
163
                self.interface,         # interface
155
 
                avahi.PROTO_INET6,      # protocol
 
164
                self.protocol,          # protocol
156
165
                dbus.UInt32(0),         # flags
157
166
                self.name, self.type,
158
167
                self.domain, self.host,
165
174
# End of Avahi example code
166
175
 
167
176
 
168
 
class Client(object):
 
177
def _datetime_to_dbus(dt, variant_level=0):
 
178
    """Convert a UTC datetime.datetime() to a D-Bus type."""
 
179
    return dbus.String(dt.isoformat(), variant_level=variant_level)
 
180
 
 
181
 
 
182
class Client(dbus.service.Object):
169
183
    """A representation of a client host served by this server.
170
184
    Attributes:
171
 
    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
172
187
    fingerprint: string (40 or 32 hexadecimal digits); used to
173
188
                 uniquely identify the client
174
 
    secret:    bytestring; sent verbatim (over TLS) to client
175
 
    host:      string; available for use by the checker command
176
 
    created:   datetime.datetime(); object creation, not client host
177
 
    last_checked_ok: datetime.datetime() or None if not yet checked OK
178
 
    timeout:   datetime.timedelta(); How long from last_checked_ok
179
 
                                     until this client is invalid
180
 
    interval:  datetime.timedelta(); How often to start a new checker
181
 
    stop_hook: If set, called by stop() as stop_hook(self)
182
 
    checker:   subprocess.Popen(); a running checker process used
183
 
                                   to see if the client lives.
184
 
                                   'None' if no process is running.
 
189
    secret:     bytestring; sent verbatim (over TLS) to client
 
190
    host:       string; available for use by the checker command
 
191
    created:    datetime.datetime(); (UTC) object creation
 
192
    last_enabled: datetime.datetime(); (UTC)
 
193
    enabled:    bool()
 
194
    last_checked_ok: datetime.datetime(); (UTC) or None
 
195
    timeout:    datetime.timedelta(); How long from last_checked_ok
 
196
                                      until this client is invalid
 
197
    interval:   datetime.timedelta(); How often to start a new checker
 
198
    disable_hook:  If set, called by disable() as disable_hook(self)
 
199
    checker:    subprocess.Popen(); a running checker process used
 
200
                                    to see if the client lives.
 
201
                                    'None' if no process is running.
185
202
    checker_initiator_tag: a gobject event source tag, or None
186
 
    stop_initiator_tag:    - '' -
 
203
    disable_initiator_tag:    - '' -
187
204
    checker_callback_tag:  - '' -
188
205
    checker_command: string; External command which is run to check if
189
206
                     client lives.  %() expansions are done at
190
207
                     runtime with vars(self) as dict, so that for
191
208
                     instance %(name)s can be used in the command.
192
 
    Private attibutes:
193
 
    _timeout: Real variable for 'timeout'
194
 
    _interval: Real variable for 'interval'
195
 
    _timeout_milliseconds: Used when calling gobject.timeout_add()
196
 
    _interval_milliseconds: - '' -
 
209
    current_checker_command: string; current running checker_command
 
210
    use_dbus: bool(); Whether to provide D-Bus interface and signals
 
211
    dbus_object_path: dbus.ObjectPath ; only set if self.use_dbus
197
212
    """
198
 
    def _set_timeout(self, timeout):
199
 
        "Setter function for 'timeout' attribute"
200
 
        self._timeout = timeout
201
 
        self._timeout_milliseconds = ((self.timeout.days
202
 
                                       * 24 * 60 * 60 * 1000)
203
 
                                      + (self.timeout.seconds * 1000)
204
 
                                      + (self.timeout.microseconds
205
 
                                         // 1000))
206
 
    timeout = property(lambda self: self._timeout,
207
 
                       _set_timeout)
208
 
    del _set_timeout
209
 
    def _set_interval(self, interval):
210
 
        "Setter function for 'interval' attribute"
211
 
        self._interval = interval
212
 
        self._interval_milliseconds = ((self.interval.days
213
 
                                        * 24 * 60 * 60 * 1000)
214
 
                                       + (self.interval.seconds
215
 
                                          * 1000)
216
 
                                       + (self.interval.microseconds
217
 
                                          // 1000))
218
 
    interval = property(lambda self: self._interval,
219
 
                        _set_interval)
220
 
    del _set_interval
221
 
    def __init__(self, name = None, stop_hook=None, config={}):
 
213
    def timeout_milliseconds(self):
 
214
        "Return the 'timeout' attribute in milliseconds"
 
215
        return ((self.timeout.days * 24 * 60 * 60 * 1000)
 
216
                + (self.timeout.seconds * 1000)
 
217
                + (self.timeout.microseconds // 1000))
 
218
    
 
219
    def interval_milliseconds(self):
 
220
        "Return the 'interval' attribute in milliseconds"
 
221
        return ((self.interval.days * 24 * 60 * 60 * 1000)
 
222
                + (self.interval.seconds * 1000)
 
223
                + (self.interval.microseconds // 1000))
 
224
    
 
225
    def __init__(self, name = None, disable_hook=None, config=None,
 
226
                 use_dbus=True):
222
227
        """Note: the 'checker' key in 'config' sets the
223
228
        'checker_command' attribute and *not* the 'checker'
224
229
        attribute."""
225
230
        self.name = name
 
231
        if config is None:
 
232
            config = {}
226
233
        logger.debug(u"Creating client %r", self.name)
 
234
        self.use_dbus = False   # During __init__
227
235
        # Uppercase and remove spaces from fingerprint for later
228
236
        # comparison purposes with return value from the fingerprint()
229
237
        # function
230
 
        self.fingerprint = config["fingerprint"].upper()\
231
 
                           .replace(u" ", u"")
 
238
        self.fingerprint = (config["fingerprint"].upper()
 
239
                            .replace(u" ", u""))
232
240
        logger.debug(u"  Fingerprint: %s", self.fingerprint)
233
241
        if "secret" in config:
234
242
            self.secret = config["secret"].decode(u"base64")
235
243
        elif "secfile" in config:
236
 
            sf = open(config["secfile"])
237
 
            self.secret = sf.read()
238
 
            sf.close()
 
244
            with closing(open(os.path.expanduser
 
245
                              (os.path.expandvars
 
246
                               (config["secfile"])))) as secfile:
 
247
                self.secret = secfile.read()
239
248
        else:
240
249
            raise TypeError(u"No secret or secfile for client %s"
241
250
                            % self.name)
242
251
        self.host = config.get("host", "")
243
 
        self.created = datetime.datetime.now()
 
252
        self.created = datetime.datetime.utcnow()
 
253
        self.enabled = False
 
254
        self.last_enabled = None
244
255
        self.last_checked_ok = None
245
256
        self.timeout = string_to_delta(config["timeout"])
246
257
        self.interval = string_to_delta(config["interval"])
247
 
        self.stop_hook = stop_hook
 
258
        self.disable_hook = disable_hook
248
259
        self.checker = None
249
260
        self.checker_initiator_tag = None
250
 
        self.stop_initiator_tag = None
 
261
        self.disable_initiator_tag = None
251
262
        self.checker_callback_tag = None
252
 
        self.check_command = config["checker"]
253
 
    def start(self):
 
263
        self.checker_command = config["checker"]
 
264
        self.current_checker_command = None
 
265
        self.last_connect = None
 
266
        # Only now, when this client is initialized, can it show up on
 
267
        # the D-Bus
 
268
        self.use_dbus = use_dbus
 
269
        if self.use_dbus:
 
270
            self.dbus_object_path = (dbus.ObjectPath
 
271
                                     ("/clients/"
 
272
                                      + self.name.replace(".", "_")))
 
273
            dbus.service.Object.__init__(self, bus,
 
274
                                         self.dbus_object_path)
 
275
    
 
276
    def enable(self):
254
277
        """Start this client's checker and timeout hooks"""
 
278
        self.last_enabled = datetime.datetime.utcnow()
255
279
        # Schedule a new checker to be started an 'interval' from now,
256
280
        # and every interval from then on.
257
 
        self.checker_initiator_tag = gobject.timeout_add\
258
 
                                     (self._interval_milliseconds,
259
 
                                      self.start_checker)
 
281
        self.checker_initiator_tag = (gobject.timeout_add
 
282
                                      (self.interval_milliseconds(),
 
283
                                       self.start_checker))
260
284
        # Also start a new checker *right now*.
261
285
        self.start_checker()
262
 
        # Schedule a stop() when 'timeout' has passed
263
 
        self.stop_initiator_tag = gobject.timeout_add\
264
 
                                  (self._timeout_milliseconds,
265
 
                                   self.stop)
266
 
    def stop(self):
267
 
        """Stop this client.
268
 
        The possibility that a client might be restarted is left open,
269
 
        but not currently used."""
270
 
        # If this client doesn't have a secret, it is already stopped.
271
 
        if hasattr(self, "secret") and self.secret:
272
 
            logger.info(u"Stopping client %s", self.name)
273
 
            self.secret = None
274
 
        else:
 
286
        # Schedule a disable() when 'timeout' has passed
 
287
        self.disable_initiator_tag = (gobject.timeout_add
 
288
                                   (self.timeout_milliseconds(),
 
289
                                    self.disable))
 
290
        self.enabled = True
 
291
        if self.use_dbus:
 
292
            # Emit D-Bus signals
 
293
            self.PropertyChanged(dbus.String(u"enabled"),
 
294
                                 dbus.Boolean(True, variant_level=1))
 
295
            self.PropertyChanged(dbus.String(u"last_enabled"),
 
296
                                 (_datetime_to_dbus(self.last_enabled,
 
297
                                                    variant_level=1)))
 
298
    
 
299
    def disable(self):
 
300
        """Disable this client."""
 
301
        if not getattr(self, "enabled", False):
275
302
            return False
276
 
        if getattr(self, "stop_initiator_tag", False):
277
 
            gobject.source_remove(self.stop_initiator_tag)
278
 
            self.stop_initiator_tag = None
 
303
        logger.info(u"Disabling client %s", self.name)
 
304
        if getattr(self, "disable_initiator_tag", False):
 
305
            gobject.source_remove(self.disable_initiator_tag)
 
306
            self.disable_initiator_tag = None
279
307
        if getattr(self, "checker_initiator_tag", False):
280
308
            gobject.source_remove(self.checker_initiator_tag)
281
309
            self.checker_initiator_tag = None
282
310
        self.stop_checker()
283
 
        if self.stop_hook:
284
 
            self.stop_hook(self)
 
311
        if self.disable_hook:
 
312
            self.disable_hook(self)
 
313
        self.enabled = False
 
314
        if self.use_dbus:
 
315
            # Emit D-Bus signal
 
316
            self.PropertyChanged(dbus.String(u"enabled"),
 
317
                                 dbus.Boolean(False, variant_level=1))
285
318
        # Do not run this again if called by a gobject.timeout_add
286
319
        return False
 
320
    
287
321
    def __del__(self):
288
 
        self.stop_hook = None
289
 
        self.stop()
290
 
    def checker_callback(self, pid, condition):
 
322
        self.disable_hook = None
 
323
        self.disable()
 
324
    
 
325
    def checker_callback(self, pid, condition, command):
291
326
        """The checker has completed, so take appropriate actions."""
292
 
        now = datetime.datetime.now()
293
327
        self.checker_callback_tag = None
294
328
        self.checker = None
295
 
        if os.WIFEXITED(condition) \
296
 
               and (os.WEXITSTATUS(condition) == 0):
297
 
            logger.info(u"Checker for %(name)s succeeded",
298
 
                        vars(self))
299
 
            self.last_checked_ok = now
300
 
            gobject.source_remove(self.stop_initiator_tag)
301
 
            self.stop_initiator_tag = gobject.timeout_add\
302
 
                                      (self._timeout_milliseconds,
303
 
                                       self.stop)
304
 
        elif not os.WIFEXITED(condition):
 
329
        if self.use_dbus:
 
330
            # Emit D-Bus signal
 
331
            self.PropertyChanged(dbus.String(u"checker_running"),
 
332
                                 dbus.Boolean(False, variant_level=1))
 
333
        if os.WIFEXITED(condition):
 
334
            exitstatus = os.WEXITSTATUS(condition)
 
335
            if exitstatus == 0:
 
336
                logger.info(u"Checker for %(name)s succeeded",
 
337
                            vars(self))
 
338
                self.checked_ok()
 
339
            else:
 
340
                logger.info(u"Checker for %(name)s failed",
 
341
                            vars(self))
 
342
            if self.use_dbus:
 
343
                # Emit D-Bus signal
 
344
                self.CheckerCompleted(dbus.Int16(exitstatus),
 
345
                                      dbus.Int64(condition),
 
346
                                      dbus.String(command))
 
347
        else:
305
348
            logger.warning(u"Checker for %(name)s crashed?",
306
349
                           vars(self))
307
 
        else:
308
 
            logger.info(u"Checker for %(name)s failed",
309
 
                        vars(self))
 
350
            if self.use_dbus:
 
351
                # Emit D-Bus signal
 
352
                self.CheckerCompleted(dbus.Int16(-1),
 
353
                                      dbus.Int64(condition),
 
354
                                      dbus.String(command))
 
355
    
 
356
    def checked_ok(self):
 
357
        """Bump up the timeout for this client.
 
358
        This should only be called when the client has been seen,
 
359
        alive and well.
 
360
        """
 
361
        self.last_checked_ok = datetime.datetime.utcnow()
 
362
        gobject.source_remove(self.disable_initiator_tag)
 
363
        self.disable_initiator_tag = (gobject.timeout_add
 
364
                                      (self.timeout_milliseconds(),
 
365
                                       self.disable))
 
366
        if self.use_dbus:
 
367
            # Emit D-Bus signal
 
368
            self.PropertyChanged(
 
369
                dbus.String(u"last_checked_ok"),
 
370
                (_datetime_to_dbus(self.last_checked_ok,
 
371
                                   variant_level=1)))
 
372
    
310
373
    def start_checker(self):
311
374
        """Start a new checker subprocess if one is not running.
312
375
        If a checker already exists, leave it running and do
319
382
        # checkers alone, the checker would have to take more time
320
383
        # than 'timeout' for the client to be declared invalid, which
321
384
        # is as it should be.
 
385
        
 
386
        # If a checker exists, make sure it is not a zombie
 
387
        if self.checker is not None:
 
388
            pid, status = os.waitpid(self.checker.pid, os.WNOHANG)
 
389
            if pid:
 
390
                logger.warning("Checker was a zombie")
 
391
                gobject.source_remove(self.checker_callback_tag)
 
392
                self.checker_callback(pid, status,
 
393
                                      self.current_checker_command)
 
394
        # Start a new checker if needed
322
395
        if self.checker is None:
323
396
            try:
324
 
                # In case check_command has exactly one % operator
325
 
                command = self.check_command % self.host
 
397
                # In case checker_command has exactly one % operator
 
398
                command = self.checker_command % self.host
326
399
            except TypeError:
327
400
                # Escape attributes for the shell
328
401
                escaped_attrs = dict((key, re.escape(str(val)))
329
402
                                     for key, val in
330
403
                                     vars(self).iteritems())
331
404
                try:
332
 
                    command = self.check_command % escaped_attrs
 
405
                    command = self.checker_command % escaped_attrs
333
406
                except TypeError, error:
334
407
                    logger.error(u'Could not format string "%s":'
335
 
                                 u' %s', self.check_command, error)
 
408
                                 u' %s', self.checker_command, error)
336
409
                    return True # Try again later
 
410
                self.current_checker_command = command
337
411
            try:
338
412
                logger.info(u"Starting checker %r for %s",
339
413
                            command, self.name)
 
414
                # We don't need to redirect stdout and stderr, since
 
415
                # in normal mode, that is already done by daemon(),
 
416
                # and in debug mode we don't want to.  (Stdin is
 
417
                # always replaced by /dev/null.)
340
418
                self.checker = subprocess.Popen(command,
341
419
                                                close_fds=True,
342
420
                                                shell=True, cwd="/")
343
 
                self.checker_callback_tag = gobject.child_watch_add\
344
 
                                            (self.checker.pid,
345
 
                                             self.checker_callback)
346
 
            except subprocess.OSError, error:
 
421
                if self.use_dbus:
 
422
                    # Emit D-Bus signal
 
423
                    self.CheckerStarted(command)
 
424
                    self.PropertyChanged(
 
425
                        dbus.String("checker_running"),
 
426
                        dbus.Boolean(True, variant_level=1))
 
427
                self.checker_callback_tag = (gobject.child_watch_add
 
428
                                             (self.checker.pid,
 
429
                                              self.checker_callback,
 
430
                                              data=command))
 
431
                # The checker may have completed before the gobject
 
432
                # watch was added.  Check for this.
 
433
                pid, status = os.waitpid(self.checker.pid, os.WNOHANG)
 
434
                if pid:
 
435
                    gobject.source_remove(self.checker_callback_tag)
 
436
                    self.checker_callback(pid, status, command)
 
437
            except OSError, error:
347
438
                logger.error(u"Failed to start subprocess: %s",
348
439
                             error)
349
440
        # Re-run this periodically if run by gobject.timeout_add
350
441
        return True
 
442
    
351
443
    def stop_checker(self):
352
444
        """Force the checker process, if any, to stop."""
353
445
        if self.checker_callback_tag:
365
457
            if error.errno != errno.ESRCH: # No such process
366
458
                raise
367
459
        self.checker = None
 
460
        if self.use_dbus:
 
461
            self.PropertyChanged(dbus.String(u"checker_running"),
 
462
                                 dbus.Boolean(False, variant_level=1))
 
463
    
368
464
    def still_valid(self):
369
465
        """Has the timeout not yet passed for this client?"""
370
 
        now = datetime.datetime.now()
 
466
        if not getattr(self, "enabled", False):
 
467
            return False
 
468
        now = datetime.datetime.utcnow()
371
469
        if self.last_checked_ok is None:
372
470
            return now < (self.created + self.timeout)
373
471
        else:
374
472
            return now < (self.last_checked_ok + self.timeout)
 
473
    
 
474
    ## D-Bus methods & signals
 
475
    _interface = u"se.bsnet.fukt.Mandos.Client"
 
476
    
 
477
    # CheckedOK - method
 
478
    CheckedOK = dbus.service.method(_interface)(checked_ok)
 
479
    CheckedOK.__name__ = "CheckedOK"
 
480
    
 
481
    # CheckerCompleted - signal
 
482
    @dbus.service.signal(_interface, signature="nxs")
 
483
    def CheckerCompleted(self, exitcode, waitstatus, command):
 
484
        "D-Bus signal"
 
485
        pass
 
486
    
 
487
    # CheckerStarted - signal
 
488
    @dbus.service.signal(_interface, signature="s")
 
489
    def CheckerStarted(self, command):
 
490
        "D-Bus signal"
 
491
        pass
 
492
    
 
493
    # GetAllProperties - method
 
494
    @dbus.service.method(_interface, out_signature="a{sv}")
 
495
    def GetAllProperties(self):
 
496
        "D-Bus method"
 
497
        return dbus.Dictionary({
 
498
                dbus.String("name"):
 
499
                    dbus.String(self.name, variant_level=1),
 
500
                dbus.String("fingerprint"):
 
501
                    dbus.String(self.fingerprint, variant_level=1),
 
502
                dbus.String("host"):
 
503
                    dbus.String(self.host, variant_level=1),
 
504
                dbus.String("created"):
 
505
                    _datetime_to_dbus(self.created, variant_level=1),
 
506
                dbus.String("last_enabled"):
 
507
                    (_datetime_to_dbus(self.last_enabled,
 
508
                                       variant_level=1)
 
509
                     if self.last_enabled is not None
 
510
                     else dbus.Boolean(False, variant_level=1)),
 
511
                dbus.String("enabled"):
 
512
                    dbus.Boolean(self.enabled, variant_level=1),
 
513
                dbus.String("last_checked_ok"):
 
514
                    (_datetime_to_dbus(self.last_checked_ok,
 
515
                                       variant_level=1)
 
516
                     if self.last_checked_ok is not None
 
517
                     else dbus.Boolean (False, variant_level=1)),
 
518
                dbus.String("timeout"):
 
519
                    dbus.UInt64(self.timeout_milliseconds(),
 
520
                                variant_level=1),
 
521
                dbus.String("interval"):
 
522
                    dbus.UInt64(self.interval_milliseconds(),
 
523
                                variant_level=1),
 
524
                dbus.String("checker"):
 
525
                    dbus.String(self.checker_command,
 
526
                                variant_level=1),
 
527
                dbus.String("checker_running"):
 
528
                    dbus.Boolean(self.checker is not None,
 
529
                                 variant_level=1),
 
530
                dbus.String("object_path"):
 
531
                    dbus.ObjectPath(self.dbus_object_path,
 
532
                                    variant_level=1)
 
533
                }, signature="sv")
 
534
    
 
535
    # IsStillValid - method
 
536
    IsStillValid = (dbus.service.method(_interface, out_signature="b")
 
537
                    (still_valid))
 
538
    IsStillValid.__name__ = "IsStillValid"
 
539
    
 
540
    # PropertyChanged - signal
 
541
    @dbus.service.signal(_interface, signature="sv")
 
542
    def PropertyChanged(self, property, value):
 
543
        "D-Bus signal"
 
544
        pass
 
545
    
 
546
    # ReceivedSecret - signal
 
547
    @dbus.service.signal(_interface)
 
548
    def ReceivedSecret(self):
 
549
        "D-Bus signal"
 
550
        pass
 
551
    
 
552
    # Rejected - signal
 
553
    @dbus.service.signal(_interface)
 
554
    def Rejected(self):
 
555
        "D-Bus signal"
 
556
        pass
 
557
    
 
558
    # SetChecker - method
 
559
    @dbus.service.method(_interface, in_signature="s")
 
560
    def SetChecker(self, checker):
 
561
        "D-Bus setter method"
 
562
        self.checker_command = checker
 
563
        # Emit D-Bus signal
 
564
        self.PropertyChanged(dbus.String(u"checker"),
 
565
                             dbus.String(self.checker_command,
 
566
                                         variant_level=1))
 
567
    
 
568
    # SetHost - method
 
569
    @dbus.service.method(_interface, in_signature="s")
 
570
    def SetHost(self, host):
 
571
        "D-Bus setter method"
 
572
        self.host = host
 
573
        # Emit D-Bus signal
 
574
        self.PropertyChanged(dbus.String(u"host"),
 
575
                             dbus.String(self.host, variant_level=1))
 
576
    
 
577
    # SetInterval - method
 
578
    @dbus.service.method(_interface, in_signature="t")
 
579
    def SetInterval(self, milliseconds):
 
580
        self.interval = datetime.timedelta(0, 0, 0, milliseconds)
 
581
        # Emit D-Bus signal
 
582
        self.PropertyChanged(dbus.String(u"interval"),
 
583
                             (dbus.UInt64(self.interval_milliseconds(),
 
584
                                          variant_level=1)))
 
585
    
 
586
    # SetSecret - method
 
587
    @dbus.service.method(_interface, in_signature="ay",
 
588
                         byte_arrays=True)
 
589
    def SetSecret(self, secret):
 
590
        "D-Bus setter method"
 
591
        self.secret = str(secret)
 
592
    
 
593
    # SetTimeout - method
 
594
    @dbus.service.method(_interface, in_signature="t")
 
595
    def SetTimeout(self, milliseconds):
 
596
        self.timeout = datetime.timedelta(0, 0, 0, milliseconds)
 
597
        # Emit D-Bus signal
 
598
        self.PropertyChanged(dbus.String(u"timeout"),
 
599
                             (dbus.UInt64(self.timeout_milliseconds(),
 
600
                                          variant_level=1)))
 
601
    
 
602
    # Enable - method
 
603
    Enable = dbus.service.method(_interface)(enable)
 
604
    Enable.__name__ = "Enable"
 
605
    
 
606
    # StartChecker - method
 
607
    @dbus.service.method(_interface)
 
608
    def StartChecker(self):
 
609
        "D-Bus method"
 
610
        self.start_checker()
 
611
    
 
612
    # Disable - method
 
613
    @dbus.service.method(_interface)
 
614
    def Disable(self):
 
615
        "D-Bus method"
 
616
        self.disable()
 
617
    
 
618
    # StopChecker - method
 
619
    StopChecker = dbus.service.method(_interface)(stop_checker)
 
620
    StopChecker.__name__ = "StopChecker"
 
621
    
 
622
    del _interface
375
623
 
376
624
 
377
625
def peer_certificate(session):
378
626
    "Return the peer's OpenPGP certificate as a bytestring"
379
627
    # If not an OpenPGP certificate...
380
 
    if gnutls.library.functions.gnutls_certificate_type_get\
381
 
            (session._c_object) \
382
 
           != gnutls.library.constants.GNUTLS_CRT_OPENPGP:
 
628
    if (gnutls.library.functions
 
629
        .gnutls_certificate_type_get(session._c_object)
 
630
        != gnutls.library.constants.GNUTLS_CRT_OPENPGP):
383
631
        # ...do the normal thing
384
632
        return session.peer_certificate
385
 
    list_size = ctypes.c_uint()
386
 
    cert_list = gnutls.library.functions.gnutls_certificate_get_peers\
387
 
        (session._c_object, ctypes.byref(list_size))
 
633
    list_size = ctypes.c_uint(1)
 
634
    cert_list = (gnutls.library.functions
 
635
                 .gnutls_certificate_get_peers
 
636
                 (session._c_object, ctypes.byref(list_size)))
 
637
    if not bool(cert_list) and list_size.value != 0:
 
638
        raise gnutls.errors.GNUTLSError("error getting peer"
 
639
                                        " certificate")
388
640
    if list_size.value == 0:
389
641
        return None
390
642
    cert = cert_list[0]
394
646
def fingerprint(openpgp):
395
647
    "Convert an OpenPGP bytestring to a hexdigit fingerprint string"
396
648
    # New GnuTLS "datum" with the OpenPGP public key
397
 
    datum = gnutls.library.types.gnutls_datum_t\
398
 
        (ctypes.cast(ctypes.c_char_p(openpgp),
399
 
                     ctypes.POINTER(ctypes.c_ubyte)),
400
 
         ctypes.c_uint(len(openpgp)))
 
649
    datum = (gnutls.library.types
 
650
             .gnutls_datum_t(ctypes.cast(ctypes.c_char_p(openpgp),
 
651
                                         ctypes.POINTER
 
652
                                         (ctypes.c_ubyte)),
 
653
                             ctypes.c_uint(len(openpgp))))
401
654
    # New empty GnuTLS certificate
402
655
    crt = gnutls.library.types.gnutls_openpgp_crt_t()
403
 
    gnutls.library.functions.gnutls_openpgp_crt_init\
404
 
        (ctypes.byref(crt))
 
656
    (gnutls.library.functions
 
657
     .gnutls_openpgp_crt_init(ctypes.byref(crt)))
405
658
    # Import the OpenPGP public key into the certificate
406
 
    gnutls.library.functions.gnutls_openpgp_crt_import\
407
 
                    (crt, ctypes.byref(datum),
408
 
                     gnutls.library.constants.GNUTLS_OPENPGP_FMT_RAW)
 
659
    (gnutls.library.functions
 
660
     .gnutls_openpgp_crt_import(crt, ctypes.byref(datum),
 
661
                                gnutls.library.constants
 
662
                                .GNUTLS_OPENPGP_FMT_RAW))
 
663
    # Verify the self signature in the key
 
664
    crtverify = ctypes.c_uint()
 
665
    (gnutls.library.functions
 
666
     .gnutls_openpgp_crt_verify_self(crt, 0, ctypes.byref(crtverify)))
 
667
    if crtverify.value != 0:
 
668
        gnutls.library.functions.gnutls_openpgp_crt_deinit(crt)
 
669
        raise gnutls.errors.CertificateSecurityError("Verify failed")
409
670
    # New buffer for the fingerprint
410
 
    buffer = ctypes.create_string_buffer(20)
411
 
    buffer_length = ctypes.c_size_t()
 
671
    buf = ctypes.create_string_buffer(20)
 
672
    buf_len = ctypes.c_size_t()
412
673
    # Get the fingerprint from the certificate into the buffer
413
 
    gnutls.library.functions.gnutls_openpgp_crt_get_fingerprint\
414
 
        (crt, ctypes.byref(buffer), ctypes.byref(buffer_length))
 
674
    (gnutls.library.functions
 
675
     .gnutls_openpgp_crt_get_fingerprint(crt, ctypes.byref(buf),
 
676
                                         ctypes.byref(buf_len)))
415
677
    # Deinit the certificate
416
678
    gnutls.library.functions.gnutls_openpgp_crt_deinit(crt)
417
679
    # Convert the buffer to a Python bytestring
418
 
    fpr = ctypes.string_at(buffer, buffer_length.value)
 
680
    fpr = ctypes.string_at(buf, buf_len.value)
419
681
    # Convert the bytestring to hexadecimal notation
420
682
    hex_fpr = u''.join(u"%02X" % ord(char) for char in fpr)
421
683
    return hex_fpr
422
684
 
423
685
 
424
 
class tcp_handler(SocketServer.BaseRequestHandler, object):
 
686
class TCP_handler(SocketServer.BaseRequestHandler, object):
425
687
    """A TCP request handler class.
426
688
    Instantiated by IPv6_TCPServer for each request to handle it.
427
689
    Note: This will run in its own forked process."""
428
690
    
429
691
    def handle(self):
430
692
        logger.info(u"TCP connection from: %s",
431
 
                     unicode(self.client_address))
432
 
        session = gnutls.connection.ClientSession\
433
 
                  (self.request, gnutls.connection.X509Credentials())
434
 
        
435
 
        line = self.request.makefile().readline()
436
 
        logger.debug(u"Protocol version: %r", line)
437
 
        try:
438
 
            if int(line.strip().split()[0]) > 1:
439
 
                raise RuntimeError
440
 
        except (ValueError, IndexError, RuntimeError), error:
441
 
            logger.error(u"Unknown protocol version: %s", error)
442
 
            return
443
 
        
444
 
        # Note: gnutls.connection.X509Credentials is really a generic
445
 
        # GnuTLS certificate credentials object so long as no X.509
446
 
        # keys are added to it.  Therefore, we can use it here despite
447
 
        # using OpenPGP certificates.
448
 
        
449
 
        #priority = ':'.join(("NONE", "+VERS-TLS1.1", "+AES-256-CBC",
450
 
        #                "+SHA1", "+COMP-NULL", "+CTYPE-OPENPGP",
451
 
        #                "+DHE-DSS"))
452
 
        priority = "NORMAL"             # Fallback default, since this
453
 
                                        # MUST be set.
454
 
        if self.server.settings["priority"]:
455
 
            priority = self.server.settings["priority"]
456
 
        gnutls.library.functions.gnutls_priority_set_direct\
457
 
            (session._c_object, priority, None);
458
 
        
459
 
        try:
460
 
            session.handshake()
461
 
        except gnutls.errors.GNUTLSError, error:
462
 
            logger.warning(u"Handshake failed: %s", error)
463
 
            # Do not run session.bye() here: the session is not
464
 
            # established.  Just abandon the request.
465
 
            return
466
 
        try:
467
 
            fpr = fingerprint(peer_certificate(session))
468
 
        except (TypeError, gnutls.errors.GNUTLSError), error:
469
 
            logger.warning(u"Bad certificate: %s", error)
470
 
            session.bye()
471
 
            return
472
 
        logger.debug(u"Fingerprint: %s", fpr)
473
 
        client = None
474
 
        for c in self.server.clients:
475
 
            if c.fingerprint == fpr:
476
 
                client = c
477
 
                break
478
 
        if not client:
479
 
            logger.warning(u"Client not found for fingerprint: %s",
480
 
                           fpr)
481
 
            session.bye()
482
 
            return
483
 
        # Have to check if client.still_valid(), since it is possible
484
 
        # that the client timed out while establishing the GnuTLS
485
 
        # session.
486
 
        if not client.still_valid():
487
 
            logger.warning(u"Client %(name)s is invalid",
488
 
                           vars(client))
489
 
            session.bye()
490
 
            return
491
 
        sent_size = 0
492
 
        while sent_size < len(client.secret):
493
 
            sent = session.send(client.secret[sent_size:])
494
 
            logger.debug(u"Sent: %d, remaining: %d",
495
 
                         sent, len(client.secret)
496
 
                         - (sent_size + sent))
497
 
            sent_size += sent
498
 
        session.bye()
499
 
 
500
 
 
501
 
class IPv6_TCPServer(SocketServer.ForkingTCPServer, object):
502
 
    """IPv6 TCP server.  Accepts 'None' as address and/or port.
 
693
                    unicode(self.client_address))
 
694
        logger.debug(u"IPC Pipe FD: %d", self.server.pipe[1])
 
695
        # Open IPC pipe to parent process
 
696
        with closing(os.fdopen(self.server.pipe[1], "w", 1)) as ipc:
 
697
            session = (gnutls.connection
 
698
                       .ClientSession(self.request,
 
699
                                      gnutls.connection
 
700
                                      .X509Credentials()))
 
701
            
 
702
            line = self.request.makefile().readline()
 
703
            logger.debug(u"Protocol version: %r", line)
 
704
            try:
 
705
                if int(line.strip().split()[0]) > 1:
 
706
                    raise RuntimeError
 
707
            except (ValueError, IndexError, RuntimeError), error:
 
708
                logger.error(u"Unknown protocol version: %s", error)
 
709
                return
 
710
            
 
711
            # Note: gnutls.connection.X509Credentials is really a
 
712
            # generic GnuTLS certificate credentials object so long as
 
713
            # no X.509 keys are added to it.  Therefore, we can use it
 
714
            # here despite using OpenPGP certificates.
 
715
            
 
716
            #priority = ':'.join(("NONE", "+VERS-TLS1.1",
 
717
            #                     "+AES-256-CBC", "+SHA1",
 
718
            #                     "+COMP-NULL", "+CTYPE-OPENPGP",
 
719
            #                     "+DHE-DSS"))
 
720
            # Use a fallback default, since this MUST be set.
 
721
            priority = self.server.settings.get("priority", "NORMAL")
 
722
            (gnutls.library.functions
 
723
             .gnutls_priority_set_direct(session._c_object,
 
724
                                         priority, None))
 
725
            
 
726
            try:
 
727
                session.handshake()
 
728
            except gnutls.errors.GNUTLSError, error:
 
729
                logger.warning(u"Handshake failed: %s", error)
 
730
                # Do not run session.bye() here: the session is not
 
731
                # established.  Just abandon the request.
 
732
                return
 
733
            logger.debug(u"Handshake succeeded")
 
734
            try:
 
735
                fpr = fingerprint(peer_certificate(session))
 
736
            except (TypeError, gnutls.errors.GNUTLSError), error:
 
737
                logger.warning(u"Bad certificate: %s", error)
 
738
                session.bye()
 
739
                return
 
740
            logger.debug(u"Fingerprint: %s", fpr)
 
741
            
 
742
            for c in self.server.clients:
 
743
                if c.fingerprint == fpr:
 
744
                    client = c
 
745
                    break
 
746
            else:
 
747
                logger.warning(u"Client not found for fingerprint: %s",
 
748
                               fpr)
 
749
                ipc.write("NOTFOUND %s\n" % fpr)
 
750
                session.bye()
 
751
                return
 
752
            # Have to check if client.still_valid(), since it is
 
753
            # possible that the client timed out while establishing
 
754
            # the GnuTLS session.
 
755
            if not client.still_valid():
 
756
                logger.warning(u"Client %(name)s is invalid",
 
757
                               vars(client))
 
758
                ipc.write("INVALID %s\n" % client.name)
 
759
                session.bye()
 
760
                return
 
761
            ipc.write("SENDING %s\n" % client.name)
 
762
            sent_size = 0
 
763
            while sent_size < len(client.secret):
 
764
                sent = session.send(client.secret[sent_size:])
 
765
                logger.debug(u"Sent: %d, remaining: %d",
 
766
                             sent, len(client.secret)
 
767
                             - (sent_size + sent))
 
768
                sent_size += sent
 
769
            session.bye()
 
770
 
 
771
 
 
772
class ForkingMixInWithPipe(SocketServer.ForkingMixIn, object):
 
773
    """Like SocketServer.ForkingMixIn, but also pass a pipe.
 
774
    Assumes a gobject.MainLoop event loop.
 
775
    """
 
776
    def process_request(self, request, client_address):
 
777
        """This overrides and wraps the original process_request().
 
778
        This function creates a new pipe in self.pipe 
 
779
        """
 
780
        self.pipe = os.pipe()
 
781
        super(ForkingMixInWithPipe,
 
782
              self).process_request(request, client_address)
 
783
        os.close(self.pipe[1])  # close write end
 
784
        # Call "handle_ipc" for both data and EOF events
 
785
        gobject.io_add_watch(self.pipe[0],
 
786
                             gobject.IO_IN | gobject.IO_HUP,
 
787
                             self.handle_ipc)
 
788
    def handle_ipc(source, condition):
 
789
        """Dummy function; override as necessary"""
 
790
        os.close(source)
 
791
        return False
 
792
 
 
793
 
 
794
class IPv6_TCPServer(ForkingMixInWithPipe,
 
795
                     SocketServer.TCPServer, object):
 
796
    """IPv6-capable TCP server.  Accepts 'None' as address and/or port
503
797
    Attributes:
504
798
        settings:       Server settings
505
799
        clients:        Set() of Client objects
 
800
        enabled:        Boolean; whether this server is activated yet
506
801
    """
507
802
    address_family = socket.AF_INET6
508
803
    def __init__(self, *args, **kwargs):
512
807
        if "clients" in kwargs:
513
808
            self.clients = kwargs["clients"]
514
809
            del kwargs["clients"]
515
 
        return super(type(self), self).__init__(*args, **kwargs)
 
810
        if "use_ipv6" in kwargs:
 
811
            if not kwargs["use_ipv6"]:
 
812
                self.address_family = socket.AF_INET
 
813
            del kwargs["use_ipv6"]
 
814
        self.enabled = False
 
815
        super(IPv6_TCPServer, self).__init__(*args, **kwargs)
516
816
    def server_bind(self):
517
817
        """This overrides the normal server_bind() function
518
818
        to bind to an interface if one was specified, and also NOT to
530
830
                                 u" bind to interface %s",
531
831
                                 self.settings["interface"])
532
832
                else:
533
 
                    raise error
 
833
                    raise
534
834
        # Only bind(2) the socket if we really need to.
535
835
        if self.server_address[0] or self.server_address[1]:
536
836
            if not self.server_address[0]:
537
 
                in6addr_any = "::"
538
 
                self.server_address = (in6addr_any,
 
837
                if self.address_family == socket.AF_INET6:
 
838
                    any_address = "::" # in6addr_any
 
839
                else:
 
840
                    any_address = socket.INADDR_ANY
 
841
                self.server_address = (any_address,
539
842
                                       self.server_address[1])
540
843
            elif not self.server_address[1]:
541
844
                self.server_address = (self.server_address[0],
547
850
#                                            if_nametoindex
548
851
#                                            (self.settings
549
852
#                                             ["interface"]))
550
 
            return super(type(self), self).server_bind()
 
853
            return super(IPv6_TCPServer, self).server_bind()
 
854
    def server_activate(self):
 
855
        if self.enabled:
 
856
            return super(IPv6_TCPServer, self).server_activate()
 
857
    def enable(self):
 
858
        self.enabled = True
 
859
    def handle_ipc(self, source, condition, file_objects={}):
 
860
        condition_names = {
 
861
            gobject.IO_IN: "IN", # There is data to read.
 
862
            gobject.IO_OUT: "OUT", # Data can be written (without
 
863
                                   # blocking).
 
864
            gobject.IO_PRI: "PRI", # There is urgent data to read.
 
865
            gobject.IO_ERR: "ERR", # Error condition.
 
866
            gobject.IO_HUP: "HUP"  # Hung up (the connection has been
 
867
                                   # broken, usually for pipes and
 
868
                                   # sockets).
 
869
            }
 
870
        conditions_string = ' | '.join(name
 
871
                                       for cond, name in
 
872
                                       condition_names.iteritems()
 
873
                                       if cond & condition)
 
874
        logger.debug("Handling IPC: FD = %d, condition = %s", source,
 
875
                     conditions_string)
 
876
        
 
877
        # Turn the pipe file descriptor into a Python file object
 
878
        if source not in file_objects:
 
879
            file_objects[source] = os.fdopen(source, "r", 1)
 
880
        
 
881
        # Read a line from the file object
 
882
        cmdline = file_objects[source].readline()
 
883
        if not cmdline:             # Empty line means end of file
 
884
            # close the IPC pipe
 
885
            file_objects[source].close()
 
886
            del file_objects[source]
 
887
            
 
888
            # Stop calling this function
 
889
            return False
 
890
        
 
891
        logger.debug("IPC command: %r\n" % cmdline)
 
892
        
 
893
        # Parse and act on command
 
894
        cmd, args = cmdline.split(None, 1)
 
895
        if cmd == "NOTFOUND":
 
896
            if self.settings["use_dbus"]:
 
897
                # Emit D-Bus signal
 
898
                mandos_dbus_service.ClientNotFound(args)
 
899
        elif cmd == "INVALID":
 
900
            if self.settings["use_dbus"]:
 
901
                for client in self.clients:
 
902
                    if client.name == args:
 
903
                        # Emit D-Bus signal
 
904
                        client.Rejected()
 
905
                        break
 
906
        elif cmd == "SENDING":
 
907
            for client in self.clients:
 
908
                if client.name == args:
 
909
                    client.checked_ok()
 
910
                    if self.settings["use_dbus"]:
 
911
                        # Emit D-Bus signal
 
912
                        client.ReceivedSecret()
 
913
                    break
 
914
        else:
 
915
            logger.error("Unknown IPC command: %r", cmdline)
 
916
        
 
917
        # Keep calling this function
 
918
        return True
551
919
 
552
920
 
553
921
def string_to_delta(interval):
554
922
    """Parse a string and return a datetime.timedelta
555
 
 
 
923
    
556
924
    >>> string_to_delta('7d')
557
925
    datetime.timedelta(7)
558
926
    >>> string_to_delta('60s')
563
931
    datetime.timedelta(1)
564
932
    >>> string_to_delta(u'1w')
565
933
    datetime.timedelta(7)
 
934
    >>> string_to_delta('5m 30s')
 
935
    datetime.timedelta(0, 330)
566
936
    """
567
 
    try:
568
 
        suffix=unicode(interval[-1])
569
 
        value=int(interval[:-1])
570
 
        if suffix == u"d":
571
 
            delta = datetime.timedelta(value)
572
 
        elif suffix == u"s":
573
 
            delta = datetime.timedelta(0, value)
574
 
        elif suffix == u"m":
575
 
            delta = datetime.timedelta(0, 0, 0, 0, value)
576
 
        elif suffix == u"h":
577
 
            delta = datetime.timedelta(0, 0, 0, 0, 0, value)
578
 
        elif suffix == u"w":
579
 
            delta = datetime.timedelta(0, 0, 0, 0, 0, 0, value)
580
 
        else:
 
937
    timevalue = datetime.timedelta(0)
 
938
    for s in interval.split():
 
939
        try:
 
940
            suffix = unicode(s[-1])
 
941
            value = int(s[:-1])
 
942
            if suffix == u"d":
 
943
                delta = datetime.timedelta(value)
 
944
            elif suffix == u"s":
 
945
                delta = datetime.timedelta(0, value)
 
946
            elif suffix == u"m":
 
947
                delta = datetime.timedelta(0, 0, 0, 0, value)
 
948
            elif suffix == u"h":
 
949
                delta = datetime.timedelta(0, 0, 0, 0, 0, value)
 
950
            elif suffix == u"w":
 
951
                delta = datetime.timedelta(0, 0, 0, 0, 0, 0, value)
 
952
            else:
 
953
                raise ValueError
 
954
        except (ValueError, IndexError):
581
955
            raise ValueError
582
 
    except (ValueError, IndexError):
583
 
        raise ValueError
584
 
    return delta
 
956
        timevalue += delta
 
957
    return timevalue
585
958
 
586
959
 
587
960
def server_state_changed(state):
588
961
    """Derived from the Avahi example code"""
589
962
    if state == avahi.SERVER_COLLISION:
590
 
        logger.error(u"Server name collision")
 
963
        logger.error(u"Zeroconf server name collision")
591
964
        service.remove()
592
965
    elif state == avahi.SERVER_RUNNING:
593
966
        service.add()
595
968
 
596
969
def entry_group_state_changed(state, error):
597
970
    """Derived from the Avahi example code"""
598
 
    logger.debug(u"state change: %i", state)
 
971
    logger.debug(u"Avahi state change: %i", state)
599
972
    
600
973
    if state == avahi.ENTRY_GROUP_ESTABLISHED:
601
 
        logger.debug(u"Service established.")
 
974
        logger.debug(u"Zeroconf service established.")
602
975
    elif state == avahi.ENTRY_GROUP_COLLISION:
603
 
        logger.warning(u"Service name collision.")
 
976
        logger.warning(u"Zeroconf service name collision.")
604
977
        service.rename()
605
978
    elif state == avahi.ENTRY_GROUP_FAILURE:
606
 
        logger.critical(u"Error in group state changed %s",
 
979
        logger.critical(u"Avahi: Error in group state changed %s",
607
980
                        unicode(error))
608
 
        raise AvahiGroupError("State changed: %s", str(error))
 
981
        raise AvahiGroupError(u"State changed: %s" % unicode(error))
609
982
 
610
983
def if_nametoindex(interface):
611
984
    """Call the C function if_nametoindex(), or equivalent"""
612
985
    global if_nametoindex
613
986
    try:
614
 
        if "ctypes.util" not in sys.modules:
615
 
            import ctypes.util
616
 
        if_nametoindex = ctypes.cdll.LoadLibrary\
617
 
            (ctypes.util.find_library("c")).if_nametoindex
 
987
        if_nametoindex = (ctypes.cdll.LoadLibrary
 
988
                          (ctypes.util.find_library("c"))
 
989
                          .if_nametoindex)
618
990
    except (OSError, AttributeError):
619
991
        if "struct" not in sys.modules:
620
992
            import struct
623
995
        def if_nametoindex(interface):
624
996
            "Get an interface index the hard way, i.e. using fcntl()"
625
997
            SIOCGIFINDEX = 0x8933  # From /usr/include/linux/sockios.h
626
 
            s = socket.socket()
627
 
            ifreq = fcntl.ioctl(s, SIOCGIFINDEX,
628
 
                                struct.pack("16s16x", interface))
629
 
            s.close()
 
998
            with closing(socket.socket()) as s:
 
999
                ifreq = fcntl.ioctl(s, SIOCGIFINDEX,
 
1000
                                    struct.pack("16s16x", interface))
630
1001
            interface_index = struct.unpack("I", ifreq[16:20])[0]
631
1002
            return interface_index
632
1003
    return if_nametoindex(interface)
656
1027
 
657
1028
 
658
1029
def main():
659
 
    global main_loop_started
660
 
    main_loop_started = False
661
 
    
662
 
    parser = OptionParser(version = "%%prog %s" % version)
 
1030
    
 
1031
    ######################################################################
 
1032
    # Parsing of options, both command line and config file
 
1033
    
 
1034
    parser = optparse.OptionParser(version = "%%prog %s" % version)
663
1035
    parser.add_option("-i", "--interface", type="string",
664
1036
                      metavar="IF", help="Bind to interface IF")
665
1037
    parser.add_option("-a", "--address", type="string",
666
1038
                      help="Address to listen for requests on")
667
1039
    parser.add_option("-p", "--port", type="int",
668
1040
                      help="Port number to receive requests on")
669
 
    parser.add_option("--check", action="store_true", default=False,
 
1041
    parser.add_option("--check", action="store_true",
670
1042
                      help="Run self-test")
671
1043
    parser.add_option("--debug", action="store_true",
672
1044
                      help="Debug mode; run in foreground and log to"
679
1051
                      default="/etc/mandos", metavar="DIR",
680
1052
                      help="Directory to search for configuration"
681
1053
                      " files")
682
 
    (options, args) = parser.parse_args()
 
1054
    parser.add_option("--no-dbus", action="store_false",
 
1055
                      dest="use_dbus",
 
1056
                      help="Do not provide D-Bus system bus"
 
1057
                      " interface")
 
1058
    parser.add_option("--no-ipv6", action="store_false",
 
1059
                      dest="use_ipv6", help="Do not use IPv6")
 
1060
    options = parser.parse_args()[0]
683
1061
    
684
1062
    if options.check:
685
1063
        import doctest
694
1072
                        "priority":
695
1073
                        "SECURE256:!CTYPE-X.509:+CTYPE-OPENPGP",
696
1074
                        "servicename": "Mandos",
 
1075
                        "use_dbus": "True",
 
1076
                        "use_ipv6": "True",
697
1077
                        }
698
1078
    
699
1079
    # Parse config file for server-global settings
700
1080
    server_config = ConfigParser.SafeConfigParser(server_defaults)
701
1081
    del server_defaults
702
1082
    server_config.read(os.path.join(options.configdir, "mandos.conf"))
703
 
    server_section = "server"
704
1083
    # Convert the SafeConfigParser object to a dict
705
 
    server_settings = dict(server_config.items(server_section))
706
 
    # Use getboolean on the boolean config option
707
 
    server_settings["debug"] = server_config.getboolean\
708
 
                               (server_section, "debug")
 
1084
    server_settings = server_config.defaults()
 
1085
    # Use the appropriate methods on the non-string config options
 
1086
    server_settings["debug"] = server_config.getboolean("DEFAULT",
 
1087
                                                        "debug")
 
1088
    server_settings["use_dbus"] = server_config.getboolean("DEFAULT",
 
1089
                                                           "use_dbus")
 
1090
    server_settings["use_ipv6"] = server_config.getboolean("DEFAULT",
 
1091
                                                           "use_ipv6")
 
1092
    if server_settings["port"]:
 
1093
        server_settings["port"] = server_config.getint("DEFAULT",
 
1094
                                                       "port")
709
1095
    del server_config
710
1096
    
711
1097
    # Override the settings from the config file with command line
712
1098
    # options, if set.
713
1099
    for option in ("interface", "address", "port", "debug",
714
 
                   "priority", "servicename", "configdir"):
 
1100
                   "priority", "servicename", "configdir",
 
1101
                   "use_dbus", "use_ipv6"):
715
1102
        value = getattr(options, option)
716
1103
        if value is not None:
717
1104
            server_settings[option] = value
718
1105
    del options
719
1106
    # Now we have our good server settings in "server_settings"
720
1107
    
 
1108
    ##################################################################
 
1109
    
 
1110
    # For convenience
721
1111
    debug = server_settings["debug"]
 
1112
    use_dbus = server_settings["use_dbus"]
 
1113
    use_ipv6 = server_settings["use_ipv6"]
722
1114
    
723
1115
    if not debug:
724
1116
        syslogger.setLevel(logging.WARNING)
725
1117
        console.setLevel(logging.WARNING)
726
1118
    
727
1119
    if server_settings["servicename"] != "Mandos":
728
 
        syslogger.setFormatter(logging.Formatter\
729
 
                               ('Mandos (%s): %%(levelname)s:'
730
 
                                ' %%(message)s'
 
1120
        syslogger.setFormatter(logging.Formatter
 
1121
                               ('Mandos (%s) [%%(process)d]:'
 
1122
                                ' %%(levelname)s: %%(message)s'
731
1123
                                % server_settings["servicename"]))
732
1124
    
733
1125
    # Parse config file with clients
734
1126
    client_defaults = { "timeout": "1h",
735
1127
                        "interval": "5m",
736
1128
                        "checker": "fping -q -- %%(host)s",
 
1129
                        "host": "",
737
1130
                        }
738
1131
    client_config = ConfigParser.SafeConfigParser(client_defaults)
739
1132
    client_config.read(os.path.join(server_settings["configdir"],
740
1133
                                    "clients.conf"))
 
1134
 
 
1135
    global mandos_dbus_service
 
1136
    mandos_dbus_service = None
 
1137
    
 
1138
    clients = Set()
 
1139
    tcp_server = IPv6_TCPServer((server_settings["address"],
 
1140
                                 server_settings["port"]),
 
1141
                                TCP_handler,
 
1142
                                settings=server_settings,
 
1143
                                clients=clients, use_ipv6=use_ipv6)
 
1144
    pidfilename = "/var/run/mandos.pid"
 
1145
    try:
 
1146
        pidfile = open(pidfilename, "w")
 
1147
    except IOError:
 
1148
        logger.error("Could not open file %r", pidfilename)
 
1149
    
 
1150
    try:
 
1151
        uid = pwd.getpwnam("_mandos").pw_uid
 
1152
        gid = pwd.getpwnam("_mandos").pw_gid
 
1153
    except KeyError:
 
1154
        try:
 
1155
            uid = pwd.getpwnam("mandos").pw_uid
 
1156
            gid = pwd.getpwnam("mandos").pw_gid
 
1157
        except KeyError:
 
1158
            try:
 
1159
                uid = pwd.getpwnam("nobody").pw_uid
 
1160
                gid = pwd.getpwnam("nogroup").pw_gid
 
1161
            except KeyError:
 
1162
                uid = 65534
 
1163
                gid = 65534
 
1164
    try:
 
1165
        os.setgid(gid)
 
1166
        os.setuid(uid)
 
1167
    except OSError, error:
 
1168
        if error[0] != errno.EPERM:
 
1169
            raise error
 
1170
    
 
1171
    # Enable all possible GnuTLS debugging
 
1172
    if debug:
 
1173
        # "Use a log level over 10 to enable all debugging options."
 
1174
        # - GnuTLS manual
 
1175
        gnutls.library.functions.gnutls_global_set_log_level(11)
 
1176
        
 
1177
        @gnutls.library.types.gnutls_log_func
 
1178
        def debug_gnutls(level, string):
 
1179
            logger.debug("GnuTLS: %s", string[:-1])
 
1180
        
 
1181
        (gnutls.library.functions
 
1182
         .gnutls_global_set_log_function(debug_gnutls))
741
1183
    
742
1184
    global service
 
1185
    protocol = avahi.PROTO_INET6 if use_ipv6 else avahi.PROTO_INET
743
1186
    service = AvahiService(name = server_settings["servicename"],
744
 
                           type = "_mandos._tcp", );
 
1187
                           servicetype = "_mandos._tcp",
 
1188
                           protocol = protocol)
745
1189
    if server_settings["interface"]:
746
 
        service.interface = if_nametoindex(server_settings["interface"])
 
1190
        service.interface = (if_nametoindex
 
1191
                             (server_settings["interface"]))
747
1192
    
748
1193
    global main_loop
749
1194
    global bus
752
1197
    DBusGMainLoop(set_as_default=True )
753
1198
    main_loop = gobject.MainLoop()
754
1199
    bus = dbus.SystemBus()
755
 
    server = dbus.Interface(
756
 
            bus.get_object( avahi.DBUS_NAME, avahi.DBUS_PATH_SERVER ),
757
 
            avahi.DBUS_INTERFACE_SERVER )
 
1200
    server = dbus.Interface(bus.get_object(avahi.DBUS_NAME,
 
1201
                                           avahi.DBUS_PATH_SERVER),
 
1202
                            avahi.DBUS_INTERFACE_SERVER)
758
1203
    # End of Avahi example code
759
 
    
760
 
    clients = Set()
761
 
    def remove_from_clients(client):
762
 
        clients.remove(client)
763
 
        if not clients:
764
 
            logger.critical(u"No clients left, exiting")
765
 
            sys.exit()
 
1204
    if use_dbus:
 
1205
        bus_name = dbus.service.BusName(u"se.bsnet.fukt.Mandos", bus)
766
1206
    
767
1207
    clients.update(Set(Client(name = section,
768
 
                              stop_hook = remove_from_clients,
769
1208
                              config
770
 
                              = dict(client_config.items(section)))
 
1209
                              = dict(client_config.items(section)),
 
1210
                              use_dbus = use_dbus)
771
1211
                       for section in client_config.sections()))
772
1212
    if not clients:
773
 
        logger.critical(u"No clients defined")
774
 
        sys.exit(1)
 
1213
        logger.warning(u"No clients defined")
775
1214
    
776
 
    if not debug:
 
1215
    if debug:
 
1216
        # Redirect stdin so all checkers get /dev/null
 
1217
        null = os.open(os.path.devnull, os.O_NOCTTY | os.O_RDWR)
 
1218
        os.dup2(null, sys.stdin.fileno())
 
1219
        if null > 2:
 
1220
            os.close(null)
 
1221
    else:
 
1222
        # No console logging
777
1223
        logger.removeHandler(console)
 
1224
        # Close all input and output, do double fork, etc.
778
1225
        daemon()
779
1226
    
780
 
    pidfilename = "/var/run/mandos/mandos.pid"
781
 
    pid = os.getpid()
782
1227
    try:
783
 
        pidfile = open(pidfilename, "w")
784
 
        pidfile.write(str(pid) + "\n")
785
 
        pidfile.close()
 
1228
        with closing(pidfile):
 
1229
            pid = os.getpid()
 
1230
            pidfile.write(str(pid) + "\n")
786
1231
        del pidfile
787
 
    except IOError, err:
788
 
        logger.error(u"Could not write %s file with PID %d",
789
 
                     pidfilename, os.getpid())
 
1232
    except IOError:
 
1233
        logger.error(u"Could not write to file %r with PID %d",
 
1234
                     pidfilename, pid)
 
1235
    except NameError:
 
1236
        # "pidfile" was never created
 
1237
        pass
 
1238
    del pidfilename
790
1239
    
791
1240
    def cleanup():
792
1241
        "Cleanup function; run on exit"
799
1248
        
800
1249
        while clients:
801
1250
            client = clients.pop()
802
 
            client.stop_hook = None
803
 
            client.stop()
 
1251
            client.disable_hook = None
 
1252
            client.disable()
804
1253
    
805
1254
    atexit.register(cleanup)
806
1255
    
809
1258
    signal.signal(signal.SIGHUP, lambda signum, frame: sys.exit())
810
1259
    signal.signal(signal.SIGTERM, lambda signum, frame: sys.exit())
811
1260
    
 
1261
    if use_dbus:
 
1262
        class MandosDBusService(dbus.service.Object):
 
1263
            """A D-Bus proxy object"""
 
1264
            def __init__(self):
 
1265
                dbus.service.Object.__init__(self, bus, "/")
 
1266
            _interface = u"se.bsnet.fukt.Mandos"
 
1267
            
 
1268
            @dbus.service.signal(_interface, signature="oa{sv}")
 
1269
            def ClientAdded(self, objpath, properties):
 
1270
                "D-Bus signal"
 
1271
                pass
 
1272
            
 
1273
            @dbus.service.signal(_interface, signature="s")
 
1274
            def ClientNotFound(self, fingerprint):
 
1275
                "D-Bus signal"
 
1276
                pass
 
1277
            
 
1278
            @dbus.service.signal(_interface, signature="os")
 
1279
            def ClientRemoved(self, objpath, name):
 
1280
                "D-Bus signal"
 
1281
                pass
 
1282
            
 
1283
            @dbus.service.method(_interface, out_signature="ao")
 
1284
            def GetAllClients(self):
 
1285
                "D-Bus method"
 
1286
                return dbus.Array(c.dbus_object_path for c in clients)
 
1287
            
 
1288
            @dbus.service.method(_interface, out_signature="a{oa{sv}}")
 
1289
            def GetAllClientsWithProperties(self):
 
1290
                "D-Bus method"
 
1291
                return dbus.Dictionary(
 
1292
                    ((c.dbus_object_path, c.GetAllProperties())
 
1293
                     for c in clients),
 
1294
                    signature="oa{sv}")
 
1295
            
 
1296
            @dbus.service.method(_interface, in_signature="o")
 
1297
            def RemoveClient(self, object_path):
 
1298
                "D-Bus method"
 
1299
                for c in clients:
 
1300
                    if c.dbus_object_path == object_path:
 
1301
                        clients.remove(c)
 
1302
                        # Don't signal anything except ClientRemoved
 
1303
                        c.use_dbus = False
 
1304
                        c.disable()
 
1305
                        # Emit D-Bus signal
 
1306
                        self.ClientRemoved(object_path, c.name)
 
1307
                        return
 
1308
                raise KeyError
 
1309
            
 
1310
            del _interface
 
1311
        
 
1312
        mandos_dbus_service = MandosDBusService()
 
1313
    
812
1314
    for client in clients:
813
 
        client.start()
814
 
    
815
 
    tcp_server = IPv6_TCPServer((server_settings["address"],
816
 
                                 server_settings["port"]),
817
 
                                tcp_handler,
818
 
                                settings=server_settings,
819
 
                                clients=clients)
 
1315
        if use_dbus:
 
1316
            # Emit D-Bus signal
 
1317
            mandos_dbus_service.ClientAdded(client.dbus_object_path,
 
1318
                                            client.GetAllProperties())
 
1319
        client.enable()
 
1320
    
 
1321
    tcp_server.enable()
 
1322
    tcp_server.server_activate()
 
1323
    
820
1324
    # Find out what port we got
821
1325
    service.port = tcp_server.socket.getsockname()[1]
822
 
    logger.info(u"Now listening on address %r, port %d, flowinfo %d,"
823
 
                u" scope_id %d" % tcp_server.socket.getsockname())
 
1326
    if use_ipv6:
 
1327
        logger.info(u"Now listening on address %r, port %d,"
 
1328
                    " flowinfo %d, scope_id %d"
 
1329
                    % tcp_server.socket.getsockname())
 
1330
    else:                       # IPv4
 
1331
        logger.info(u"Now listening on address %r, port %d"
 
1332
                    % tcp_server.socket.getsockname())
824
1333
    
825
1334
    #service.interface = tcp_server.socket.getsockname()[3]
826
1335
    
836
1345
        
837
1346
        gobject.io_add_watch(tcp_server.fileno(), gobject.IO_IN,
838
1347
                             lambda *args, **kwargs:
839
 
                             tcp_server.handle_request\
840
 
                             (*args[2:], **kwargs) or True)
 
1348
                             (tcp_server.handle_request
 
1349
                              (*args[2:], **kwargs) or True))
841
1350
        
842
1351
        logger.debug(u"Starting main loop")
843
 
        main_loop_started = True
844
1352
        main_loop.run()
845
1353
    except AvahiError, error:
846
 
        logger.critical(u"AvahiError: %s" + unicode(error))
 
1354
        logger.critical(u"AvahiError: %s", error)
847
1355
        sys.exit(1)
848
1356
    except KeyboardInterrupt:
849
1357
        if debug:
850
 
            print
 
1358
            print >> sys.stderr
 
1359
        logger.debug("Server received KeyboardInterrupt")
 
1360
    logger.debug("Server exiting")
851
1361
 
852
1362
if __name__ == '__main__':
853
1363
    main()