/mandos/trunk

To get this branch, use:
bzr branch http://bzr.recompile.se/loggerhead/mandos/trunk

« back to all changes in this revision

Viewing changes to mandos

  • Committer: Teddy Hogeborn
  • Date: 2009-01-20 02:23:59 UTC
  • Revision ID: teddy@fukt.bsnet.se-20090120022359-0go4voqdz5pxc1pu
* mandos (Client.CheckerCompleted): Changed signature to "nxs"; return
                                    exitstatus and waitstatus, not
                                    just a bool and waitstatus.  All
                                    emitters changed.

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
 
# following functions: "add_service", "remove_service",
10
 
# "server_state_changed", "entry_group_state_changed", and some lines
11
 
# 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
 
# Everything else is Copyright © 2007-2008 Teddy Hogeborn and Björn
14
 
# Påhlsson.
 
13
# Everything else is
 
14
# Copyright © 2008,2009 Teddy Hogeborn
 
15
# Copyright © 2008,2009 Björn Påhlsson
15
16
16
17
# This program is free software: you can redistribute it and/or modify
17
18
# it under the terms of the GNU General Public License as published by
24
25
#     GNU General Public License for more details.
25
26
26
27
# You should have received a copy of the GNU General Public License
27
 
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
28
# along with this program.  If not, see
 
29
# <http://www.gnu.org/licenses/>.
28
30
29
 
# Contact the authors at <https://www.fukt.bsnet.se/~belorn/> and
30
 
# <https://www.fukt.bsnet.se/~teddy/>.
 
31
# Contact the authors at <mandos@fukt.bsnet.se>.
31
32
32
33
 
33
 
from __future__ import division
 
34
from __future__ import division, with_statement, absolute_import
34
35
 
35
36
import SocketServer
36
37
import socket
37
 
import select
38
 
from optparse import OptionParser
 
38
import optparse
39
39
import datetime
40
40
import errno
41
41
import gnutls.crypto
55
55
import stat
56
56
import logging
57
57
import logging.handlers
 
58
import pwd
 
59
from contextlib import closing
58
60
 
59
61
import dbus
 
62
import dbus.service
60
63
import gobject
61
64
import avahi
62
65
from dbus.mainloop.glib import DBusGMainLoop
63
66
import ctypes
64
 
 
65
 
# Brief description of the operation of this program:
66
 
67
 
# This server announces itself as a Zeroconf service.  Connecting
68
 
# clients use the TLS protocol, with the unusual quirk that this
69
 
# server program acts as a TLS "client" while the connecting clients
70
 
# acts as a TLS "server".  The clients (acting as a TLS "server") must
71
 
# supply an OpenPGP certificate, and the fingerprint of this
72
 
# certificate is used by this server to look up (in a list read from a
73
 
# file at start time) which binary blob to give the client.  No other
74
 
# authentication or authorization is done by this server.
75
 
 
 
67
import ctypes.util
 
68
 
 
69
version = "1.0.5"
76
70
 
77
71
logger = logging.Logger('mandos')
78
 
syslogger = logging.handlers.SysLogHandler\
79
 
            (facility = logging.handlers.SysLogHandler.LOG_DAEMON)
80
 
syslogger.setFormatter(logging.Formatter\
81
 
                        ('%(levelname)s: %(message)s'))
 
72
syslogger = (logging.handlers.SysLogHandler
 
73
             (facility = logging.handlers.SysLogHandler.LOG_DAEMON,
 
74
              address = "/dev/log"))
 
75
syslogger.setFormatter(logging.Formatter
 
76
                       ('Mandos: %(levelname)s: %(message)s'))
82
77
logger.addHandler(syslogger)
83
 
del syslogger
84
 
 
85
 
# This variable is used to optionally bind to a specified interface.
86
 
# It is a global variable to fit in with the other variables from the
87
 
# Avahi example code.
88
 
serviceInterface = avahi.IF_UNSPEC
 
78
 
 
79
console = logging.StreamHandler()
 
80
console.setFormatter(logging.Formatter('%(name)s: %(levelname)s:'
 
81
                                       ' %(message)s'))
 
82
logger.addHandler(console)
 
83
 
 
84
class AvahiError(Exception):
 
85
    def __init__(self, value, *args, **kwargs):
 
86
        self.value = value
 
87
        super(AvahiError, self).__init__(value, *args, **kwargs)
 
88
    def __unicode__(self):
 
89
        return unicode(repr(self.value))
 
90
 
 
91
class AvahiServiceError(AvahiError):
 
92
    pass
 
93
 
 
94
class AvahiGroupError(AvahiError):
 
95
    pass
 
96
 
 
97
 
 
98
class AvahiService(object):
 
99
    """An Avahi (Zeroconf) service.
 
100
    Attributes:
 
101
    interface: integer; avahi.IF_UNSPEC or an interface index.
 
102
               Used to optionally bind to the specified interface.
 
103
    name: string; Example: 'Mandos'
 
104
    type: string; Example: '_mandos._tcp'.
 
105
                  See <http://www.dns-sd.org/ServiceTypes.html>
 
106
    port: integer; what port to announce
 
107
    TXT: list of strings; TXT record for the service
 
108
    domain: string; Domain to publish on, default to .local if empty.
 
109
    host: string; Host to publish records for, default is localhost
 
110
    max_renames: integer; maximum number of renames
 
111
    rename_count: integer; counter so we only rename after collisions
 
112
                  a sensible number of times
 
113
    """
 
114
    def __init__(self, interface = avahi.IF_UNSPEC, name = None,
 
115
                 servicetype = None, port = None, TXT = None,
 
116
                 domain = "", host = "", max_renames = 32768):
 
117
        self.interface = interface
 
118
        self.name = name
 
119
        self.type = servicetype
 
120
        self.port = port
 
121
        self.TXT = TXT if TXT is not None else []
 
122
        self.domain = domain
 
123
        self.host = host
 
124
        self.rename_count = 0
 
125
        self.max_renames = max_renames
 
126
    def rename(self):
 
127
        """Derived from the Avahi example code"""
 
128
        if self.rename_count >= self.max_renames:
 
129
            logger.critical(u"No suitable Zeroconf service name found"
 
130
                            u" after %i retries, exiting.",
 
131
                            self.rename_count)
 
132
            raise AvahiServiceError(u"Too many renames")
 
133
        self.name = server.GetAlternativeServiceName(self.name)
 
134
        logger.info(u"Changing Zeroconf service name to %r ...",
 
135
                    str(self.name))
 
136
        syslogger.setFormatter(logging.Formatter
 
137
                               ('Mandos (%s): %%(levelname)s:'
 
138
                                ' %%(message)s' % self.name))
 
139
        self.remove()
 
140
        self.add()
 
141
        self.rename_count += 1
 
142
    def remove(self):
 
143
        """Derived from the Avahi example code"""
 
144
        if group is not None:
 
145
            group.Reset()
 
146
    def add(self):
 
147
        """Derived from the Avahi example code"""
 
148
        global group
 
149
        if group is None:
 
150
            group = dbus.Interface(bus.get_object
 
151
                                   (avahi.DBUS_NAME,
 
152
                                    server.EntryGroupNew()),
 
153
                                   avahi.DBUS_INTERFACE_ENTRY_GROUP)
 
154
            group.connect_to_signal('StateChanged',
 
155
                                    entry_group_state_changed)
 
156
        logger.debug(u"Adding Zeroconf service '%s' of type '%s' ...",
 
157
                     service.name, service.type)
 
158
        group.AddService(
 
159
                self.interface,         # interface
 
160
                avahi.PROTO_INET6,      # protocol
 
161
                dbus.UInt32(0),         # flags
 
162
                self.name, self.type,
 
163
                self.domain, self.host,
 
164
                dbus.UInt16(self.port),
 
165
                avahi.string_array_to_txt_array(self.TXT))
 
166
        group.Commit()
 
167
 
89
168
# From the Avahi example code:
90
 
serviceName = "Mandos"
91
 
serviceType = "_mandos._tcp" # http://www.dns-sd.org/ServiceTypes.html
92
 
servicePort = None                      # Not known at startup
93
 
serviceTXT = []                         # TXT record for the service
94
 
domain = ""                  # Domain to publish on, default to .local
95
 
host = ""          # Host to publish records for, default to localhost
96
 
group = None #our entry group
97
 
rename_count = 12       # Counter so we only rename after collisions a
98
 
                        # sensible number of times
 
169
group = None                            # our entry group
99
170
# End of Avahi example code
100
171
 
101
172
 
102
 
class Client(object):
 
173
def _datetime_to_dbus(dt, variant_level=0):
 
174
    """Convert a UTC datetime.datetime() to a D-Bus type."""
 
175
    return dbus.String(dt.isoformat(), variant_level=variant_level)
 
176
 
 
177
 
 
178
class Client(dbus.service.Object):
103
179
    """A representation of a client host served by this server.
104
180
    Attributes:
105
 
    name:      string; from the config file, used in log messages
 
181
    name:       string; from the config file, used in log messages and
 
182
                        D-Bus identifiers
106
183
    fingerprint: string (40 or 32 hexadecimal digits); used to
107
184
                 uniquely identify the client
108
 
    secret:    bytestring; sent verbatim (over TLS) to client
109
 
    fqdn:      string (FQDN); available for use by the checker command
110
 
    created:   datetime.datetime()
111
 
    last_seen: datetime.datetime() or None if not yet seen
112
 
    timeout:   datetime.timedelta(); How long from last_seen until
113
 
                                     this client is invalid
114
 
    interval:  datetime.timedelta(); How often to start a new checker
115
 
    stop_hook: If set, called by stop() as stop_hook(self)
116
 
    checker:   subprocess.Popen(); a running checker process used
117
 
                                   to see if the client lives.
118
 
                                   Is None if no process is running.
 
185
    secret:     bytestring; sent verbatim (over TLS) to client
 
186
    host:       string; available for use by the checker command
 
187
    created:    datetime.datetime(); (UTC) object creation
 
188
    last_enabled: datetime.datetime(); (UTC)
 
189
    enabled:    bool()
 
190
    last_checked_ok: datetime.datetime(); (UTC) or None
 
191
    timeout:    datetime.timedelta(); How long from last_checked_ok
 
192
                                      until this client is invalid
 
193
    interval:   datetime.timedelta(); How often to start a new checker
 
194
    disable_hook:  If set, called by disable() as disable_hook(self)
 
195
    checker:    subprocess.Popen(); a running checker process used
 
196
                                    to see if the client lives.
 
197
                                    'None' if no process is running.
119
198
    checker_initiator_tag: a gobject event source tag, or None
120
 
    stop_initiator_tag:    - '' -
 
199
    disable_initiator_tag:    - '' -
121
200
    checker_callback_tag:  - '' -
122
201
    checker_command: string; External command which is run to check if
123
 
                     client lives.  %()s expansions are done at
 
202
                     client lives.  %() expansions are done at
124
203
                     runtime with vars(self) as dict, so that for
125
204
                     instance %(name)s can be used in the command.
126
 
    Private attibutes:
127
 
    _timeout: Real variable for 'timeout'
128
 
    _interval: Real variable for 'interval'
129
 
    _timeout_milliseconds: Used by gobject.timeout_add()
130
 
    _interval_milliseconds: - '' -
 
205
    use_dbus: bool(); Whether to provide D-Bus interface and signals
 
206
    dbus_object_path: dbus.ObjectPath ; only set if self.use_dbus
131
207
    """
132
 
    def _set_timeout(self, timeout):
133
 
        "Setter function for 'timeout' attribute"
134
 
        self._timeout = timeout
135
 
        self._timeout_milliseconds = ((self.timeout.days
136
 
                                       * 24 * 60 * 60 * 1000)
137
 
                                      + (self.timeout.seconds * 1000)
138
 
                                      + (self.timeout.microseconds
139
 
                                         // 1000))
140
 
    timeout = property(lambda self: self._timeout,
141
 
                       _set_timeout)
142
 
    del _set_timeout
143
 
    def _set_interval(self, interval):
144
 
        "Setter function for 'interval' attribute"
145
 
        self._interval = interval
146
 
        self._interval_milliseconds = ((self.interval.days
147
 
                                        * 24 * 60 * 60 * 1000)
148
 
                                       + (self.interval.seconds
149
 
                                          * 1000)
150
 
                                       + (self.interval.microseconds
151
 
                                          // 1000))
152
 
    interval = property(lambda self: self._interval,
153
 
                        _set_interval)
154
 
    del _set_interval
155
 
    def __init__(self, name=None, options=None, stop_hook=None,
156
 
                 fingerprint=None, secret=None, secfile=None,
157
 
                 fqdn=None, timeout=None, interval=-1, checker=None):
158
 
        """Note: the 'checker' argument sets the 'checker_command'
159
 
        attribute and not the 'checker' attribute.."""
 
208
    def timeout_milliseconds(self):
 
209
        "Return the 'timeout' attribute in milliseconds"
 
210
        return ((self.timeout.days * 24 * 60 * 60 * 1000)
 
211
                + (self.timeout.seconds * 1000)
 
212
                + (self.timeout.microseconds // 1000))
 
213
    
 
214
    def interval_milliseconds(self):
 
215
        "Return the 'interval' attribute in milliseconds"
 
216
        return ((self.interval.days * 24 * 60 * 60 * 1000)
 
217
                + (self.interval.seconds * 1000)
 
218
                + (self.interval.microseconds // 1000))
 
219
    
 
220
    def __init__(self, name = None, disable_hook=None, config=None,
 
221
                 use_dbus=True):
 
222
        """Note: the 'checker' key in 'config' sets the
 
223
        'checker_command' attribute and *not* the 'checker'
 
224
        attribute."""
160
225
        self.name = name
161
 
        # Uppercase and remove spaces from fingerprint
162
 
        # for later comparison purposes with return value of
163
 
        # the fingerprint() function
164
 
        self.fingerprint = fingerprint.upper().replace(u" ", u"")
165
 
        if secret:
166
 
            self.secret = secret.decode(u"base64")
167
 
        elif secfile:
168
 
            sf = open(secfile)
169
 
            self.secret = sf.read()
170
 
            sf.close()
171
 
        else:
172
 
            raise RuntimeError(u"No secret or secfile for client %s"
173
 
                               % self.name)
174
 
        self.fqdn = fqdn                # string
175
 
        self.created = datetime.datetime.now()
176
 
        self.last_seen = None
177
 
        if timeout is None:
178
 
            self.timeout = options.timeout
179
 
        else:
180
 
            self.timeout = string_to_delta(timeout)
181
 
        if interval == -1:
182
 
            self.interval = options.interval
183
 
        else:
184
 
            self.interval = string_to_delta(interval)
185
 
        self.stop_hook = stop_hook
 
226
        if config is None:
 
227
            config = {}
 
228
        logger.debug(u"Creating client %r", self.name)
 
229
        self.use_dbus = False   # During __init__
 
230
        # Uppercase and remove spaces from fingerprint for later
 
231
        # comparison purposes with return value from the fingerprint()
 
232
        # function
 
233
        self.fingerprint = (config["fingerprint"].upper()
 
234
                            .replace(u" ", u""))
 
235
        logger.debug(u"  Fingerprint: %s", self.fingerprint)
 
236
        if "secret" in config:
 
237
            self.secret = config["secret"].decode(u"base64")
 
238
        elif "secfile" in config:
 
239
            with closing(open(os.path.expanduser
 
240
                              (os.path.expandvars
 
241
                               (config["secfile"])))) as secfile:
 
242
                self.secret = secfile.read()
 
243
        else:
 
244
            raise TypeError(u"No secret or secfile for client %s"
 
245
                            % self.name)
 
246
        self.host = config.get("host", "")
 
247
        self.created = datetime.datetime.utcnow()
 
248
        self.enabled = False
 
249
        self.last_enabled = None
 
250
        self.last_checked_ok = None
 
251
        self.timeout = string_to_delta(config["timeout"])
 
252
        self.interval = string_to_delta(config["interval"])
 
253
        self.disable_hook = disable_hook
186
254
        self.checker = None
187
255
        self.checker_initiator_tag = None
188
 
        self.stop_initiator_tag = None
 
256
        self.disable_initiator_tag = None
189
257
        self.checker_callback_tag = None
190
 
        self.check_command = checker
191
 
    def start(self):
 
258
        self.checker_command = config["checker"]
 
259
        self.last_connect = None
 
260
        # Only now, when this client is initialized, can it show up on
 
261
        # the D-Bus
 
262
        self.use_dbus = use_dbus
 
263
        if self.use_dbus:
 
264
            self.dbus_object_path = (dbus.ObjectPath
 
265
                                     ("/clients/"
 
266
                                      + self.name.replace(".", "_")))
 
267
            dbus.service.Object.__init__(self, bus,
 
268
                                         self.dbus_object_path)
 
269
    
 
270
    def enable(self):
192
271
        """Start this client's checker and timeout hooks"""
 
272
        self.last_enabled = datetime.datetime.utcnow()
193
273
        # Schedule a new checker to be started an 'interval' from now,
194
274
        # and every interval from then on.
195
 
        self.checker_initiator_tag = gobject.timeout_add\
196
 
                                     (self._interval_milliseconds,
197
 
                                      self.start_checker)
 
275
        self.checker_initiator_tag = (gobject.timeout_add
 
276
                                      (self.interval_milliseconds(),
 
277
                                       self.start_checker))
198
278
        # Also start a new checker *right now*.
199
279
        self.start_checker()
200
 
        # Schedule a stop() when 'timeout' has passed
201
 
        self.stop_initiator_tag = gobject.timeout_add\
202
 
                                  (self._timeout_milliseconds,
203
 
                                   self.stop)
204
 
    def stop(self):
205
 
        """Stop this client.
206
 
        The possibility that this client might be restarted is left
207
 
        open, but not currently used."""
208
 
        # If this client doesn't have a secret, it is already stopped.
209
 
        if self.secret:
210
 
            logger.debug(u"Stopping client %s", self.name)
211
 
            self.secret = None
212
 
        else:
 
280
        # Schedule a disable() when 'timeout' has passed
 
281
        self.disable_initiator_tag = (gobject.timeout_add
 
282
                                   (self.timeout_milliseconds(),
 
283
                                    self.disable))
 
284
        self.enabled = True
 
285
        if self.use_dbus:
 
286
            # Emit D-Bus signals
 
287
            self.PropertyChanged(dbus.String(u"enabled"),
 
288
                                 dbus.Boolean(True, variant_level=1))
 
289
            self.PropertyChanged(dbus.String(u"last_enabled"),
 
290
                                 (_datetime_to_dbus(self.last_enabled,
 
291
                                                    variant_level=1)))
 
292
    
 
293
    def disable(self):
 
294
        """Disable this client."""
 
295
        if not getattr(self, "enabled", False):
213
296
            return False
214
 
        if hasattr(self, "stop_initiator_tag") \
215
 
               and self.stop_initiator_tag:
216
 
            gobject.source_remove(self.stop_initiator_tag)
217
 
            self.stop_initiator_tag = None
218
 
        if hasattr(self, "checker_initiator_tag") \
219
 
               and self.checker_initiator_tag:
 
297
        logger.info(u"Disabling client %s", self.name)
 
298
        if getattr(self, "disable_initiator_tag", False):
 
299
            gobject.source_remove(self.disable_initiator_tag)
 
300
            self.disable_initiator_tag = None
 
301
        if getattr(self, "checker_initiator_tag", False):
220
302
            gobject.source_remove(self.checker_initiator_tag)
221
303
            self.checker_initiator_tag = None
222
304
        self.stop_checker()
223
 
        if self.stop_hook:
224
 
            self.stop_hook(self)
 
305
        if self.disable_hook:
 
306
            self.disable_hook(self)
 
307
        self.enabled = False
 
308
        if self.use_dbus:
 
309
            # Emit D-Bus signal
 
310
            self.PropertyChanged(dbus.String(u"enabled"),
 
311
                                 dbus.Boolean(False, variant_level=1))
225
312
        # Do not run this again if called by a gobject.timeout_add
226
313
        return False
 
314
    
227
315
    def __del__(self):
228
 
        self.stop_hook = None
229
 
        self.stop()
230
 
    def checker_callback(self, pid, condition):
 
316
        self.disable_hook = None
 
317
        self.disable()
 
318
    
 
319
    def checker_callback(self, pid, condition, command):
231
320
        """The checker has completed, so take appropriate actions."""
232
 
        now = datetime.datetime.now()
233
321
        self.checker_callback_tag = None
234
322
        self.checker = None
235
 
        if os.WIFEXITED(condition) \
236
 
               and (os.WEXITSTATUS(condition) == 0):
237
 
            logger.debug(u"Checker for %(name)s succeeded",
238
 
                         vars(self))
239
 
            self.last_seen = now
240
 
            gobject.source_remove(self.stop_initiator_tag)
241
 
            self.stop_initiator_tag = gobject.timeout_add\
242
 
                                      (self._timeout_milliseconds,
243
 
                                       self.stop)
244
 
        elif not os.WIFEXITED(condition):
 
323
        if self.use_dbus:
 
324
            # Emit D-Bus signal
 
325
            self.PropertyChanged(dbus.String(u"checker_running"),
 
326
                                 dbus.Boolean(False, variant_level=1))
 
327
        if os.WIFEXITED(condition):
 
328
            exitstatus = os.WEXITSTATUS(condition)
 
329
            if exitstatus == 0:
 
330
                logger.info(u"Checker for %(name)s succeeded",
 
331
                            vars(self))
 
332
                self.bump_timeout()
 
333
            else:
 
334
                logger.info(u"Checker for %(name)s failed",
 
335
                            vars(self))
 
336
            if self.use_dbus:
 
337
                # Emit D-Bus signal
 
338
                self.CheckerCompleted(dbus.Int16(exitstatus),
 
339
                                      dbus.Int64(condition),
 
340
                                      dbus.String(command))
 
341
        else:
245
342
            logger.warning(u"Checker for %(name)s crashed?",
246
343
                           vars(self))
247
 
        else:
248
 
            logger.debug(u"Checker for %(name)s failed",
249
 
                         vars(self))
 
344
            if self.use_dbus:
 
345
                # Emit D-Bus signal
 
346
                self.CheckerCompleted(dbus.Int16(-1),
 
347
                                      dbus.Int64(condition),
 
348
                                      dbus.String(command))
 
349
    
 
350
    def bump_timeout(self):
 
351
        """Bump up the timeout for this client.
 
352
        This should only be called when the client has been seen,
 
353
        alive and well.
 
354
        """
 
355
        self.last_checked_ok = datetime.datetime.utcnow()
 
356
        gobject.source_remove(self.disable_initiator_tag)
 
357
        self.disable_initiator_tag = (gobject.timeout_add
 
358
                                      (self.timeout_milliseconds(),
 
359
                                       self.disable))
 
360
        if self.use_dbus:
 
361
            # Emit D-Bus signal
 
362
            self.PropertyChanged(
 
363
                dbus.String(u"last_checked_ok"),
 
364
                (_datetime_to_dbus(self.last_checked_ok,
 
365
                                   variant_level=1)))
 
366
    
250
367
    def start_checker(self):
251
368
        """Start a new checker subprocess if one is not running.
252
369
        If a checker already exists, leave it running and do
261
378
        # is as it should be.
262
379
        if self.checker is None:
263
380
            try:
264
 
                command = self.check_command % self.fqdn
 
381
                # In case checker_command has exactly one % operator
 
382
                command = self.checker_command % self.host
265
383
            except TypeError:
 
384
                # Escape attributes for the shell
266
385
                escaped_attrs = dict((key, re.escape(str(val)))
267
386
                                     for key, val in
268
387
                                     vars(self).iteritems())
269
388
                try:
270
 
                    command = self.check_command % escaped_attrs
 
389
                    command = self.checker_command % escaped_attrs
271
390
                except TypeError, error:
272
 
                    logger.critical(u'Could not format string "%s":'
273
 
                                    u' %s', self.check_command, error)
 
391
                    logger.error(u'Could not format string "%s":'
 
392
                                 u' %s', self.checker_command, error)
274
393
                    return True # Try again later
275
394
            try:
276
 
                logger.debug(u"Starting checker %r for %s",
277
 
                             command, self.name)
278
 
                self.checker = subprocess.\
279
 
                               Popen(command,
280
 
                                     close_fds=True, shell=True,
281
 
                                     cwd="/")
282
 
                self.checker_callback_tag = gobject.child_watch_add\
283
 
                                            (self.checker.pid,
284
 
                                             self.checker_callback)
285
 
            except subprocess.OSError, error:
 
395
                logger.info(u"Starting checker %r for %s",
 
396
                            command, self.name)
 
397
                # We don't need to redirect stdout and stderr, since
 
398
                # in normal mode, that is already done by daemon(),
 
399
                # and in debug mode we don't want to.  (Stdin is
 
400
                # always replaced by /dev/null.)
 
401
                self.checker = subprocess.Popen(command,
 
402
                                                close_fds=True,
 
403
                                                shell=True, cwd="/")
 
404
                if self.use_dbus:
 
405
                    # Emit D-Bus signal
 
406
                    self.CheckerStarted(command)
 
407
                    self.PropertyChanged(
 
408
                        dbus.String("checker_running"),
 
409
                        dbus.Boolean(True, variant_level=1))
 
410
                self.checker_callback_tag = (gobject.child_watch_add
 
411
                                             (self.checker.pid,
 
412
                                              self.checker_callback,
 
413
                                              data=command))
 
414
            except OSError, error:
286
415
                logger.error(u"Failed to start subprocess: %s",
287
416
                             error)
288
417
        # Re-run this periodically if run by gobject.timeout_add
289
418
        return True
 
419
    
290
420
    def stop_checker(self):
291
421
        """Force the checker process, if any, to stop."""
292
422
        if self.checker_callback_tag:
293
423
            gobject.source_remove(self.checker_callback_tag)
294
424
            self.checker_callback_tag = None
295
 
        if not hasattr(self, "checker") or self.checker is None:
 
425
        if getattr(self, "checker", None) is None:
296
426
            return
297
 
        logger.debug("Stopping checker for %(name)s", vars(self))
 
427
        logger.debug(u"Stopping checker for %(name)s", vars(self))
298
428
        try:
299
429
            os.kill(self.checker.pid, signal.SIGTERM)
300
430
            #os.sleep(0.5)
301
431
            #if self.checker.poll() is None:
302
432
            #    os.kill(self.checker.pid, signal.SIGKILL)
303
433
        except OSError, error:
304
 
            if error.errno != errno.ESRCH:
 
434
            if error.errno != errno.ESRCH: # No such process
305
435
                raise
306
436
        self.checker = None
307
 
    def still_valid(self, now=None):
 
437
        if self.use_dbus:
 
438
            self.PropertyChanged(dbus.String(u"checker_running"),
 
439
                                 dbus.Boolean(False, variant_level=1))
 
440
    
 
441
    def still_valid(self):
308
442
        """Has the timeout not yet passed for this client?"""
309
 
        if now is None:
310
 
            now = datetime.datetime.now()
311
 
        if self.last_seen is None:
 
443
        if not getattr(self, "enabled", False):
 
444
            return False
 
445
        now = datetime.datetime.utcnow()
 
446
        if self.last_checked_ok is None:
312
447
            return now < (self.created + self.timeout)
313
448
        else:
314
 
            return now < (self.last_seen + self.timeout)
 
449
            return now < (self.last_checked_ok + self.timeout)
 
450
    
 
451
    ## D-Bus methods & signals
 
452
    _interface = u"se.bsnet.fukt.Mandos.Client"
 
453
    
 
454
    # BumpTimeout - method
 
455
    BumpTimeout = dbus.service.method(_interface)(bump_timeout)
 
456
    BumpTimeout.__name__ = "BumpTimeout"
 
457
    
 
458
    # CheckerCompleted - signal
 
459
    @dbus.service.signal(_interface, signature="nxs")
 
460
    def CheckerCompleted(self, exitcode, waitstatus, command):
 
461
        "D-Bus signal"
 
462
        pass
 
463
    
 
464
    # CheckerStarted - signal
 
465
    @dbus.service.signal(_interface, signature="s")
 
466
    def CheckerStarted(self, command):
 
467
        "D-Bus signal"
 
468
        pass
 
469
    
 
470
    # GetAllProperties - method
 
471
    @dbus.service.method(_interface, out_signature="a{sv}")
 
472
    def GetAllProperties(self):
 
473
        "D-Bus method"
 
474
        return dbus.Dictionary({
 
475
                dbus.String("name"):
 
476
                    dbus.String(self.name, variant_level=1),
 
477
                dbus.String("fingerprint"):
 
478
                    dbus.String(self.fingerprint, variant_level=1),
 
479
                dbus.String("host"):
 
480
                    dbus.String(self.host, variant_level=1),
 
481
                dbus.String("created"):
 
482
                    _datetime_to_dbus(self.created, variant_level=1),
 
483
                dbus.String("last_enabled"):
 
484
                    (_datetime_to_dbus(self.last_enabled,
 
485
                                       variant_level=1)
 
486
                     if self.last_enabled is not None
 
487
                     else dbus.Boolean(False, variant_level=1)),
 
488
                dbus.String("enabled"):
 
489
                    dbus.Boolean(self.enabled, variant_level=1),
 
490
                dbus.String("last_checked_ok"):
 
491
                    (_datetime_to_dbus(self.last_checked_ok,
 
492
                                       variant_level=1)
 
493
                     if self.last_checked_ok is not None
 
494
                     else dbus.Boolean (False, variant_level=1)),
 
495
                dbus.String("timeout"):
 
496
                    dbus.UInt64(self.timeout_milliseconds(),
 
497
                                variant_level=1),
 
498
                dbus.String("interval"):
 
499
                    dbus.UInt64(self.interval_milliseconds(),
 
500
                                variant_level=1),
 
501
                dbus.String("checker"):
 
502
                    dbus.String(self.checker_command,
 
503
                                variant_level=1),
 
504
                dbus.String("checker_running"):
 
505
                    dbus.Boolean(self.checker is not None,
 
506
                                 variant_level=1),
 
507
                dbus.String("object_path"):
 
508
                    dbus.ObjectPath(self.dbus_object_path,
 
509
                                    variant_level=1)
 
510
                }, signature="sv")
 
511
    
 
512
    # IsStillValid - method
 
513
    IsStillValid = (dbus.service.method(_interface, out_signature="b")
 
514
                    (still_valid))
 
515
    IsStillValid.__name__ = "IsStillValid"
 
516
    
 
517
    # PropertyChanged - signal
 
518
    @dbus.service.signal(_interface, signature="sv")
 
519
    def PropertyChanged(self, property, value):
 
520
        "D-Bus signal"
 
521
        pass
 
522
    
 
523
    # SetChecker - method
 
524
    @dbus.service.method(_interface, in_signature="s")
 
525
    def SetChecker(self, checker):
 
526
        "D-Bus setter method"
 
527
        self.checker_command = checker
 
528
        # Emit D-Bus signal
 
529
        self.PropertyChanged(dbus.String(u"checker"),
 
530
                             dbus.String(self.checker_command,
 
531
                                         variant_level=1))
 
532
    
 
533
    # SetHost - method
 
534
    @dbus.service.method(_interface, in_signature="s")
 
535
    def SetHost(self, host):
 
536
        "D-Bus setter method"
 
537
        self.host = host
 
538
        # Emit D-Bus signal
 
539
        self.PropertyChanged(dbus.String(u"host"),
 
540
                             dbus.String(self.host, variant_level=1))
 
541
    
 
542
    # SetInterval - method
 
543
    @dbus.service.method(_interface, in_signature="t")
 
544
    def SetInterval(self, milliseconds):
 
545
        self.interval = datetime.timedelta(0, 0, 0, milliseconds)
 
546
        # Emit D-Bus signal
 
547
        self.PropertyChanged(dbus.String(u"interval"),
 
548
                             (dbus.UInt64(self.interval_milliseconds(),
 
549
                                          variant_level=1)))
 
550
    
 
551
    # SetSecret - method
 
552
    @dbus.service.method(_interface, in_signature="ay",
 
553
                         byte_arrays=True)
 
554
    def SetSecret(self, secret):
 
555
        "D-Bus setter method"
 
556
        self.secret = str(secret)
 
557
    
 
558
    # SetTimeout - method
 
559
    @dbus.service.method(_interface, in_signature="t")
 
560
    def SetTimeout(self, milliseconds):
 
561
        self.timeout = datetime.timedelta(0, 0, 0, milliseconds)
 
562
        # Emit D-Bus signal
 
563
        self.PropertyChanged(dbus.String(u"timeout"),
 
564
                             (dbus.UInt64(self.timeout_milliseconds(),
 
565
                                          variant_level=1)))
 
566
    
 
567
    # Enable - method
 
568
    Enable = dbus.service.method(_interface)(enable)
 
569
    Enable.__name__ = "Enable"
 
570
    
 
571
    # StartChecker - method
 
572
    @dbus.service.method(_interface)
 
573
    def StartChecker(self):
 
574
        "D-Bus method"
 
575
        self.start_checker()
 
576
    
 
577
    # Disable - method
 
578
    @dbus.service.method(_interface)
 
579
    def Disable(self):
 
580
        "D-Bus method"
 
581
        self.disable()
 
582
    
 
583
    # StopChecker - method
 
584
    StopChecker = dbus.service.method(_interface)(stop_checker)
 
585
    StopChecker.__name__ = "StopChecker"
 
586
    
 
587
    del _interface
315
588
 
316
589
 
317
590
def peer_certificate(session):
318
591
    "Return the peer's OpenPGP certificate as a bytestring"
319
592
    # If not an OpenPGP certificate...
320
 
    if gnutls.library.functions.gnutls_certificate_type_get\
321
 
            (session._c_object) \
322
 
           != gnutls.library.constants.GNUTLS_CRT_OPENPGP:
 
593
    if (gnutls.library.functions
 
594
        .gnutls_certificate_type_get(session._c_object)
 
595
        != gnutls.library.constants.GNUTLS_CRT_OPENPGP):
323
596
        # ...do the normal thing
324
597
        return session.peer_certificate
325
598
    list_size = ctypes.c_uint()
326
 
    cert_list = gnutls.library.functions.gnutls_certificate_get_peers\
327
 
        (session._c_object, ctypes.byref(list_size))
 
599
    cert_list = (gnutls.library.functions
 
600
                 .gnutls_certificate_get_peers
 
601
                 (session._c_object, ctypes.byref(list_size)))
328
602
    if list_size.value == 0:
329
603
        return None
330
604
    cert = cert_list[0]
333
607
 
334
608
def fingerprint(openpgp):
335
609
    "Convert an OpenPGP bytestring to a hexdigit fingerprint string"
 
610
    # New GnuTLS "datum" with the OpenPGP public key
 
611
    datum = (gnutls.library.types
 
612
             .gnutls_datum_t(ctypes.cast(ctypes.c_char_p(openpgp),
 
613
                                         ctypes.POINTER
 
614
                                         (ctypes.c_ubyte)),
 
615
                             ctypes.c_uint(len(openpgp))))
336
616
    # New empty GnuTLS certificate
337
617
    crt = gnutls.library.types.gnutls_openpgp_crt_t()
338
 
    gnutls.library.functions.gnutls_openpgp_crt_init\
339
 
        (ctypes.byref(crt))
340
 
    # New GnuTLS "datum" with the OpenPGP public key
341
 
    datum = gnutls.library.types.gnutls_datum_t\
342
 
        (ctypes.cast(ctypes.c_char_p(openpgp),
343
 
                     ctypes.POINTER(ctypes.c_ubyte)),
344
 
         ctypes.c_uint(len(openpgp)))
 
618
    (gnutls.library.functions
 
619
     .gnutls_openpgp_crt_init(ctypes.byref(crt)))
345
620
    # Import the OpenPGP public key into the certificate
346
 
    ret = gnutls.library.functions.gnutls_openpgp_crt_import\
347
 
        (crt,
348
 
         ctypes.byref(datum),
349
 
         gnutls.library.constants.GNUTLS_OPENPGP_FMT_RAW)
 
621
    (gnutls.library.functions
 
622
     .gnutls_openpgp_crt_import(crt, ctypes.byref(datum),
 
623
                                gnutls.library.constants
 
624
                                .GNUTLS_OPENPGP_FMT_RAW))
 
625
    # Verify the self signature in the key
 
626
    crtverify = ctypes.c_uint()
 
627
    (gnutls.library.functions
 
628
     .gnutls_openpgp_crt_verify_self(crt, 0, ctypes.byref(crtverify)))
 
629
    if crtverify.value != 0:
 
630
        gnutls.library.functions.gnutls_openpgp_crt_deinit(crt)
 
631
        raise gnutls.errors.CertificateSecurityError("Verify failed")
350
632
    # New buffer for the fingerprint
351
 
    buffer = ctypes.create_string_buffer(20)
352
 
    buffer_length = ctypes.c_size_t()
 
633
    buf = ctypes.create_string_buffer(20)
 
634
    buf_len = ctypes.c_size_t()
353
635
    # Get the fingerprint from the certificate into the buffer
354
 
    gnutls.library.functions.gnutls_openpgp_crt_get_fingerprint\
355
 
        (crt, ctypes.byref(buffer), ctypes.byref(buffer_length))
 
636
    (gnutls.library.functions
 
637
     .gnutls_openpgp_crt_get_fingerprint(crt, ctypes.byref(buf),
 
638
                                         ctypes.byref(buf_len)))
356
639
    # Deinit the certificate
357
640
    gnutls.library.functions.gnutls_openpgp_crt_deinit(crt)
358
641
    # Convert the buffer to a Python bytestring
359
 
    fpr = ctypes.string_at(buffer, buffer_length.value)
 
642
    fpr = ctypes.string_at(buf, buf_len.value)
360
643
    # Convert the bytestring to hexadecimal notation
361
644
    hex_fpr = u''.join(u"%02X" % ord(char) for char in fpr)
362
645
    return hex_fpr
363
646
 
364
647
 
365
 
class tcp_handler(SocketServer.BaseRequestHandler, object):
 
648
class TCP_handler(SocketServer.BaseRequestHandler, object):
366
649
    """A TCP request handler class.
367
650
    Instantiated by IPv6_TCPServer for each request to handle it.
368
651
    Note: This will run in its own forked process."""
369
652
    
370
653
    def handle(self):
371
 
        logger.debug(u"TCP connection from: %s",
372
 
                     unicode(self.client_address))
373
 
        session = gnutls.connection.ClientSession(self.request,
374
 
                                                  gnutls.connection.\
375
 
                                                  X509Credentials())
 
654
        logger.info(u"TCP connection from: %s",
 
655
                    unicode(self.client_address))
 
656
        session = (gnutls.connection
 
657
                   .ClientSession(self.request,
 
658
                                  gnutls.connection
 
659
                                  .X509Credentials()))
 
660
        
 
661
        line = self.request.makefile().readline()
 
662
        logger.debug(u"Protocol version: %r", line)
 
663
        try:
 
664
            if int(line.strip().split()[0]) > 1:
 
665
                raise RuntimeError
 
666
        except (ValueError, IndexError, RuntimeError), error:
 
667
            logger.error(u"Unknown protocol version: %s", error)
 
668
            return
 
669
        
 
670
        # Note: gnutls.connection.X509Credentials is really a generic
 
671
        # GnuTLS certificate credentials object so long as no X.509
 
672
        # keys are added to it.  Therefore, we can use it here despite
 
673
        # using OpenPGP certificates.
376
674
        
377
675
        #priority = ':'.join(("NONE", "+VERS-TLS1.1", "+AES-256-CBC",
378
676
        #                "+SHA1", "+COMP-NULL", "+CTYPE-OPENPGP",
379
677
        #                "+DHE-DSS"))
380
 
        priority = "SECURE256"
381
 
        
382
 
        gnutls.library.functions.gnutls_priority_set_direct\
383
 
            (session._c_object, priority, None);
 
678
        # Use a fallback default, since this MUST be set.
 
679
        priority = self.server.settings.get("priority", "NORMAL")
 
680
        (gnutls.library.functions
 
681
         .gnutls_priority_set_direct(session._c_object,
 
682
                                     priority, None))
384
683
        
385
684
        try:
386
685
            session.handshake()
387
686
        except gnutls.errors.GNUTLSError, error:
388
 
            logger.debug(u"Handshake failed: %s", error)
 
687
            logger.warning(u"Handshake failed: %s", error)
389
688
            # Do not run session.bye() here: the session is not
390
689
            # established.  Just abandon the request.
391
690
            return
392
691
        try:
393
692
            fpr = fingerprint(peer_certificate(session))
394
693
        except (TypeError, gnutls.errors.GNUTLSError), error:
395
 
            logger.debug(u"Bad certificate: %s", error)
 
694
            logger.warning(u"Bad certificate: %s", error)
396
695
            session.bye()
397
696
            return
398
697
        logger.debug(u"Fingerprint: %s", fpr)
399
 
        client = None
400
698
        for c in self.server.clients:
401
699
            if c.fingerprint == fpr:
402
700
                client = c
403
701
                break
 
702
        else:
 
703
            logger.warning(u"Client not found for fingerprint: %s",
 
704
                           fpr)
 
705
            session.bye()
 
706
            return
404
707
        # Have to check if client.still_valid(), since it is possible
405
708
        # that the client timed out while establishing the GnuTLS
406
709
        # session.
407
 
        if (not client) or (not client.still_valid()):
408
 
            if client:
409
 
                logger.debug(u"Client %(name)s is invalid",
410
 
                             vars(client))
411
 
            else:
412
 
                logger.debug(u"Client not found for fingerprint: %s",
413
 
                             fpr)
 
710
        if not client.still_valid():
 
711
            logger.warning(u"Client %(name)s is invalid",
 
712
                           vars(client))
414
713
            session.bye()
415
714
            return
 
715
        ## This won't work here, since we're in a fork.
 
716
        # client.bump_timeout()
416
717
        sent_size = 0
417
718
        while sent_size < len(client.secret):
418
719
            sent = session.send(client.secret[sent_size:])
423
724
        session.bye()
424
725
 
425
726
 
426
 
class IPv6_TCPServer(SocketServer.ForkingTCPServer, object):
 
727
class IPv6_TCPServer(SocketServer.ForkingMixIn,
 
728
                     SocketServer.TCPServer, object):
427
729
    """IPv6 TCP server.  Accepts 'None' as address and/or port.
428
730
    Attributes:
429
 
        options:        Command line options
 
731
        settings:       Server settings
430
732
        clients:        Set() of Client objects
 
733
        enabled:        Boolean; whether this server is activated yet
431
734
    """
432
735
    address_family = socket.AF_INET6
433
736
    def __init__(self, *args, **kwargs):
434
 
        if "options" in kwargs:
435
 
            self.options = kwargs["options"]
436
 
            del kwargs["options"]
 
737
        if "settings" in kwargs:
 
738
            self.settings = kwargs["settings"]
 
739
            del kwargs["settings"]
437
740
        if "clients" in kwargs:
438
741
            self.clients = kwargs["clients"]
439
742
            del kwargs["clients"]
440
 
        return super(type(self), self).__init__(*args, **kwargs)
 
743
        self.enabled = False
 
744
        super(IPv6_TCPServer, self).__init__(*args, **kwargs)
441
745
    def server_bind(self):
442
746
        """This overrides the normal server_bind() function
443
747
        to bind to an interface if one was specified, and also NOT to
444
748
        bind to an address or port if they were not specified."""
445
 
        if self.options.interface:
446
 
            if not hasattr(socket, "SO_BINDTODEVICE"):
447
 
                # From /usr/include/asm-i486/socket.h
448
 
                socket.SO_BINDTODEVICE = 25
 
749
        if self.settings["interface"]:
 
750
            # 25 is from /usr/include/asm-i486/socket.h
 
751
            SO_BINDTODEVICE = getattr(socket, "SO_BINDTODEVICE", 25)
449
752
            try:
450
753
                self.socket.setsockopt(socket.SOL_SOCKET,
451
 
                                       socket.SO_BINDTODEVICE,
452
 
                                       self.options.interface)
 
754
                                       SO_BINDTODEVICE,
 
755
                                       self.settings["interface"])
453
756
            except socket.error, error:
454
757
                if error[0] == errno.EPERM:
455
 
                    logger.warning(u"No permission to"
456
 
                                   u" bind to interface %s",
457
 
                                   self.options.interface)
 
758
                    logger.error(u"No permission to"
 
759
                                 u" bind to interface %s",
 
760
                                 self.settings["interface"])
458
761
                else:
459
762
                    raise error
460
763
        # Only bind(2) the socket if we really need to.
463
766
                in6addr_any = "::"
464
767
                self.server_address = (in6addr_any,
465
768
                                       self.server_address[1])
466
 
            elif self.server_address[1] is None:
 
769
            elif not self.server_address[1]:
467
770
                self.server_address = (self.server_address[0],
468
771
                                       0)
469
 
            return super(type(self), self).server_bind()
 
772
#                 if self.settings["interface"]:
 
773
#                     self.server_address = (self.server_address[0],
 
774
#                                            0, # port
 
775
#                                            0, # flowinfo
 
776
#                                            if_nametoindex
 
777
#                                            (self.settings
 
778
#                                             ["interface"]))
 
779
            return super(IPv6_TCPServer, self).server_bind()
 
780
    def server_activate(self):
 
781
        if self.enabled:
 
782
            return super(IPv6_TCPServer, self).server_activate()
 
783
    def enable(self):
 
784
        self.enabled = True
470
785
 
471
786
 
472
787
def string_to_delta(interval):
482
797
    datetime.timedelta(1)
483
798
    >>> string_to_delta(u'1w')
484
799
    datetime.timedelta(7)
 
800
    >>> string_to_delta('5m 30s')
 
801
    datetime.timedelta(0, 330)
485
802
    """
486
 
    try:
487
 
        suffix=unicode(interval[-1])
488
 
        value=int(interval[:-1])
489
 
        if suffix == u"d":
490
 
            delta = datetime.timedelta(value)
491
 
        elif suffix == u"s":
492
 
            delta = datetime.timedelta(0, value)
493
 
        elif suffix == u"m":
494
 
            delta = datetime.timedelta(0, 0, 0, 0, value)
495
 
        elif suffix == u"h":
496
 
            delta = datetime.timedelta(0, 0, 0, 0, 0, value)
497
 
        elif suffix == u"w":
498
 
            delta = datetime.timedelta(0, 0, 0, 0, 0, 0, value)
499
 
        else:
 
803
    timevalue = datetime.timedelta(0)
 
804
    for s in interval.split():
 
805
        try:
 
806
            suffix = unicode(s[-1])
 
807
            value = int(s[:-1])
 
808
            if suffix == u"d":
 
809
                delta = datetime.timedelta(value)
 
810
            elif suffix == u"s":
 
811
                delta = datetime.timedelta(0, value)
 
812
            elif suffix == u"m":
 
813
                delta = datetime.timedelta(0, 0, 0, 0, value)
 
814
            elif suffix == u"h":
 
815
                delta = datetime.timedelta(0, 0, 0, 0, 0, value)
 
816
            elif suffix == u"w":
 
817
                delta = datetime.timedelta(0, 0, 0, 0, 0, 0, value)
 
818
            else:
 
819
                raise ValueError
 
820
        except (ValueError, IndexError):
500
821
            raise ValueError
501
 
    except (ValueError, IndexError):
502
 
        raise ValueError
503
 
    return delta
504
 
 
505
 
 
506
 
def add_service():
507
 
    """Derived from the Avahi example code"""
508
 
    global group, serviceName, serviceType, servicePort, serviceTXT, \
509
 
           domain, host
510
 
    if group is None:
511
 
        group = dbus.Interface(
512
 
                bus.get_object( avahi.DBUS_NAME,
513
 
                                server.EntryGroupNew()),
514
 
                avahi.DBUS_INTERFACE_ENTRY_GROUP)
515
 
        group.connect_to_signal('StateChanged',
516
 
                                entry_group_state_changed)
517
 
    logger.debug(u"Adding service '%s' of type '%s' ...",
518
 
                 serviceName, serviceType)
519
 
    
520
 
    group.AddService(
521
 
            serviceInterface,           # interface
522
 
            avahi.PROTO_INET6,          # protocol
523
 
            dbus.UInt32(0),             # flags
524
 
            serviceName, serviceType,
525
 
            domain, host,
526
 
            dbus.UInt16(servicePort),
527
 
            avahi.string_array_to_txt_array(serviceTXT))
528
 
    group.Commit()
529
 
 
530
 
 
531
 
def remove_service():
532
 
    """From the Avahi example code"""
533
 
    global group
534
 
    
535
 
    if not group is None:
536
 
        group.Reset()
 
822
        timevalue += delta
 
823
    return timevalue
537
824
 
538
825
 
539
826
def server_state_changed(state):
540
827
    """Derived from the Avahi example code"""
541
828
    if state == avahi.SERVER_COLLISION:
542
 
        logger.warning(u"Server name collision")
543
 
        remove_service()
 
829
        logger.error(u"Zeroconf server name collision")
 
830
        service.remove()
544
831
    elif state == avahi.SERVER_RUNNING:
545
 
        add_service()
 
832
        service.add()
546
833
 
547
834
 
548
835
def entry_group_state_changed(state, error):
549
836
    """Derived from the Avahi example code"""
550
 
    global serviceName, server, rename_count
551
 
    
552
 
    logger.debug(u"state change: %i", state)
 
837
    logger.debug(u"Avahi state change: %i", state)
553
838
    
554
839
    if state == avahi.ENTRY_GROUP_ESTABLISHED:
555
 
        logger.debug(u"Service established.")
 
840
        logger.debug(u"Zeroconf service established.")
556
841
    elif state == avahi.ENTRY_GROUP_COLLISION:
557
 
        
558
 
        rename_count = rename_count - 1
559
 
        if rename_count > 0:
560
 
            name = server.GetAlternativeServiceName(name)
561
 
            logger.warning(u"Service name collision, "
562
 
                           u"changing name to '%s' ...", name)
563
 
            remove_service()
564
 
            add_service()
565
 
            
566
 
        else:
567
 
            logger.error(u"No suitable service name found after %i"
568
 
                         u" retries, exiting.", n_rename)
569
 
            killme(1)
 
842
        logger.warning(u"Zeroconf service name collision.")
 
843
        service.rename()
570
844
    elif state == avahi.ENTRY_GROUP_FAILURE:
571
 
        logger.error(u"Error in group state changed %s",
572
 
                     unicode(error))
573
 
        killme(1)
574
 
 
 
845
        logger.critical(u"Avahi: Error in group state changed %s",
 
846
                        unicode(error))
 
847
        raise AvahiGroupError(u"State changed: %s" % unicode(error))
575
848
 
576
849
def if_nametoindex(interface):
577
 
    """Call the C function if_nametoindex()"""
 
850
    """Call the C function if_nametoindex(), or equivalent"""
 
851
    global if_nametoindex
578
852
    try:
579
 
        libc = ctypes.cdll.LoadLibrary("libc.so.6")
580
 
        return libc.if_nametoindex(interface)
 
853
        if_nametoindex = (ctypes.cdll.LoadLibrary
 
854
                          (ctypes.util.find_library("c"))
 
855
                          .if_nametoindex)
581
856
    except (OSError, AttributeError):
582
857
        if "struct" not in sys.modules:
583
858
            import struct
584
859
        if "fcntl" not in sys.modules:
585
860
            import fcntl
586
 
        SIOCGIFINDEX = 0x8933      # From /usr/include/linux/sockios.h
587
 
        s = socket.socket()
588
 
        ifreq = fcntl.ioctl(s, SIOCGIFINDEX,
589
 
                            struct.pack("16s16x", interface))
590
 
        s.close()
591
 
        interface_index = struct.unpack("I", ifreq[16:20])[0]
592
 
        return interface_index
593
 
 
594
 
 
595
 
def daemon(nochdir, noclose):
 
861
        def if_nametoindex(interface):
 
862
            "Get an interface index the hard way, i.e. using fcntl()"
 
863
            SIOCGIFINDEX = 0x8933  # From /usr/include/linux/sockios.h
 
864
            with closing(socket.socket()) as s:
 
865
                ifreq = fcntl.ioctl(s, SIOCGIFINDEX,
 
866
                                    struct.pack("16s16x", interface))
 
867
            interface_index = struct.unpack("I", ifreq[16:20])[0]
 
868
            return interface_index
 
869
    return if_nametoindex(interface)
 
870
 
 
871
 
 
872
def daemon(nochdir = False, noclose = False):
596
873
    """See daemon(3).  Standard BSD Unix function.
597
874
    This should really exist as os.daemon, but it doesn't (yet)."""
598
875
    if os.fork():
600
877
    os.setsid()
601
878
    if not nochdir:
602
879
        os.chdir("/")
 
880
    if os.fork():
 
881
        sys.exit()
603
882
    if not noclose:
604
883
        # Close all standard open file descriptors
605
 
        null = os.open("/dev/null", os.O_NOCTTY | os.O_RDWR)
 
884
        null = os.open(os.path.devnull, os.O_NOCTTY | os.O_RDWR)
606
885
        if not stat.S_ISCHR(os.fstat(null).st_mode):
607
886
            raise OSError(errno.ENODEV,
608
887
                          "/dev/null not a character device")
613
892
            os.close(null)
614
893
 
615
894
 
616
 
def killme(status = 0):
617
 
    logger.debug("Stopping server with exit status %d", status)
618
 
    exitstatus = status
619
 
    if main_loop_started:
620
 
        main_loop.quit()
621
 
    else:
622
 
        sys.exit(status)
623
 
 
624
 
 
625
895
def main():
626
 
    global exitstatus
627
 
    exitstatus = 0
628
 
    global main_loop_started
629
 
    main_loop_started = False
630
 
    
631
 
    parser = OptionParser()
 
896
    parser = optparse.OptionParser(version = "%%prog %s" % version)
632
897
    parser.add_option("-i", "--interface", type="string",
633
 
                      default=None, metavar="IF",
634
 
                      help="Bind to interface IF")
635
 
    parser.add_option("-a", "--address", type="string", default=None,
 
898
                      metavar="IF", help="Bind to interface IF")
 
899
    parser.add_option("-a", "--address", type="string",
636
900
                      help="Address to listen for requests on")
637
 
    parser.add_option("-p", "--port", type="int", default=None,
 
901
    parser.add_option("-p", "--port", type="int",
638
902
                      help="Port number to receive requests on")
639
 
    parser.add_option("--timeout", type="string", # Parsed later
640
 
                      default="1h",
641
 
                      help="Amount of downtime allowed for clients")
642
 
    parser.add_option("--interval", type="string", # Parsed later
643
 
                      default="5m",
644
 
                      help="How often to check that a client is up")
645
 
    parser.add_option("--check", action="store_true", default=False,
 
903
    parser.add_option("--check", action="store_true",
646
904
                      help="Run self-test")
647
 
    parser.add_option("--debug", action="store_true", default=False,
648
 
                      help="Debug mode")
649
 
    (options, args) = parser.parse_args()
 
905
    parser.add_option("--debug", action="store_true",
 
906
                      help="Debug mode; run in foreground and log to"
 
907
                      " terminal")
 
908
    parser.add_option("--priority", type="string", help="GnuTLS"
 
909
                      " priority string (see GnuTLS documentation)")
 
910
    parser.add_option("--servicename", type="string", metavar="NAME",
 
911
                      help="Zeroconf service name")
 
912
    parser.add_option("--configdir", type="string",
 
913
                      default="/etc/mandos", metavar="DIR",
 
914
                      help="Directory to search for configuration"
 
915
                      " files")
 
916
    parser.add_option("--no-dbus", action="store_false",
 
917
                      dest="use_dbus",
 
918
                      help="Do not provide D-Bus system bus"
 
919
                      " interface")
 
920
    options = parser.parse_args()[0]
650
921
    
651
922
    if options.check:
652
923
        import doctest
653
924
        doctest.testmod()
654
925
        sys.exit()
655
926
    
656
 
    # Parse the time arguments
657
 
    try:
658
 
        options.timeout = string_to_delta(options.timeout)
659
 
    except ValueError:
660
 
        parser.error("option --timeout: Unparseable time")
661
 
    try:
662
 
        options.interval = string_to_delta(options.interval)
663
 
    except ValueError:
664
 
        parser.error("option --interval: Unparseable time")
665
 
    
666
 
    # Parse config file
667
 
    defaults = { "checker": "fping -q -- %%(fqdn)s" }
668
 
    client_config = ConfigParser.SafeConfigParser(defaults)
669
 
    #client_config.readfp(open("global.conf"), "global.conf")
670
 
    client_config.read("mandos-clients.conf")
 
927
    # Default values for config file for server-global settings
 
928
    server_defaults = { "interface": "",
 
929
                        "address": "",
 
930
                        "port": "",
 
931
                        "debug": "False",
 
932
                        "priority":
 
933
                        "SECURE256:!CTYPE-X.509:+CTYPE-OPENPGP",
 
934
                        "servicename": "Mandos",
 
935
                        "use_dbus": "True",
 
936
                        }
 
937
    
 
938
    # Parse config file for server-global settings
 
939
    server_config = ConfigParser.SafeConfigParser(server_defaults)
 
940
    del server_defaults
 
941
    server_config.read(os.path.join(options.configdir, "mandos.conf"))
 
942
    # Convert the SafeConfigParser object to a dict
 
943
    server_settings = server_config.defaults()
 
944
    # Use getboolean on the boolean config options
 
945
    server_settings["debug"] = (server_config.getboolean
 
946
                                ("DEFAULT", "debug"))
 
947
    server_settings["use_dbus"] = (server_config.getboolean
 
948
                                   ("DEFAULT", "use_dbus"))
 
949
    del server_config
 
950
    
 
951
    # Override the settings from the config file with command line
 
952
    # options, if set.
 
953
    for option in ("interface", "address", "port", "debug",
 
954
                   "priority", "servicename", "configdir",
 
955
                   "use_dbus"):
 
956
        value = getattr(options, option)
 
957
        if value is not None:
 
958
            server_settings[option] = value
 
959
    del options
 
960
    # Now we have our good server settings in "server_settings"
 
961
    
 
962
    # For convenience
 
963
    debug = server_settings["debug"]
 
964
    use_dbus = server_settings["use_dbus"]
 
965
    
 
966
    if not debug:
 
967
        syslogger.setLevel(logging.WARNING)
 
968
        console.setLevel(logging.WARNING)
 
969
    
 
970
    if server_settings["servicename"] != "Mandos":
 
971
        syslogger.setFormatter(logging.Formatter
 
972
                               ('Mandos (%s): %%(levelname)s:'
 
973
                                ' %%(message)s'
 
974
                                % server_settings["servicename"]))
 
975
    
 
976
    # Parse config file with clients
 
977
    client_defaults = { "timeout": "1h",
 
978
                        "interval": "5m",
 
979
                        "checker": "fping -q -- %%(host)s",
 
980
                        "host": "",
 
981
                        }
 
982
    client_config = ConfigParser.SafeConfigParser(client_defaults)
 
983
    client_config.read(os.path.join(server_settings["configdir"],
 
984
                                    "clients.conf"))
 
985
    
 
986
    clients = Set()
 
987
    tcp_server = IPv6_TCPServer((server_settings["address"],
 
988
                                 server_settings["port"]),
 
989
                                TCP_handler,
 
990
                                settings=server_settings,
 
991
                                clients=clients)
 
992
    pidfilename = "/var/run/mandos.pid"
 
993
    try:
 
994
        pidfile = open(pidfilename, "w")
 
995
    except IOError, error:
 
996
        logger.error("Could not open file %r", pidfilename)
 
997
    
 
998
    try:
 
999
        uid = pwd.getpwnam("_mandos").pw_uid
 
1000
        gid = pwd.getpwnam("_mandos").pw_gid
 
1001
    except KeyError:
 
1002
        try:
 
1003
            uid = pwd.getpwnam("mandos").pw_uid
 
1004
            gid = pwd.getpwnam("mandos").pw_gid
 
1005
        except KeyError:
 
1006
            try:
 
1007
                uid = pwd.getpwnam("nobody").pw_uid
 
1008
                gid = pwd.getpwnam("nogroup").pw_gid
 
1009
            except KeyError:
 
1010
                uid = 65534
 
1011
                gid = 65534
 
1012
    try:
 
1013
        os.setuid(uid)
 
1014
        os.setgid(gid)
 
1015
    except OSError, error:
 
1016
        if error[0] != errno.EPERM:
 
1017
            raise error
 
1018
    
 
1019
    global service
 
1020
    service = AvahiService(name = server_settings["servicename"],
 
1021
                           servicetype = "_mandos._tcp", )
 
1022
    if server_settings["interface"]:
 
1023
        service.interface = (if_nametoindex
 
1024
                             (server_settings["interface"]))
671
1025
    
672
1026
    global main_loop
673
1027
    global bus
676
1030
    DBusGMainLoop(set_as_default=True )
677
1031
    main_loop = gobject.MainLoop()
678
1032
    bus = dbus.SystemBus()
679
 
    server = dbus.Interface(
680
 
            bus.get_object( avahi.DBUS_NAME, avahi.DBUS_PATH_SERVER ),
681
 
            avahi.DBUS_INTERFACE_SERVER )
 
1033
    server = dbus.Interface(bus.get_object(avahi.DBUS_NAME,
 
1034
                                           avahi.DBUS_PATH_SERVER),
 
1035
                            avahi.DBUS_INTERFACE_SERVER)
682
1036
    # End of Avahi example code
 
1037
    if use_dbus:
 
1038
        bus_name = dbus.service.BusName(u"se.bsnet.fukt.Mandos", bus)
683
1039
    
684
 
    debug = options.debug
 
1040
    clients.update(Set(Client(name = section,
 
1041
                              config
 
1042
                              = dict(client_config.items(section)),
 
1043
                              use_dbus = use_dbus)
 
1044
                       for section in client_config.sections()))
 
1045
    if not clients:
 
1046
        logger.warning(u"No clients defined")
685
1047
    
686
1048
    if debug:
687
 
        console = logging.StreamHandler()
688
 
        # console.setLevel(logging.DEBUG)
689
 
        console.setFormatter(logging.Formatter\
690
 
                             ('%(levelname)s: %(message)s'))
691
 
        logger.addHandler(console)
692
 
        del console
693
 
    
694
 
    clients = Set()
695
 
    def remove_from_clients(client):
696
 
        clients.remove(client)
697
 
        if not clients:
698
 
            logger.debug(u"No clients left, exiting")
699
 
            killme()
700
 
    
701
 
    clients.update(Set(Client(name=section, options=options,
702
 
                              stop_hook = remove_from_clients,
703
 
                              **(dict(client_config\
704
 
                                      .items(section))))
705
 
                       for section in client_config.sections()))
706
 
    
707
 
    if not debug:
708
 
        daemon(False, False)
 
1049
        # Redirect stdin so all checkers get /dev/null
 
1050
        null = os.open(os.path.devnull, os.O_NOCTTY | os.O_RDWR)
 
1051
        os.dup2(null, sys.stdin.fileno())
 
1052
        if null > 2:
 
1053
            os.close(null)
 
1054
    else:
 
1055
        # No console logging
 
1056
        logger.removeHandler(console)
 
1057
        # Close all input and output, do double fork, etc.
 
1058
        daemon()
 
1059
    
 
1060
    try:
 
1061
        pid = os.getpid()
 
1062
        pidfile.write(str(pid) + "\n")
 
1063
        pidfile.close()
 
1064
        del pidfile
 
1065
    except IOError:
 
1066
        logger.error(u"Could not write to file %r with PID %d",
 
1067
                     pidfilename, pid)
 
1068
    except NameError:
 
1069
        # "pidfile" was never created
 
1070
        pass
 
1071
    del pidfilename
709
1072
    
710
1073
    def cleanup():
711
1074
        "Cleanup function; run on exit"
718
1081
        
719
1082
        while clients:
720
1083
            client = clients.pop()
721
 
            client.stop_hook = None
722
 
            client.stop()
 
1084
            client.disable_hook = None
 
1085
            client.disable()
723
1086
    
724
1087
    atexit.register(cleanup)
725
1088
    
726
1089
    if not debug:
727
1090
        signal.signal(signal.SIGINT, signal.SIG_IGN)
728
 
    signal.signal(signal.SIGHUP, lambda signum, frame: killme())
729
 
    signal.signal(signal.SIGTERM, lambda signum, frame: killme())
 
1091
    signal.signal(signal.SIGHUP, lambda signum, frame: sys.exit())
 
1092
    signal.signal(signal.SIGTERM, lambda signum, frame: sys.exit())
 
1093
    
 
1094
    if use_dbus:
 
1095
        class MandosServer(dbus.service.Object):
 
1096
            """A D-Bus proxy object"""
 
1097
            def __init__(self):
 
1098
                dbus.service.Object.__init__(self, bus, "/")
 
1099
            _interface = u"se.bsnet.fukt.Mandos"
 
1100
            
 
1101
            @dbus.service.signal(_interface, signature="oa{sv}")
 
1102
            def ClientAdded(self, objpath, properties):
 
1103
                "D-Bus signal"
 
1104
                pass
 
1105
            
 
1106
            @dbus.service.signal(_interface, signature="os")
 
1107
            def ClientRemoved(self, objpath, name):
 
1108
                "D-Bus signal"
 
1109
                pass
 
1110
            
 
1111
            @dbus.service.method(_interface, out_signature="ao")
 
1112
            def GetAllClients(self):
 
1113
                return dbus.Array(c.dbus_object_path for c in clients)
 
1114
            
 
1115
            @dbus.service.method(_interface, out_signature="a{oa{sv}}")
 
1116
            def GetAllClientsWithProperties(self):
 
1117
                return dbus.Dictionary(
 
1118
                    ((c.dbus_object_path, c.GetAllProperties())
 
1119
                     for c in clients),
 
1120
                    signature="oa{sv}")
 
1121
            
 
1122
            @dbus.service.method(_interface, in_signature="o")
 
1123
            def RemoveClient(self, object_path):
 
1124
                for c in clients:
 
1125
                    if c.dbus_object_path == object_path:
 
1126
                        clients.remove(c)
 
1127
                        # Don't signal anything except ClientRemoved
 
1128
                        c.use_dbus = False
 
1129
                        c.disable()
 
1130
                        # Emit D-Bus signal
 
1131
                        self.ClientRemoved(object_path, c.name)
 
1132
                        return
 
1133
                raise KeyError
 
1134
            
 
1135
            del _interface
 
1136
        
 
1137
        mandos_server = MandosServer()
730
1138
    
731
1139
    for client in clients:
732
 
        client.start()
733
 
    
734
 
    tcp_server = IPv6_TCPServer((options.address, options.port),
735
 
                                tcp_handler,
736
 
                                options=options,
737
 
                                clients=clients)
738
 
    # Find out what random port we got
739
 
    global servicePort
740
 
    servicePort = tcp_server.socket.getsockname()[1]
741
 
    logger.debug(u"Now listening on port %d", servicePort)
742
 
    
743
 
    if options.interface is not None:
744
 
        global serviceInterface
745
 
        serviceInterface = if_nametoindex(options.interface)
746
 
    
747
 
    # From the Avahi example code
748
 
    server.connect_to_signal("StateChanged", server_state_changed)
749
 
    try:
750
 
        server_state_changed(server.GetState())
751
 
    except dbus.exceptions.DBusException, error:
752
 
        logger.critical(u"DBusException: %s", error)
753
 
        killme(1)
754
 
    # End of Avahi example code
755
 
    
756
 
    gobject.io_add_watch(tcp_server.fileno(), gobject.IO_IN,
757
 
                         lambda *args, **kwargs:
758
 
                         tcp_server.handle_request(*args[2:],
759
 
                                                   **kwargs) or True)
760
 
    try:
761
 
        logger.debug("Starting main loop")
762
 
        main_loop_started = True
 
1140
        if use_dbus:
 
1141
            # Emit D-Bus signal
 
1142
            mandos_server.ClientAdded(client.dbus_object_path,
 
1143
                                      client.GetAllProperties())
 
1144
        client.enable()
 
1145
    
 
1146
    tcp_server.enable()
 
1147
    tcp_server.server_activate()
 
1148
    
 
1149
    # Find out what port we got
 
1150
    service.port = tcp_server.socket.getsockname()[1]
 
1151
    logger.info(u"Now listening on address %r, port %d, flowinfo %d,"
 
1152
                u" scope_id %d" % tcp_server.socket.getsockname())
 
1153
    
 
1154
    #service.interface = tcp_server.socket.getsockname()[3]
 
1155
    
 
1156
    try:
 
1157
        # From the Avahi example code
 
1158
        server.connect_to_signal("StateChanged", server_state_changed)
 
1159
        try:
 
1160
            server_state_changed(server.GetState())
 
1161
        except dbus.exceptions.DBusException, error:
 
1162
            logger.critical(u"DBusException: %s", error)
 
1163
            sys.exit(1)
 
1164
        # End of Avahi example code
 
1165
        
 
1166
        gobject.io_add_watch(tcp_server.fileno(), gobject.IO_IN,
 
1167
                             lambda *args, **kwargs:
 
1168
                             (tcp_server.handle_request
 
1169
                              (*args[2:], **kwargs) or True))
 
1170
        
 
1171
        logger.debug(u"Starting main loop")
763
1172
        main_loop.run()
 
1173
    except AvahiError, error:
 
1174
        logger.critical(u"AvahiError: %s", error)
 
1175
        sys.exit(1)
764
1176
    except KeyboardInterrupt:
765
1177
        if debug:
766
1178
            print
767
 
    
768
 
    sys.exit(exitstatus)
769
1179
 
770
1180
if __name__ == '__main__':
771
1181
    main()