/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

* clients.conf.xml: Renamed to "mandos-clients.conf.xml".

* Makefile (DOCBOOKTOMAN, DOCS): New
  (all): Include $(DOCS)
  (%.5: %.xml, %.8: %.xml, %.8mandos: %.xml): New.
  (clean): Also remove $(DOCS)

Show diffs side-by-side

added added

removed removed

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