/mandos/release

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

« back to all changes in this revision

Viewing changes to mandos

  • Committer: Teddy Hogeborn
  • Date: 2008-09-30 07:23:39 UTC
  • Revision ID: teddy@fukt.bsnet.se-20080930072339-jn15gyrtfpdk2dhx
* .bzrignore: Added "man" directory (created by "make install-html").

* Makefile: Add "common.ent" dependency to all manual pages.
  (htmldir, version, SED): New variables.
  (CFLAGS): Add -D option to define VERSION to $(version).
  (MANPOST, HTMLPOST): Use $(SED).
  (PROGS): Use $(CPROGS)
  (CPROGS): New; C-only programs.
  (objects): Use $(CPROGS).
  (common.ent, mandos, mandos-keygen): New targets; update version
                                       number to $(version).
  (clean): Use $(CPROGS).
  (check): Depend on "all".
  (install-html): Install to $(htmldir).

* common.ent: New file with "version" entity.

* mandos-clients.conf.xml: Use "common.ent".
* mandos-keygen.xml: - '' -
* mandos.conf.xml: - '' -
* mandos.xml: - '' -
* plugin-runner.xml: - '' -
* plugins.d/mandos-client.xml: - '' -
* plugins.d/password-prompt.xml: - '' -

* plugin-runner.c (argp_program_version): Use VERSION.
* plugins.d/mandos-client.c (argp_program_version): - '' -
* plugins.d/password-prompt.c (argp_program_version): - '' -

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
1
#!/usr/bin/python
 
2
# -*- mode: python; coding: utf-8 -*-
 
3
 
4
# Mandos server - give out binary blobs to connecting clients.
 
5
 
6
# This program is partly derived from an example program for an Avahi
 
7
# service publisher, downloaded from
 
8
# <http://avahi.org/wiki/PythonPublishExample>.  This includes the
 
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
 
13
# Everything else is
 
14
# Copyright © 2008 Teddy Hogeborn & Björn Påhlsson
 
15
 
16
# This program is free software: you can redistribute it and/or modify
 
17
# it under the terms of the GNU General Public License as published by
 
18
# the Free Software Foundation, either version 3 of the License, or
 
19
# (at your option) any later version.
 
20
#
 
21
#     This program is distributed in the hope that it will be useful,
 
22
#     but WITHOUT ANY WARRANTY; without even the implied warranty of
 
23
#     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 
24
#     GNU General Public License for more details.
 
25
 
26
# You should have received a copy of the GNU General Public License
 
27
# along with this program.  If not, see
 
28
# <http://www.gnu.org/licenses/>.
 
29
 
30
# Contact the authors at <mandos@fukt.bsnet.se>.
 
31
 
32
 
 
33
from __future__ import division
2
34
 
3
35
import SocketServer
4
36
import socket
5
 
import select
6
37
from optparse import OptionParser
7
38
import datetime
8
39
import errno
9
40
import gnutls.crypto
10
41
import gnutls.connection
11
42
import gnutls.errors
 
43
import gnutls.library.functions
 
44
import gnutls.library.constants
 
45
import gnutls.library.types
12
46
import ConfigParser
13
47
import sys
14
48
import re
15
49
import os
16
50
import signal
 
51
from sets import Set
 
52
import subprocess
 
53
import atexit
 
54
import stat
 
55
import logging
 
56
import logging.handlers
 
57
import pwd
 
58
 
 
59
import dbus
 
60
import gobject
 
61
import avahi
 
62
from dbus.mainloop.glib import DBusGMainLoop
 
63
import ctypes
 
64
import ctypes.util
 
65
 
 
66
version = "1.0"
 
67
 
 
68
logger = logging.Logger('mandos')
 
69
syslogger = logging.handlers.SysLogHandler\
 
70
            (facility = logging.handlers.SysLogHandler.LOG_DAEMON,
 
71
             address = "/dev/log")
 
72
syslogger.setFormatter(logging.Formatter\
 
73
                        ('Mandos: %(levelname)s: %(message)s'))
 
74
logger.addHandler(syslogger)
 
75
 
 
76
console = logging.StreamHandler()
 
77
console.setFormatter(logging.Formatter('%(name)s: %(levelname)s:'
 
78
                                       ' %(message)s'))
 
79
logger.addHandler(console)
 
80
 
 
81
class AvahiError(Exception):
 
82
    def __init__(self, value):
 
83
        self.value = value
 
84
        super(AvahiError, self).__init__()
 
85
    def __str__(self):
 
86
        return repr(self.value)
 
87
 
 
88
class AvahiServiceError(AvahiError):
 
89
    pass
 
90
 
 
91
class AvahiGroupError(AvahiError):
 
92
    pass
 
93
 
 
94
 
 
95
class AvahiService(object):
 
96
    """An Avahi (Zeroconf) service.
 
97
    Attributes:
 
98
    interface: integer; avahi.IF_UNSPEC or an interface index.
 
99
               Used to optionally bind to the specified interface.
 
100
    name: string; Example: 'Mandos'
 
101
    type: string; Example: '_mandos._tcp'.
 
102
                  See <http://www.dns-sd.org/ServiceTypes.html>
 
103
    port: integer; what port to announce
 
104
    TXT: list of strings; TXT record for the service
 
105
    domain: string; Domain to publish on, default to .local if empty.
 
106
    host: string; Host to publish records for, default is localhost
 
107
    max_renames: integer; maximum number of renames
 
108
    rename_count: integer; counter so we only rename after collisions
 
109
                  a sensible number of times
 
110
    """
 
111
    def __init__(self, interface = avahi.IF_UNSPEC, name = None,
 
112
                 servicetype = None, port = None, TXT = None, domain = "",
 
113
                 host = "", max_renames = 32768):
 
114
        self.interface = interface
 
115
        self.name = name
 
116
        self.type = servicetype
 
117
        self.port = port
 
118
        if TXT is None:
 
119
            self.TXT = []
 
120
        else:
 
121
            self.TXT = TXT
 
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("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\
 
151
                    (bus.get_object(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
 
 
168
# From the Avahi example code:
 
169
group = None                            # our entry group
 
170
# End of Avahi example code
 
171
 
17
172
 
18
173
class Client(object):
19
 
    def __init__(self, name=None, options=None, dn=None,
20
 
                 password=None, passfile=None, fqdn=None,
21
 
                 timeout=None, interval=-1):
 
174
    """A representation of a client host served by this server.
 
175
    Attributes:
 
176
    name:      string; from the config file, used in log messages
 
177
    fingerprint: string (40 or 32 hexadecimal digits); used to
 
178
                 uniquely identify the client
 
179
    secret:    bytestring; sent verbatim (over TLS) to client
 
180
    host:      string; available for use by the checker command
 
181
    created:   datetime.datetime(); object creation, not client host
 
182
    last_checked_ok: datetime.datetime() or None if not yet checked OK
 
183
    timeout:   datetime.timedelta(); How long from last_checked_ok
 
184
                                     until this client is invalid
 
185
    interval:  datetime.timedelta(); How often to start a new checker
 
186
    stop_hook: If set, called by stop() as stop_hook(self)
 
187
    checker:   subprocess.Popen(); a running checker process used
 
188
                                   to see if the client lives.
 
189
                                   'None' if no process is running.
 
190
    checker_initiator_tag: a gobject event source tag, or None
 
191
    stop_initiator_tag:    - '' -
 
192
    checker_callback_tag:  - '' -
 
193
    checker_command: string; External command which is run to check if
 
194
                     client lives.  %() expansions are done at
 
195
                     runtime with vars(self) as dict, so that for
 
196
                     instance %(name)s can be used in the command.
 
197
    Private attibutes:
 
198
    _timeout: Real variable for 'timeout'
 
199
    _interval: Real variable for 'interval'
 
200
    _timeout_milliseconds: Used when calling gobject.timeout_add()
 
201
    _interval_milliseconds: - '' -
 
202
    """
 
203
    def _set_timeout(self, timeout):
 
204
        "Setter function for 'timeout' attribute"
 
205
        self._timeout = timeout
 
206
        self._timeout_milliseconds = ((self.timeout.days
 
207
                                       * 24 * 60 * 60 * 1000)
 
208
                                      + (self.timeout.seconds * 1000)
 
209
                                      + (self.timeout.microseconds
 
210
                                         // 1000))
 
211
    timeout = property(lambda self: self._timeout,
 
212
                       _set_timeout)
 
213
    del _set_timeout
 
214
    def _set_interval(self, interval):
 
215
        "Setter function for 'interval' attribute"
 
216
        self._interval = interval
 
217
        self._interval_milliseconds = ((self.interval.days
 
218
                                        * 24 * 60 * 60 * 1000)
 
219
                                       + (self.interval.seconds
 
220
                                          * 1000)
 
221
                                       + (self.interval.microseconds
 
222
                                          // 1000))
 
223
    interval = property(lambda self: self._interval,
 
224
                        _set_interval)
 
225
    del _set_interval
 
226
    def __init__(self, name = None, stop_hook=None, config=None):
 
227
        """Note: the 'checker' key in 'config' sets the
 
228
        'checker_command' attribute and *not* the 'checker'
 
229
        attribute."""
 
230
        if config is None:
 
231
            config = {}
22
232
        self.name = name
23
 
        self.dn = dn
24
 
        if password:
25
 
            self.password = password
26
 
        elif passfile:
27
 
            self.password = open(passfile).readall()
 
233
        logger.debug(u"Creating client %r", self.name)
 
234
        # Uppercase and remove spaces from fingerprint for later
 
235
        # comparison purposes with return value from the fingerprint()
 
236
        # function
 
237
        self.fingerprint = config["fingerprint"].upper()\
 
238
                           .replace(u" ", u"")
 
239
        logger.debug(u"  Fingerprint: %s", self.fingerprint)
 
240
        if "secret" in config:
 
241
            self.secret = config["secret"].decode(u"base64")
 
242
        elif "secfile" in config:
 
243
            secfile = open(config["secfile"])
 
244
            self.secret = secfile.read()
 
245
            secfile.close()
28
246
        else:
29
 
            print "No Password or Passfile in client config file"
30
 
            # raise RuntimeError XXX
31
 
            self.password = "gazonk"
32
 
        self.fqdn = fqdn                # string
 
247
            raise TypeError(u"No secret or secfile for client %s"
 
248
                            % self.name)
 
249
        self.host = config.get("host", "")
33
250
        self.created = datetime.datetime.now()
34
 
        self.last_seen = None           # datetime.datetime()
35
 
        if timeout is None:
36
 
            timeout = options.timeout
37
 
        self.timeout = timeout          # datetime.timedelta()
38
 
        if interval == -1:
39
 
            interval = options.interval
40
 
        self.interval = interval        # datetime.timedelta()
41
 
        self.next_check = datetime.datetime.now() # datetime.datetime()
42
 
        self.checker = None             # or a subprocess.Popen()
43
 
    def check_action(self, now=None):
44
 
        """The checker said something and might have completed.
45
 
        Check if is has, and take appropriate actions."""
46
 
        if self.checker.poll() is None:
47
 
            # False alarm, no result yet
48
 
            #self.checker.read()
49
 
            return
50
 
        if now is None:
51
 
            now = datetime.datetime.now()
52
 
        if self.checker.returncode == 0:
53
 
            self.last_seen = now
54
 
        while self.next_check <= now:
55
 
            self.next_check += self.interval
56
 
    handle_request = check_action
 
251
        self.last_checked_ok = None
 
252
        self.timeout = string_to_delta(config["timeout"])
 
253
        self.interval = string_to_delta(config["interval"])
 
254
        self.stop_hook = stop_hook
 
255
        self.checker = None
 
256
        self.checker_initiator_tag = None
 
257
        self.stop_initiator_tag = None
 
258
        self.checker_callback_tag = None
 
259
        self.check_command = config["checker"]
 
260
    def start(self):
 
261
        """Start this client's checker and timeout hooks"""
 
262
        # Schedule a new checker to be started an 'interval' from now,
 
263
        # and every interval from then on.
 
264
        self.checker_initiator_tag = gobject.timeout_add\
 
265
                                     (self._interval_milliseconds,
 
266
                                      self.start_checker)
 
267
        # Also start a new checker *right now*.
 
268
        self.start_checker()
 
269
        # Schedule a stop() when 'timeout' has passed
 
270
        self.stop_initiator_tag = gobject.timeout_add\
 
271
                                  (self._timeout_milliseconds,
 
272
                                   self.stop)
 
273
    def stop(self):
 
274
        """Stop this client.
 
275
        The possibility that a client might be restarted is left open,
 
276
        but not currently used."""
 
277
        # If this client doesn't have a secret, it is already stopped.
 
278
        if hasattr(self, "secret") and self.secret:
 
279
            logger.info(u"Stopping client %s", self.name)
 
280
            self.secret = None
 
281
        else:
 
282
            return False
 
283
        if getattr(self, "stop_initiator_tag", False):
 
284
            gobject.source_remove(self.stop_initiator_tag)
 
285
            self.stop_initiator_tag = None
 
286
        if getattr(self, "checker_initiator_tag", False):
 
287
            gobject.source_remove(self.checker_initiator_tag)
 
288
            self.checker_initiator_tag = None
 
289
        self.stop_checker()
 
290
        if self.stop_hook:
 
291
            self.stop_hook(self)
 
292
        # Do not run this again if called by a gobject.timeout_add
 
293
        return False
 
294
    def __del__(self):
 
295
        self.stop_hook = None
 
296
        self.stop()
 
297
    def checker_callback(self, pid, condition):
 
298
        """The checker has completed, so take appropriate actions."""
 
299
        now = datetime.datetime.now()
 
300
        self.checker_callback_tag = None
 
301
        self.checker = None
 
302
        if os.WIFEXITED(condition) \
 
303
               and (os.WEXITSTATUS(condition) == 0):
 
304
            logger.info(u"Checker for %(name)s succeeded",
 
305
                        vars(self))
 
306
            self.last_checked_ok = now
 
307
            gobject.source_remove(self.stop_initiator_tag)
 
308
            self.stop_initiator_tag = gobject.timeout_add\
 
309
                                      (self._timeout_milliseconds,
 
310
                                       self.stop)
 
311
        elif not os.WIFEXITED(condition):
 
312
            logger.warning(u"Checker for %(name)s crashed?",
 
313
                           vars(self))
 
314
        else:
 
315
            logger.info(u"Checker for %(name)s failed",
 
316
                        vars(self))
57
317
    def start_checker(self):
58
 
        self.stop_checker()
59
 
        try:
60
 
            self.checker = subprocess.Popen("sleep 1; fping -q -- %s"
61
 
                                            % re.escape(self.fqdn),
62
 
                                            stdout=subprocess.PIPE,
63
 
                                            close_fds=True,
64
 
                                            shell=True, cwd="/")
65
 
        except subprocess.OSError, e:
66
 
            print "Failed to start subprocess:", e
 
318
        """Start a new checker subprocess if one is not running.
 
319
        If a checker already exists, leave it running and do
 
320
        nothing."""
 
321
        # The reason for not killing a running checker is that if we
 
322
        # did that, then if a checker (for some reason) started
 
323
        # running slowly and taking more than 'interval' time, the
 
324
        # client would inevitably timeout, since no checker would get
 
325
        # a chance to run to completion.  If we instead leave running
 
326
        # checkers alone, the checker would have to take more time
 
327
        # than 'timeout' for the client to be declared invalid, which
 
328
        # is as it should be.
 
329
        if self.checker is None:
 
330
            try:
 
331
                # In case check_command has exactly one % operator
 
332
                command = self.check_command % self.host
 
333
            except TypeError:
 
334
                # Escape attributes for the shell
 
335
                escaped_attrs = dict((key, re.escape(str(val)))
 
336
                                     for key, val in
 
337
                                     vars(self).iteritems())
 
338
                try:
 
339
                    command = self.check_command % escaped_attrs
 
340
                except TypeError, error:
 
341
                    logger.error(u'Could not format string "%s":'
 
342
                                 u' %s', self.check_command, error)
 
343
                    return True # Try again later
 
344
            try:
 
345
                logger.info(u"Starting checker %r for %s",
 
346
                            command, self.name)
 
347
                # We don't need to redirect stdout and stderr, since
 
348
                # in normal mode, that is already done by daemon(),
 
349
                # and in debug mode we don't want to.  (Stdin is
 
350
                # always replaced by /dev/null.)
 
351
                self.checker = subprocess.Popen(command,
 
352
                                                close_fds=True,
 
353
                                                shell=True, cwd="/")
 
354
                self.checker_callback_tag = gobject.child_watch_add\
 
355
                                            (self.checker.pid,
 
356
                                             self.checker_callback)
 
357
            except OSError, error:
 
358
                logger.error(u"Failed to start subprocess: %s",
 
359
                             error)
 
360
        # Re-run this periodically if run by gobject.timeout_add
 
361
        return True
67
362
    def stop_checker(self):
68
 
        if self.checker is None:
 
363
        """Force the checker process, if any, to stop."""
 
364
        if self.checker_callback_tag:
 
365
            gobject.source_remove(self.checker_callback_tag)
 
366
            self.checker_callback_tag = None
 
367
        if getattr(self, "checker", None) is None:
69
368
            return
70
 
        os.kill(self.checker.pid, signal.SIGTERM)
71
 
        if self.checker.poll() is None:
72
 
            os.kill(self.checker.pid, signal.SIGKILL)
 
369
        logger.debug(u"Stopping checker for %(name)s", vars(self))
 
370
        try:
 
371
            os.kill(self.checker.pid, signal.SIGTERM)
 
372
            #os.sleep(0.5)
 
373
            #if self.checker.poll() is None:
 
374
            #    os.kill(self.checker.pid, signal.SIGKILL)
 
375
        except OSError, error:
 
376
            if error.errno != errno.ESRCH: # No such process
 
377
                raise
73
378
        self.checker = None
74
 
    __del__ = stop_checker
75
 
    def fileno(self):
76
 
        if self.checker is None:
77
 
            return None
78
 
        return self.checker.stdout.fileno()
79
 
    def next_stop(self):
80
 
        """The time when something must be done about this client"""
81
 
        return min(self.last_seen + self.timeout, self.next_check)
82
 
    def still_valid(self, now=None):
83
 
        """Has this client's timeout not passed?"""
84
 
        if now is None:
85
 
            now = datetime.datetime.now()
86
 
        return now < (self.last_seen + timeout)
87
 
    def it_is_time_to_check(self, now=None):
88
 
        if now is None:
89
 
            now = datetime.datetime.now()
90
 
        return self.next_check <= now
91
 
 
92
 
 
93
 
class server_metaclass(type):
94
 
    "Common behavior for the UDP and TCP server classes"
95
 
    def __new__(cls, name, bases, attrs):
96
 
        attrs["address_family"] = socket.AF_INET6
97
 
        attrs["allow_reuse_address"] = True
98
 
        def server_bind(self):
99
 
            if self.options.interface:
100
 
                if not hasattr(socket, "SO_BINDTODEVICE"):
101
 
                    # From /usr/include/asm-i486/socket.h
102
 
                    socket.SO_BINDTODEVICE = 25
103
 
                try:
104
 
                    self.socket.setsockopt(socket.SOL_SOCKET,
105
 
                                           socket.SO_BINDTODEVICE,
106
 
                                           self.options.interface)
107
 
                except socket.error, error:
108
 
                    if error[0] == errno.EPERM:
109
 
                        print "Warning: No permission to bind to interface", \
110
 
                              self.options.interface
111
 
                    else:
112
 
                        raise error
113
 
            return super(type(self), self).server_bind()
114
 
        attrs["server_bind"] = server_bind
115
 
        def init(self, *args, **kwargs):
116
 
            if "options" in kwargs:
117
 
                self.options = kwargs["options"]
118
 
                del kwargs["options"]
119
 
            if "clients" in kwargs:
120
 
                self.clients = kwargs["clients"]
121
 
                del kwargs["clients"]
122
 
            if "credentials" in kwargs:
123
 
                self.credentials = kwargs["credentials"]
124
 
                del kwargs["credentials"]
125
 
            return super(type(self), self).__init__(*args, **kwargs)
126
 
        attrs["__init__"] = init
127
 
        return type.__new__(cls, name, bases, attrs)
128
 
 
129
 
 
130
 
class udp_handler(SocketServer.DatagramRequestHandler, object):
131
 
    def handle(self):
132
 
        self.wfile.write("Polo")
133
 
        print "UDP request answered"
134
 
 
135
 
 
136
 
class IPv6_UDPServer(SocketServer.UDPServer, object):
137
 
    __metaclass__ = server_metaclass
138
 
    def verify_request(self, request, client_address):
139
 
        print "UDP request came"
140
 
        return request[0] == "Marco"
141
 
 
142
 
 
143
 
class tcp_handler(SocketServer.BaseRequestHandler, object):
144
 
    def handle(self):
145
 
        print "TCP request came"
146
 
        print "Request:", self.request
147
 
        print "Client Address:", self.client_address
148
 
        print "Server:", self.server
149
 
        session = gnutls.connection.ServerSession(self.request,
150
 
                                                  self.server.credentials)
151
 
        session.handshake()
152
 
        if session.peer_certificate:
153
 
            print "DN:", session.peer_certificate.subject
154
 
        try:
155
 
            session.verify_peer()
156
 
        except gnutls.errors.CertificateError, error:
157
 
            print "Verify failed", error
158
 
            session.bye()
159
 
            return
160
 
        try:
161
 
            session.send(dict((client.dn, client.password)
162
 
                              for client in self.server.clients)
163
 
                         [session.peer_certificate.subject])
164
 
        except KeyError:
165
 
            session.send("gazonk")
166
 
            # Log maybe? XXX
 
379
    def still_valid(self):
 
380
        """Has the timeout not yet passed for this client?"""
 
381
        now = datetime.datetime.now()
 
382
        if self.last_checked_ok is None:
 
383
            return now < (self.created + self.timeout)
 
384
        else:
 
385
            return now < (self.last_checked_ok + self.timeout)
 
386
 
 
387
 
 
388
def peer_certificate(session):
 
389
    "Return the peer's OpenPGP certificate as a bytestring"
 
390
    # If not an OpenPGP certificate...
 
391
    if gnutls.library.functions.gnutls_certificate_type_get\
 
392
            (session._c_object) \
 
393
           != gnutls.library.constants.GNUTLS_CRT_OPENPGP:
 
394
        # ...do the normal thing
 
395
        return session.peer_certificate
 
396
    list_size = ctypes.c_uint()
 
397
    cert_list = gnutls.library.functions.gnutls_certificate_get_peers\
 
398
        (session._c_object, ctypes.byref(list_size))
 
399
    if list_size.value == 0:
 
400
        return None
 
401
    cert = cert_list[0]
 
402
    return ctypes.string_at(cert.data, cert.size)
 
403
 
 
404
 
 
405
def fingerprint(openpgp):
 
406
    "Convert an OpenPGP bytestring to a hexdigit fingerprint string"
 
407
    # New GnuTLS "datum" with the OpenPGP public key
 
408
    datum = gnutls.library.types.gnutls_datum_t\
 
409
        (ctypes.cast(ctypes.c_char_p(openpgp),
 
410
                     ctypes.POINTER(ctypes.c_ubyte)),
 
411
         ctypes.c_uint(len(openpgp)))
 
412
    # New empty GnuTLS certificate
 
413
    crt = gnutls.library.types.gnutls_openpgp_crt_t()
 
414
    gnutls.library.functions.gnutls_openpgp_crt_init\
 
415
        (ctypes.byref(crt))
 
416
    # Import the OpenPGP public key into the certificate
 
417
    gnutls.library.functions.gnutls_openpgp_crt_import\
 
418
                    (crt, ctypes.byref(datum),
 
419
                     gnutls.library.constants.GNUTLS_OPENPGP_FMT_RAW)
 
420
    # Verify the self signature in the key
 
421
    crtverify = ctypes.c_uint()
 
422
    gnutls.library.functions.gnutls_openpgp_crt_verify_self\
 
423
        (crt, 0, ctypes.byref(crtverify))
 
424
    if crtverify.value != 0:
 
425
        gnutls.library.functions.gnutls_openpgp_crt_deinit(crt)
 
426
        raise gnutls.errors.CertificateSecurityError("Verify failed")
 
427
    # New buffer for the fingerprint
 
428
    buf = ctypes.create_string_buffer(20)
 
429
    buf_len = ctypes.c_size_t()
 
430
    # Get the fingerprint from the certificate into the buffer
 
431
    gnutls.library.functions.gnutls_openpgp_crt_get_fingerprint\
 
432
        (crt, ctypes.byref(buf), ctypes.byref(buf_len))
 
433
    # Deinit the certificate
 
434
    gnutls.library.functions.gnutls_openpgp_crt_deinit(crt)
 
435
    # Convert the buffer to a Python bytestring
 
436
    fpr = ctypes.string_at(buf, buf_len.value)
 
437
    # Convert the bytestring to hexadecimal notation
 
438
    hex_fpr = u''.join(u"%02X" % ord(char) for char in fpr)
 
439
    return hex_fpr
 
440
 
 
441
 
 
442
class TCP_handler(SocketServer.BaseRequestHandler, object):
 
443
    """A TCP request handler class.
 
444
    Instantiated by IPv6_TCPServer for each request to handle it.
 
445
    Note: This will run in its own forked process."""
 
446
    
 
447
    def handle(self):
 
448
        logger.info(u"TCP connection from: %s",
 
449
                     unicode(self.client_address))
 
450
        session = gnutls.connection.ClientSession\
 
451
                  (self.request, gnutls.connection.X509Credentials())
 
452
        
 
453
        line = self.request.makefile().readline()
 
454
        logger.debug(u"Protocol version: %r", line)
 
455
        try:
 
456
            if int(line.strip().split()[0]) > 1:
 
457
                raise RuntimeError
 
458
        except (ValueError, IndexError, RuntimeError), error:
 
459
            logger.error(u"Unknown protocol version: %s", error)
 
460
            return
 
461
        
 
462
        # Note: gnutls.connection.X509Credentials is really a generic
 
463
        # GnuTLS certificate credentials object so long as no X.509
 
464
        # keys are added to it.  Therefore, we can use it here despite
 
465
        # using OpenPGP certificates.
 
466
        
 
467
        #priority = ':'.join(("NONE", "+VERS-TLS1.1", "+AES-256-CBC",
 
468
        #                "+SHA1", "+COMP-NULL", "+CTYPE-OPENPGP",
 
469
        #                "+DHE-DSS"))
 
470
        priority = "NORMAL"             # Fallback default, since this
 
471
                                        # MUST be set.
 
472
        if self.server.settings["priority"]:
 
473
            priority = self.server.settings["priority"]
 
474
        gnutls.library.functions.gnutls_priority_set_direct\
 
475
            (session._c_object, priority, None)
 
476
        
 
477
        try:
 
478
            session.handshake()
 
479
        except gnutls.errors.GNUTLSError, error:
 
480
            logger.warning(u"Handshake failed: %s", error)
 
481
            # Do not run session.bye() here: the session is not
 
482
            # established.  Just abandon the request.
 
483
            return
 
484
        try:
 
485
            fpr = fingerprint(peer_certificate(session))
 
486
        except (TypeError, gnutls.errors.GNUTLSError), error:
 
487
            logger.warning(u"Bad certificate: %s", error)
 
488
            session.bye()
 
489
            return
 
490
        logger.debug(u"Fingerprint: %s", fpr)
 
491
        client = None
 
492
        for c in self.server.clients:
 
493
            if c.fingerprint == fpr:
 
494
                client = c
 
495
                break
 
496
        if not client:
 
497
            logger.warning(u"Client not found for fingerprint: %s",
 
498
                           fpr)
 
499
            session.bye()
 
500
            return
 
501
        # Have to check if client.still_valid(), since it is possible
 
502
        # that the client timed out while establishing the GnuTLS
 
503
        # session.
 
504
        if not client.still_valid():
 
505
            logger.warning(u"Client %(name)s is invalid",
 
506
                           vars(client))
 
507
            session.bye()
 
508
            return
 
509
        sent_size = 0
 
510
        while sent_size < len(client.secret):
 
511
            sent = session.send(client.secret[sent_size:])
 
512
            logger.debug(u"Sent: %d, remaining: %d",
 
513
                         sent, len(client.secret)
 
514
                         - (sent_size + sent))
 
515
            sent_size += sent
167
516
        session.bye()
168
517
 
169
518
 
170
519
class IPv6_TCPServer(SocketServer.ForkingTCPServer, object):
171
 
    __metaclass__ = server_metaclass
172
 
    request_queue_size = 1024
173
 
 
174
 
 
175
 
in6addr_any = "::"
 
520
    """IPv6 TCP server.  Accepts 'None' as address and/or port.
 
521
    Attributes:
 
522
        settings:       Server settings
 
523
        clients:        Set() of Client objects
 
524
        enabled:        Boolean; whether this server is activated yet
 
525
    """
 
526
    address_family = socket.AF_INET6
 
527
    def __init__(self, *args, **kwargs):
 
528
        if "settings" in kwargs:
 
529
            self.settings = kwargs["settings"]
 
530
            del kwargs["settings"]
 
531
        if "clients" in kwargs:
 
532
            self.clients = kwargs["clients"]
 
533
            del kwargs["clients"]
 
534
        self.enabled = False
 
535
        super(IPv6_TCPServer, self).__init__(*args, **kwargs)
 
536
    def server_bind(self):
 
537
        """This overrides the normal server_bind() function
 
538
        to bind to an interface if one was specified, and also NOT to
 
539
        bind to an address or port if they were not specified."""
 
540
        if self.settings["interface"]:
 
541
            # 25 is from /usr/include/asm-i486/socket.h
 
542
            SO_BINDTODEVICE = getattr(socket, "SO_BINDTODEVICE", 25)
 
543
            try:
 
544
                self.socket.setsockopt(socket.SOL_SOCKET,
 
545
                                       SO_BINDTODEVICE,
 
546
                                       self.settings["interface"])
 
547
            except socket.error, error:
 
548
                if error[0] == errno.EPERM:
 
549
                    logger.error(u"No permission to"
 
550
                                 u" bind to interface %s",
 
551
                                 self.settings["interface"])
 
552
                else:
 
553
                    raise error
 
554
        # Only bind(2) the socket if we really need to.
 
555
        if self.server_address[0] or self.server_address[1]:
 
556
            if not self.server_address[0]:
 
557
                in6addr_any = "::"
 
558
                self.server_address = (in6addr_any,
 
559
                                       self.server_address[1])
 
560
            elif not self.server_address[1]:
 
561
                self.server_address = (self.server_address[0],
 
562
                                       0)
 
563
#                 if self.settings["interface"]:
 
564
#                     self.server_address = (self.server_address[0],
 
565
#                                            0, # port
 
566
#                                            0, # flowinfo
 
567
#                                            if_nametoindex
 
568
#                                            (self.settings
 
569
#                                             ["interface"]))
 
570
            return super(IPv6_TCPServer, self).server_bind()
 
571
    def server_activate(self):
 
572
        if self.enabled:
 
573
            return super(IPv6_TCPServer, self).server_activate()
 
574
    def enable(self):
 
575
        self.enabled = True
 
576
 
176
577
 
177
578
def string_to_delta(interval):
178
579
    """Parse a string and return a datetime.timedelta
187
588
    datetime.timedelta(1)
188
589
    >>> string_to_delta(u'1w')
189
590
    datetime.timedelta(7)
 
591
    >>> string_to_delta('5m 30s')
 
592
    datetime.timedelta(0, 330)
190
593
    """
191
 
    try:
192
 
        suffix=unicode(interval[-1])
193
 
        value=int(interval[:-1])
194
 
        if suffix == u"d":
195
 
            delta = datetime.timedelta(value)
196
 
        elif suffix == u"s":
197
 
            delta = datetime.timedelta(0, value)
198
 
        elif suffix == u"m":
199
 
            delta = datetime.timedelta(0, 0, 0, 0, value)
200
 
        elif suffix == u"h":
201
 
            delta = datetime.timedelta(0, 0, 0, 0, 0, value)
202
 
        elif suffix == u"w":
203
 
            delta = datetime.timedelta(0, 0, 0, 0, 0, 0, value)
204
 
        else:
 
594
    timevalue = datetime.timedelta(0)
 
595
    for s in interval.split():
 
596
        try:
 
597
            suffix = unicode(s[-1])
 
598
            value = int(s[:-1])
 
599
            if suffix == u"d":
 
600
                delta = datetime.timedelta(value)
 
601
            elif suffix == u"s":
 
602
                delta = datetime.timedelta(0, value)
 
603
            elif suffix == u"m":
 
604
                delta = datetime.timedelta(0, 0, 0, 0, value)
 
605
            elif suffix == u"h":
 
606
                delta = datetime.timedelta(0, 0, 0, 0, 0, value)
 
607
            elif suffix == u"w":
 
608
                delta = datetime.timedelta(0, 0, 0, 0, 0, 0, value)
 
609
            else:
 
610
                raise ValueError
 
611
        except (ValueError, IndexError):
205
612
            raise ValueError
206
 
    except (ValueError, IndexError):
207
 
        raise ValueError
208
 
    return delta
 
613
        timevalue += delta
 
614
    return timevalue
 
615
 
 
616
 
 
617
def server_state_changed(state):
 
618
    """Derived from the Avahi example code"""
 
619
    if state == avahi.SERVER_COLLISION:
 
620
        logger.error(u"Zeroconf server name collision")
 
621
        service.remove()
 
622
    elif state == avahi.SERVER_RUNNING:
 
623
        service.add()
 
624
 
 
625
 
 
626
def entry_group_state_changed(state, error):
 
627
    """Derived from the Avahi example code"""
 
628
    logger.debug(u"Avahi state change: %i", state)
 
629
    
 
630
    if state == avahi.ENTRY_GROUP_ESTABLISHED:
 
631
        logger.debug(u"Zeroconf service established.")
 
632
    elif state == avahi.ENTRY_GROUP_COLLISION:
 
633
        logger.warning(u"Zeroconf service name collision.")
 
634
        service.rename()
 
635
    elif state == avahi.ENTRY_GROUP_FAILURE:
 
636
        logger.critical(u"Avahi: Error in group state changed %s",
 
637
                        unicode(error))
 
638
        raise AvahiGroupError("State changed: %s", str(error))
 
639
 
 
640
def if_nametoindex(interface):
 
641
    """Call the C function if_nametoindex(), or equivalent"""
 
642
    global if_nametoindex
 
643
    try:
 
644
        if_nametoindex = ctypes.cdll.LoadLibrary\
 
645
            (ctypes.util.find_library("c")).if_nametoindex
 
646
    except (OSError, AttributeError):
 
647
        if "struct" not in sys.modules:
 
648
            import struct
 
649
        if "fcntl" not in sys.modules:
 
650
            import fcntl
 
651
        def if_nametoindex(interface):
 
652
            "Get an interface index the hard way, i.e. using fcntl()"
 
653
            SIOCGIFINDEX = 0x8933  # From /usr/include/linux/sockios.h
 
654
            s = socket.socket()
 
655
            ifreq = fcntl.ioctl(s, SIOCGIFINDEX,
 
656
                                struct.pack("16s16x", interface))
 
657
            s.close()
 
658
            interface_index = struct.unpack("I", ifreq[16:20])[0]
 
659
            return interface_index
 
660
    return if_nametoindex(interface)
 
661
 
 
662
 
 
663
def daemon(nochdir = False, noclose = False):
 
664
    """See daemon(3).  Standard BSD Unix function.
 
665
    This should really exist as os.daemon, but it doesn't (yet)."""
 
666
    if os.fork():
 
667
        sys.exit()
 
668
    os.setsid()
 
669
    if not nochdir:
 
670
        os.chdir("/")
 
671
    if os.fork():
 
672
        sys.exit()
 
673
    if not noclose:
 
674
        # Close all standard open file descriptors
 
675
        null = os.open(os.path.devnull, os.O_NOCTTY | os.O_RDWR)
 
676
        if not stat.S_ISCHR(os.fstat(null).st_mode):
 
677
            raise OSError(errno.ENODEV,
 
678
                          "/dev/null not a character device")
 
679
        os.dup2(null, sys.stdin.fileno())
 
680
        os.dup2(null, sys.stdout.fileno())
 
681
        os.dup2(null, sys.stderr.fileno())
 
682
        if null > 2:
 
683
            os.close(null)
209
684
 
210
685
 
211
686
def main():
212
 
    parser = OptionParser()
 
687
    parser = OptionParser(version = "%%prog %s" % version)
213
688
    parser.add_option("-i", "--interface", type="string",
214
 
                      default="eth0", metavar="IF",
215
 
                      help="Interface to bind to")
216
 
    parser.add_option("--cert", type="string", default="cert.pem",
217
 
                      metavar="FILE",
218
 
                      help="Public key certificate to use")
219
 
    parser.add_option("--key", type="string", default="key.pem",
220
 
                      metavar="FILE",
221
 
                      help="Private key to use")
222
 
    parser.add_option("--ca", type="string", default="ca.pem",
223
 
                      metavar="FILE",
224
 
                      help="Certificate Authority certificate to use")
225
 
    parser.add_option("--crl", type="string", default="crl.pem",
226
 
                      metavar="FILE",
227
 
                      help="Certificate Revokation List to use")
228
 
    parser.add_option("-p", "--port", type="int", default=49001,
 
689
                      metavar="IF", help="Bind to interface IF")
 
690
    parser.add_option("-a", "--address", type="string",
 
691
                      help="Address to listen for requests on")
 
692
    parser.add_option("-p", "--port", type="int",
229
693
                      help="Port number to receive requests on")
230
 
    parser.add_option("--dh", type="int", metavar="BITS",
231
 
                      help="DH group to use")
232
 
    parser.add_option("-t", "--timeout", type="string", # Parsed later
233
 
                      default="15m",
234
 
                      help="Amount of downtime allowed for clients")
235
 
    parser.add_option("--interval", type="string", # Parsed later
236
 
                      default="5m",
237
 
                      help="How often to check that a client is up")
238
694
    parser.add_option("--check", action="store_true", default=False,
239
695
                      help="Run self-test")
240
 
    (options, args) = parser.parse_args()
241
 
 
 
696
    parser.add_option("--debug", action="store_true",
 
697
                      help="Debug mode; run in foreground and log to"
 
698
                      " terminal")
 
699
    parser.add_option("--priority", type="string", help="GnuTLS"
 
700
                      " priority string (see GnuTLS documentation)")
 
701
    parser.add_option("--servicename", type="string", metavar="NAME",
 
702
                      help="Zeroconf service name")
 
703
    parser.add_option("--configdir", type="string",
 
704
                      default="/etc/mandos", metavar="DIR",
 
705
                      help="Directory to search for configuration"
 
706
                      " files")
 
707
    options = parser.parse_args()[0]
 
708
    
242
709
    if options.check:
243
710
        import doctest
244
711
        doctest.testmod()
245
712
        sys.exit()
246
713
    
247
 
    # Parse the time arguments
248
 
    try:
249
 
        options.timeout = string_to_delta(options.timeout)
250
 
    except ValueError:
251
 
        parser.error("option --timeout: Unparseable time")
252
 
    
253
 
    try:
254
 
        options.interval = string_to_delta(options.interval)
255
 
    except ValueError:
256
 
        parser.error("option --interval: Unparseable time")
257
 
    
258
 
    cert = gnutls.crypto.X509Certificate(open(options.cert).read())
259
 
    key = gnutls.crypto.X509PrivateKey(open(options.key).read())
260
 
    ca = gnutls.crypto.X509Certificate(open(options.ca).read())
261
 
    crl = gnutls.crypto.X509CRL(open(options.crl).read())
262
 
    cred = gnutls.connection.X509Credentials(cert, key, [ca], [crl])
263
 
    
264
 
    # Parse config file
265
 
    defaults = {}
266
 
    client_config_object = ConfigParser.SafeConfigParser(defaults)
267
 
    client_config_object.read("mandos-clients.conf")
268
 
    clients = [Client(name=section, options=options,
269
 
                      **(dict(client_config_object.items(section))))
270
 
               for section in client_config_object.sections()]
271
 
    
272
 
    udp_server = IPv6_UDPServer((in6addr_any, options.port),
273
 
                                udp_handler,
274
 
                                options=options)
275
 
    
276
 
    tcp_server = IPv6_TCPServer((in6addr_any, options.port),
277
 
                                tcp_handler,
278
 
                                options=options,
279
 
                                clients=clients,
280
 
                                credentials=cred)
281
 
    
282
 
    while True:
283
 
        try:
284
 
            input, out, err = select.select((udp_server,
285
 
                                             tcp_server), (), ())
286
 
            if not input:
287
 
                pass
288
 
            else:
289
 
                for obj in input:
290
 
                    obj.handle_request()
291
 
        except KeyboardInterrupt:
292
 
            break
293
 
    
294
 
    # Cleanup here
 
714
    # Default values for config file for server-global settings
 
715
    server_defaults = { "interface": "",
 
716
                        "address": "",
 
717
                        "port": "",
 
718
                        "debug": "False",
 
719
                        "priority":
 
720
                        "SECURE256:!CTYPE-X.509:+CTYPE-OPENPGP",
 
721
                        "servicename": "Mandos",
 
722
                        }
 
723
    
 
724
    # Parse config file for server-global settings
 
725
    server_config = ConfigParser.SafeConfigParser(server_defaults)
 
726
    del server_defaults
 
727
    server_config.read(os.path.join(options.configdir, "mandos.conf"))
 
728
    # Convert the SafeConfigParser object to a dict
 
729
    server_settings = server_config.defaults()
 
730
    # Use getboolean on the boolean config option
 
731
    server_settings["debug"] = server_config.getboolean\
 
732
                               ("DEFAULT", "debug")
 
733
    del server_config
 
734
    
 
735
    # Override the settings from the config file with command line
 
736
    # options, if set.
 
737
    for option in ("interface", "address", "port", "debug",
 
738
                   "priority", "servicename", "configdir"):
 
739
        value = getattr(options, option)
 
740
        if value is not None:
 
741
            server_settings[option] = value
 
742
    del options
 
743
    # Now we have our good server settings in "server_settings"
 
744
    
 
745
    debug = server_settings["debug"]
 
746
    
 
747
    if not debug:
 
748
        syslogger.setLevel(logging.WARNING)
 
749
        console.setLevel(logging.WARNING)
 
750
    
 
751
    if server_settings["servicename"] != "Mandos":
 
752
        syslogger.setFormatter(logging.Formatter\
 
753
                               ('Mandos (%s): %%(levelname)s:'
 
754
                                ' %%(message)s'
 
755
                                % server_settings["servicename"]))
 
756
    
 
757
    # Parse config file with clients
 
758
    client_defaults = { "timeout": "1h",
 
759
                        "interval": "5m",
 
760
                        "checker": "fping -q -- %(host)s",
 
761
                        "host": "",
 
762
                        }
 
763
    client_config = ConfigParser.SafeConfigParser(client_defaults)
 
764
    client_config.read(os.path.join(server_settings["configdir"],
 
765
                                    "clients.conf"))
 
766
    
 
767
    clients = Set()
 
768
    tcp_server = IPv6_TCPServer((server_settings["address"],
 
769
                                 server_settings["port"]),
 
770
                                TCP_handler,
 
771
                                settings=server_settings,
 
772
                                clients=clients)
 
773
    pidfilename = "/var/run/mandos.pid"
 
774
    try:
 
775
        pidfile = open(pidfilename, "w")
 
776
    except IOError, error:
 
777
        logger.error("Could not open file %r", pidfilename)
 
778
    
 
779
    uid = 65534
 
780
    gid = 65534
 
781
    try:
 
782
        uid = pwd.getpwnam("mandos").pw_uid
 
783
    except KeyError:
 
784
        try:
 
785
            uid = pwd.getpwnam("nobody").pw_uid
 
786
        except KeyError:
 
787
            pass
 
788
    try:
 
789
        gid = pwd.getpwnam("mandos").pw_gid
 
790
    except KeyError:
 
791
        try:
 
792
            gid = pwd.getpwnam("nogroup").pw_gid
 
793
        except KeyError:
 
794
            pass
 
795
    try:
 
796
        os.setuid(uid)
 
797
        os.setgid(gid)
 
798
    except OSError, error:
 
799
        if error[0] != errno.EPERM:
 
800
            raise error
 
801
    
 
802
    global service
 
803
    service = AvahiService(name = server_settings["servicename"],
 
804
                           servicetype = "_mandos._tcp", )
 
805
    if server_settings["interface"]:
 
806
        service.interface = if_nametoindex\
 
807
                            (server_settings["interface"])
 
808
    
 
809
    global main_loop
 
810
    global bus
 
811
    global server
 
812
    # From the Avahi example code
 
813
    DBusGMainLoop(set_as_default=True )
 
814
    main_loop = gobject.MainLoop()
 
815
    bus = dbus.SystemBus()
 
816
    server = dbus.Interface(bus.get_object(avahi.DBUS_NAME,
 
817
                                           avahi.DBUS_PATH_SERVER),
 
818
                            avahi.DBUS_INTERFACE_SERVER)
 
819
    # End of Avahi example code
 
820
    
 
821
    def remove_from_clients(client):
 
822
        clients.remove(client)
 
823
        if not clients:
 
824
            logger.critical(u"No clients left, exiting")
 
825
            sys.exit()
 
826
    
 
827
    clients.update(Set(Client(name = section,
 
828
                              stop_hook = remove_from_clients,
 
829
                              config
 
830
                              = dict(client_config.items(section)))
 
831
                       for section in client_config.sections()))
 
832
    if not clients:
 
833
        logger.critical(u"No clients defined")
 
834
        sys.exit(1)
 
835
    
 
836
    if debug:
 
837
        # Redirect stdin so all checkers get /dev/null
 
838
        null = os.open(os.path.devnull, os.O_NOCTTY | os.O_RDWR)
 
839
        os.dup2(null, sys.stdin.fileno())
 
840
        if null > 2:
 
841
            os.close(null)
 
842
    else:
 
843
        # No console logging
 
844
        logger.removeHandler(console)
 
845
        # Close all input and output, do double fork, etc.
 
846
        daemon()
 
847
    
 
848
    try:
 
849
        pid = os.getpid()
 
850
        pidfile.write(str(pid) + "\n")
 
851
        pidfile.close()
 
852
        del pidfile
 
853
    except IOError:
 
854
        logger.error(u"Could not write to file %r with PID %d",
 
855
                     pidfilename, pid)
 
856
    except NameError:
 
857
        # "pidfile" was never created
 
858
        pass
 
859
    del pidfilename
 
860
    
 
861
    def cleanup():
 
862
        "Cleanup function; run on exit"
 
863
        global group
 
864
        # From the Avahi example code
 
865
        if not group is None:
 
866
            group.Free()
 
867
            group = None
 
868
        # End of Avahi example code
 
869
        
 
870
        while clients:
 
871
            client = clients.pop()
 
872
            client.stop_hook = None
 
873
            client.stop()
 
874
    
 
875
    atexit.register(cleanup)
 
876
    
 
877
    if not debug:
 
878
        signal.signal(signal.SIGINT, signal.SIG_IGN)
 
879
    signal.signal(signal.SIGHUP, lambda signum, frame: sys.exit())
 
880
    signal.signal(signal.SIGTERM, lambda signum, frame: sys.exit())
 
881
    
295
882
    for client in clients:
296
 
        client.stop_checker()
297
 
 
298
 
 
299
 
if __name__ == "__main__":
 
883
        client.start()
 
884
    
 
885
    tcp_server.enable()
 
886
    tcp_server.server_activate()
 
887
    
 
888
    # Find out what port we got
 
889
    service.port = tcp_server.socket.getsockname()[1]
 
890
    logger.info(u"Now listening on address %r, port %d, flowinfo %d,"
 
891
                u" scope_id %d" % tcp_server.socket.getsockname())
 
892
    
 
893
    #service.interface = tcp_server.socket.getsockname()[3]
 
894
    
 
895
    try:
 
896
        # From the Avahi example code
 
897
        server.connect_to_signal("StateChanged", server_state_changed)
 
898
        try:
 
899
            server_state_changed(server.GetState())
 
900
        except dbus.exceptions.DBusException, error:
 
901
            logger.critical(u"DBusException: %s", error)
 
902
            sys.exit(1)
 
903
        # End of Avahi example code
 
904
        
 
905
        gobject.io_add_watch(tcp_server.fileno(), gobject.IO_IN,
 
906
                             lambda *args, **kwargs:
 
907
                             tcp_server.handle_request\
 
908
                             (*args[2:], **kwargs) or True)
 
909
        
 
910
        logger.debug(u"Starting main loop")
 
911
        main_loop.run()
 
912
    except AvahiError, error:
 
913
        logger.critical(u"AvahiError: %s" + unicode(error))
 
914
        sys.exit(1)
 
915
    except KeyboardInterrupt:
 
916
        if debug:
 
917
            print
 
918
 
 
919
if __name__ == '__main__':
300
920
    main()
301