Statistics
| Branch: | Tag: | Revision:

root / vncauthproxy / proxy.py @ bd377d7e

History | View | Annotate | Download (30.8 kB)

1
#!/usr/bin/env python
2
"""
3
vncauthproxy - a VNC authentication proxy
4
"""
5
#
6
# Copyright (c) 2010-2013 Greek Research and Technology Network S.A.
7
#
8
# This program is free software; you can redistribute it and/or modify
9
# it under the terms of the GNU General Public License as published by
10
# the Free Software Foundation; either version 2 of the License, or
11
# (at your option) any later version.
12
#
13
# This program is distributed in the hope that it will be useful, but
14
# WITHOUT ANY WARRANTY; without even the implied warranty of
15
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
16
# General Public License for more details.
17
#
18
# You should have received a copy of the GNU General Public License
19
# along with this program; if not, write to the Free Software
20
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
21
# 02110-1301, USA.
22

    
23
# Daemon files
24
DEFAULT_LOG_FILE = "/var/log/vncauthproxy/vncauthproxy.log"
25
DEFAULT_PID_FILE = "/var/run/vncauthproxy/vncauthproxy.pid"
26

    
27
# By default, bind / listen for control connections to TCP(v4) 127.0.0.1:24999
28
DEFAULT_LISTEN_ADDRESS = "127.0.0.1"
29
DEFAULT_LISTEN_PORT = 24999
30

    
31
# Backlog for the control socket
32
DEFAULT_BACKLOG = 256
33

    
34
# Timeout for the VNC server connection establishment / RFB handshake
35
DEFAULT_SERVER_TIMEOUT = 60.0
36

    
37
# Connect retries and delay between retries for the VNC server socket
38
DEFAULT_CONNECT_RETRIES = 3
39
DEFAULT_RETRY_WAIT = 0.1
40

    
41
# Connect timeout for the listening sockets
42
DEFAULT_CONNECT_TIMEOUT = 30
43

    
44
# Port range for the listening sockets
45
#
46
# We must take care not to fall into the ephemeral port range,
47
# this can lead to transient failures to bind a chosen port.
48
#
49
# By default, Linux uses 32768 to 61000, see:
50
# http://www.ncftp.com/ncftpd/doc/misc/ephemeral_ports.html#Linux
51
# so 25000-30000 seems to be a sensible default.
52
#
53
# We also take into account the ports that Ganeti daemons bind to, the port
54
# range used by DRBD etc.
55
DEFAULT_MIN_PORT = 25000
56
DEFAULT_MAX_PORT = 30000
57

    
58
# SSL certificate / key files
59
DEFAULT_CERT_FILE = "/var/lib/vncauthproxy/cert.pem"
60
DEFAULT_KEY_FILE = "/var/lib/vncauthproxy/key.pem"
61

    
62
# Auth file
63
DEFAULT_AUTH_FILE = "/var/lib/vncauthproxy/users"
64

    
65
import os
66
import sys
67
import logging
68
import gevent
69
import gevent.event
70
import daemon
71
import random
72
import daemon.runner
73
import hashlib
74
import re
75

    
76
from vncauthproxy import rfb
77

    
78
try:
79
    import simplejson as json
80
except ImportError:
81
    import json
82

    
83
from gevent import socket, ssl
84
from signal import SIGINT, SIGTERM
85
from gevent.select import select
86

    
87
from lockfile import LockTimeout, AlreadyLocked
88
# Take care of differences between python-daemon versions.
89
try:
90
    from daemon import pidfile as pidlockfile
91
except ImportError:
92
    from daemon import pidlockfile
93

    
94

    
95
logger = None
96

    
97

    
98
class InternalError(Exception):
99
    """Exception for internal vncauthproxy errors"""
100
    pass
101

    
102

    
103
# Currently, gevent uses libevent-dns for asynchronous DNS resolution,
104
# which opens a socket upon initialization time. Since we can't get the fd
105
# reliably, We have to maintain all file descriptors open (which won't harm
106
# anyway)
107
class AllFilesDaemonContext(daemon.DaemonContext):
108
    """DaemonContext class keeping all file descriptors open"""
109
    def _get_exclude_file_descriptors(self):
110
        class All:
111
            def __contains__(self, value):
112
                return True
113
        return All()
114

    
115

    
116
class VncAuthProxy(gevent.Greenlet):
117
    """
118
    Simple class implementing a VNC Forwarder with MITM authentication as a
119
    Greenlet
120

121
    VncAuthProxy forwards VNC traffic from a specified port of the local host
122
    to a specified remote host:port. Furthermore, it implements VNC
123
    Authentication, intercepting the client/server handshake and asking the
124
    client for authentication even if the backend requires none.
125

126
    It is primarily intended for use in virtualization environments, as a VNC
127
    ``switch''.
128

129
    """
130
    id = 1
131

    
132
    def __init__(self, logger, client):
133
        """
134
        @type logger: logging.Logger
135
        @param logger: the logger to use
136
        @type client: socket.socket
137
        @param listeners: the client control connection socket
138

139
        """
140
        gevent.Greenlet.__init__(self)
141
        self.id = VncAuthProxy.id
142
        VncAuthProxy.id += 1
143
        self.log = logger
144
        self.client = client
145
        # A list of worker/forwarder greenlets, one for each direction
146
        self.workers = []
147
        self.listeners = []
148
        self.sport = None
149
        self.pool = None
150
        self.daddr = None
151
        self.dport = None
152
        self.server = None
153
        self.password = None
154

    
155
    def _cleanup(self):
156
        """Cleanup everything: workers, sockets, ports
157

158
        Kill all remaining forwarder greenlets, close all active sockets,
159
        return the source port to the pool if applicable, then exit
160
        gracefully.
161

162
        """
163
        # Make sure all greenlets are dead, then clean them up
164
        self.debug("Cleaning up %d workers", len(self.workers))
165
        for g in self.workers:
166
            g.kill()
167
        gevent.joinall(self.workers)
168
        del self.workers
169

    
170
        self.debug("Cleaning up sockets")
171
        while self.listeners:
172
            self.listeners.pop().close()
173

    
174
        if self.server:
175
            self.server.close()
176

    
177
        if self.client:
178
            self.client.close()
179

    
180
        # Reintroduce the port number of the client socket in
181
        # the port pool, if applicable.
182
        if not self.pool is None:
183
            self.pool.append(self.sport)
184
            self.debug("Returned port %d to port pool, contains %d ports",
185
                       self.sport, len(self.pool))
186

    
187
        self.info("Cleaned up connection, all done")
188
        raise gevent.GreenletExit
189

    
190
    def __str__(self):
191
        return "VncAuthProxy: %d -> %s:%d" % (self.sport, self.daddr,
192
                                              self.dport)
193

    
194
    def _forward(self, source, dest):
195
        """
196
        Forward traffic from source to dest
197

198
        @type source: socket
199
        @param source: source socket
200
        @type dest: socket
201
        @param dest: destination socket
202

203
        """
204

    
205
        while True:
206
            d = source.recv(16384)
207
            if d == '':
208
                if source == self.client:
209
                    self.info("Client connection closed")
210
                else:
211
                    self.info("Server connection closed")
212
                break
213
            dest.sendall(d)
214
        # No need to close the source and dest sockets here.
215
        # They are owned by and will be closed by the original greenlet.
216

    
217
    def _perform_server_handshake(self):
218
        """
219
        Initiate a connection with the backend server and perform basic
220
        RFB 3.8 handshake with it.
221

222
        Return a socket connected to the backend server.
223

224
        """
225
        server = None
226

    
227
        tries = VncAuthProxy.connect_retries
228
        while tries:
229
            tries -= 1
230

    
231
            # Initiate server connection
232
            for res in socket.getaddrinfo(self.daddr, self.dport,
233
                                          socket.AF_UNSPEC,
234
                                          socket.SOCK_STREAM, 0,
235
                                          socket.AI_PASSIVE):
236
                af, socktype, proto, canonname, sa = res
237
                try:
238
                    server = socket.socket(af, socktype, proto)
239
                except socket.error:
240
                    server = None
241
                    continue
242

    
243
                # Set socket timeout for the initial handshake
244
                server.settimeout(VncAuthProxy.server_timeout)
245

    
246
                try:
247
                    self.debug("Connecting to %s:%s", *sa[:2])
248
                    server.connect(sa)
249
                    self.debug("Connection to %s:%s successful", *sa[:2])
250
                except socket.error:
251
                    self.debug("Failed to perform sever hanshake, retrying...")
252
                    server.close()
253
                    server = None
254
                    continue
255

    
256
                # We succesfully connected to the server
257
                tries = 0
258
                break
259

    
260
            # Wait and retry
261
            gevent.sleep(VncAuthProxy.retry_wait)
262

    
263
        if server is None:
264
            raise InternalError("Failed to connect to server")
265

    
266
        version = server.recv(1024)
267
        if not rfb.check_version(version):
268
            raise InternalError("Unsupported RFB version: %s"
269
                                % version.strip())
270

    
271
        server.send(rfb.RFB_VERSION_3_8 + "\n")
272

    
273
        res = server.recv(1024)
274
        types = rfb.parse_auth_request(res)
275
        if not types:
276
            raise InternalError("Error handshaking with the server")
277

    
278
        else:
279
            self.debug("Supported authentication types: %s",
280
                       " ".join([str(x) for x in types]))
281

    
282
        if rfb.RFB_AUTHTYPE_NONE not in types:
283
            raise InternalError("Error, server demands authentication")
284

    
285
        server.send(rfb.to_u8(rfb.RFB_AUTHTYPE_NONE))
286

    
287
        # Check authentication response
288
        res = server.recv(4)
289
        res = rfb.from_u32(res)
290

    
291
        if res != 0:
292
            raise InternalError("Authentication error")
293

    
294
        # Reset the timeout for the rest of the session
295
        server.settimeout(None)
296

    
297
        self.server = server
298

    
299
    def _establish_connection(self):
300
        client = self.client
301
        ports = VncAuthProxy.ports
302

    
303
        # Receive and parse a client request.
304
        response = {
305
            "source_port": 0,
306
            "status": "FAILED",
307
        }
308
        try:
309
            # Control request, in JSON:
310
            #
311
            # {
312
            #     "source_port":
313
            #         <source port or 0 for automatic allocation>,
314
            #     "destination_address":
315
            #         <destination address of backend server>,
316
            #     "destination_port":
317
            #         <destination port>
318
            #     "password":
319
            #         <the password to use to authenticate clients>
320
            #     "auth_user":
321
            #         <user for control connection authentication>,
322
            #      "auth_password":
323
            #         <password for control connection authentication>,
324
            # }
325
            #
326
            # The <password> is used for MITM authentication of clients
327
            # connecting to <source_port>, who will subsequently be
328
            # forwarded to a VNC server listening at
329
            # <destination_address>:<destination_port>
330
            #
331
            # Control reply, in JSON:
332
            # {
333
            #     "source_port": <the allocated source port>
334
            #     "status": <one of "OK" or "FAILED">
335
            # }
336
            #
337
            buf = client.recv(1024)
338
            req = json.loads(buf)
339

    
340
            auth_user = req['auth_user']
341
            auth_password = req['auth_password']
342
            sport_orig = int(req['source_port'])
343
            self.daddr = req['destination_address']
344
            self.dport = int(req['destination_port'])
345
            self.password = req['password']
346

    
347
            if auth_user not in VncAuthProxy.authdb:
348
                msg = "Authentication failure: user not found"
349
                raise InternalError(msg)
350

    
351
            (cipher, authdb_password) = VncAuthProxy.authdb[auth_user]
352
            if cipher == 'HA1':
353
                message = auth_user + ':vncauthproxy:' + auth_password
354
                auth_password = hashlib.md5(message).hexdigest()
355

    
356
            if auth_password != authdb_password:
357
                msg = "Authentication failure: wrong password"
358
                raise InternalError(msg)
359
        except KeyError:
360
            msg = "Malformed request: %s" % buf
361
            raise InternalError(msg)
362
        except InternalError as err:
363
            self.warn(err)
364
            response['reason'] = str(err)
365
            client.send(json.dumps(response))
366
            client.close()
367
            raise gevent.GreenletExit
368
        except Exception as err:
369
            self.exception(err)
370
            self.error("Unexpected error")
371
            client.send(json.dumps(response))
372
            client.close()
373
            raise gevent.GreenletExit
374

    
375
        server = None
376
        pool = None
377
        sport = sport_orig
378
        try:
379
            # If the client has so indicated, pick an ephemeral source port
380
            # randomly, and remove it from the port pool.
381
            if sport_orig == 0:
382
                while True:
383
                    try:
384
                        sport = random.choice(ports)
385
                        ports.remove(sport)
386
                        break
387
                    except ValueError:
388
                        self.debug("Port %d already taken", sport)
389

    
390
                self.debug("Got port %d from pool, %d remaining",
391
                           sport, len(ports))
392
                pool = ports
393

    
394
            self.sport = sport
395
            self.pool = pool
396

    
397
            self.listeners = get_listening_sockets(sport)
398
            self._perform_server_handshake()
399

    
400
            self.info("New forwarding: %d (client req'd: %d) -> %s:%d",
401
                      sport, sport_orig, self.daddr, self.dport)
402
            response = {"source_port": sport,
403
                        "status": "OK"}
404
        except IndexError:
405
            self.error("FAILED forwarding, out of ports for [req'd by "
406
                       "client: %d -> %s:%d]",
407
                       sport_orig, self.daddr, self.dport)
408
            raise gevent.GreenletExit
409
        except InternalError as err:
410
            self.error(err)
411
            self.error(("FAILED forwarding: %d (client req'd: %d) -> "
412
                        "%s:%d"), sport, sport_orig, self.daddr, self.dport)
413
            if pool:
414
                pool.append(sport)
415
                self.debug("Returned port %d to pool, %d remanining",
416
                           sport, len(pool))
417
            if server:
418
                server.close()
419
            raise gevent.GreenletExit
420
        except Exception as err:
421
            self.exception(err)
422
            self.error("Unexpected error")
423
            self.error(("FAILED forwarding: %d (client req'd: %d) -> "
424
                        "%s:%d"), sport, sport_orig, self.daddr, self.dport)
425
            if pool:
426
                pool.append(sport)
427
                self.debug("Returned port %d to pool, %d remanining",
428
                           sport, len(pool))
429
            if server:
430
                server.close()
431
            raise gevent.GreenletExit
432
        finally:
433
            client.send(json.dumps(response))
434
            client.close()
435

    
436
    def _client_handshake(self):
437
        """
438
        Perform handshake/authentication with a connecting client
439

440
        Outline:
441
        1. Client connects
442
        2. We fake RFB 3.8 protocol and require VNC authentication
443
           [processing also supports RFB 3.3]
444
        3. Client accepts authentication method
445
        4. We send an authentication challenge
446
        5. Client sends the authentication response
447
        6. We check the authentication
448

449
        Upon return, self.client socket is connected to the client.
450

451
        """
452
        self.client.send(rfb.RFB_VERSION_3_8 + "\n")
453
        client_version_str = self.client.recv(1024)
454
        client_version = rfb.check_version(client_version_str)
455
        if not client_version:
456
            self.error("Invalid version: %s", client_version_str)
457
            raise gevent.GreenletExit
458

    
459
        # Both for RFB 3.3 and 3.8
460
        self.debug("Requesting authentication")
461
        auth_request = rfb.make_auth_request(rfb.RFB_AUTHTYPE_VNC,
462
                                             version=client_version)
463
        self.client.send(auth_request)
464

    
465
        # The client gets to propose an authtype only for RFB 3.8
466
        if client_version == rfb.RFB_VERSION_3_8:
467
            res = self.client.recv(1024)
468
            type = rfb.parse_client_authtype(res)
469
            if type == rfb.RFB_AUTHTYPE_ERROR:
470
                self.warn("Client refused authentication: %s", res[1:])
471
            else:
472
                self.debug("Client requested authtype %x", type)
473

    
474
            if type != rfb.RFB_AUTHTYPE_VNC:
475
                self.error("Wrong auth type: %d", type)
476
                self.client.send(rfb.to_u32(rfb.RFB_AUTH_ERROR))
477
                raise gevent.GreenletExit
478

    
479
        # Generate the challenge
480
        challenge = os.urandom(16)
481
        self.client.send(challenge)
482
        response = self.client.recv(1024)
483
        if len(response) != 16:
484
            self.error("Wrong response length %d, should be 16", len(response))
485
            raise gevent.GreenletExit
486

    
487
        if rfb.check_password(challenge, response, self.password):
488
            self.debug("Authentication successful")
489
        else:
490
            self.warn("Authentication failed")
491
            self.client.send(rfb.to_u32(rfb.RFB_AUTH_ERROR))
492
            raise gevent.GreenletExit
493

    
494
        # Accept the authentication
495
        self.client.send(rfb.to_u32(rfb.RFB_AUTH_SUCCESS))
496

    
497
    def _proxy(self):
498
        try:
499
            self.info("Waiting for a client to connect at %s",
500
                      ", ".join(["%s:%d" % s.getsockname()[:2]
501
                                 for s in self.listeners]))
502
            rlist, _, _ = select(self.listeners, [], [],
503
                                 timeout=VncAuthProxy.connect_timeout)
504
            if not rlist:
505
                self.info("Timed out, no connection after %d sec",
506
                          VncAuthProxy.connect_timeout)
507
                raise gevent.GreenletExit
508

    
509
            for sock in rlist:
510
                self.client, addrinfo = sock.accept()
511
                self.info("Connection from %s:%d", *addrinfo[:2])
512

    
513
                # Close all listening sockets, we only want a one-shot
514
                # connection from a single client.
515
                while self.listeners:
516
                    sock = self.listeners.pop().close()
517
                break
518

    
519
            # Perform RFB handshake with the client.
520
            self._client_handshake()
521

    
522
            # Bridge both connections through two "forwarder" greenlets.
523
            # This greenlet will wait until any of the workers dies.
524
            # Final cleanup will take place in _cleanup().
525
            dead = gevent.event.Event()
526
            dead.clear()
527

    
528
            # This callback will get called if any of the two workers dies.
529
            def callback(g):
530
                self.debug("Worker %d/%d died", self.workers.index(g),
531
                           len(self.workers))
532
                dead.set()
533

    
534
            self.workers.append(gevent.spawn(self._forward,
535
                                             self.client, self.server))
536
            self.workers.append(gevent.spawn(self._forward,
537
                                             self.server, self.client))
538
            for g in self.workers:
539
                g.link(callback)
540

    
541
            # Wait until any of the workers dies
542
            self.debug("Waiting for any of %d workers to die",
543
                       len(self.workers))
544
            dead.wait()
545

    
546
            # We can go now, _cleanup() will take care of
547
            # all worker, socket and port cleanup
548
            self.debug("A forwarder died, our work here is done")
549
            raise gevent.GreenletExit
550
        except Exception as err:
551
            # Any unhandled exception in the previous block
552
            # is an error and must be logged accordingly
553
            if not isinstance(err, gevent.GreenletExit):
554
                self.exception(err)
555
                self.error("Unexpected error")
556
            raise err
557
        finally:
558
            self._cleanup()
559

    
560
    def _run(self):
561
        self._establish_connection()
562
        self._proxy()
563

    
564
# Logging support inside VncAuthproxy
565
# Wrap all common logging functions in logging-specific methods
566
for funcname in ["info", "debug", "warn", "error", "critical",
567
                 "exception"]:
568

    
569
    def gen(funcname):
570
        def wrapped_log_func(self, *args, **kwargs):
571
            func = getattr(self.log, funcname)
572
            func("[C%d] %s" % (self.id, args[0]), *args[1:], **kwargs)
573
        return wrapped_log_func
574
    setattr(VncAuthProxy, funcname, gen(funcname))
575

    
576

    
577
def fatal_signal_handler(signame):
578
    logger.info("Caught %s, will raise SystemExit", signame)
579
    raise SystemExit
580

    
581

    
582
def get_listening_sockets(sport, saddr=None, reuse_addr=False):
583
    sockets = []
584

    
585
    # Use two sockets, one for IPv4, one for IPv6. IPv4-to-IPv6 mapped
586
    # addresses do not work reliably everywhere (under linux it may have
587
    # been disabled in /proc/sys/net/ipv6/bind_ipv6_only).
588
    for res in socket.getaddrinfo(saddr, sport, socket.AF_UNSPEC,
589
                                  socket.SOCK_STREAM, 0, socket.AI_PASSIVE):
590
        af, socktype, proto, canonname, sa = res
591
        try:
592
            s = None
593
            s = socket.socket(af, socktype, proto)
594

    
595
            if af == socket.AF_INET6:
596
                # Bind v6 only when AF_INET6, otherwise either v4 or v6 bind
597
                # will fail.
598
                s.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, 1)
599

    
600
            if reuse_addr:
601
                s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
602

    
603
            s.bind(sa)
604
            s.listen(1)
605
            sockets.append(s)
606
            logger.debug("Listening on %s:%d", *sa[:2])
607
        except socket.error as err:
608
            logger.error("Error binding to %s:%d: %s", sa[0], sa[1], err[1])
609
            if s:
610
                s.close()
611
            while sockets:
612
                sockets.pop().close()
613

    
614
            # Make sure we fail immediately if we cannot get a socket
615
            raise InternalError(err)
616

    
617
    return sockets
618

    
619

    
620
def parse_auth_file(auth_file):
621
    supported_ciphers = ('cleartext', 'HA1', None)
622
    regexp = re.compile(r'^\s*(?P<user>\S+)\s+({(?P<cipher>\S+)})?'
623
                        r'(?P<pass>\S+)\s*$')
624

    
625
    users = {}
626

    
627
    if os.path.isfile(auth_file) is False:
628
        logger.warning("Authentication file not found. Continuing without "
629
                       "users")
630
        return users
631

    
632
    try:
633
        with open(auth_file) as f:
634
            lines = [l.strip() for l in f.readlines()]
635

    
636
            for line in lines:
637
                if not line or line.startswith('#'):
638
                    continue
639

    
640
                m = regexp.match(line)
641
                if not m:
642
                    raise InternalError("Invaild entry in auth file: %s"
643
                                        % line)
644

    
645
                user = m.group('user')
646
                cipher = m.group('cipher')
647
                if cipher not in supported_ciphers:
648
                    raise InternalError("Unsupported cipher in auth file: "
649
                                        "%s" % line)
650

    
651
                password = (cipher, m.group('pass'))
652

    
653
                if user in users:
654
                    raise InternalError("Duplicate user entry in auth file")
655

    
656
                users[user] = password
657
    except IOError as err:
658
        logger.error("Error while reading the auth file:")
659
        logger.exception(err)
660

    
661
    if not users:
662
        logger.warning("No users defined")
663

    
664
    return users
665

    
666

    
667
def parse_arguments(args):
668
    from optparse import OptionParser
669

    
670
    parser = OptionParser()
671
    parser.add_option("-d", "--debug", action="store_true", dest="debug",
672
                      help="Enable debugging information")
673
    parser.add_option("--log", dest="log_file",
674
                      default=DEFAULT_LOG_FILE,
675
                      metavar="FILE",
676
                      help=("Write log to FILE (default: %s)" %
677
                            DEFAULT_LOG_FILE))
678
    parser.add_option('--pid-file', dest="pid_file",
679
                      default=DEFAULT_PID_FILE,
680
                      metavar='PIDFILE',
681
                      help=("Save PID to file (default: %s)" %
682
                            DEFAULT_PID_FILE))
683
    parser.add_option("--listen-address", dest="listen_address",
684
                      default=DEFAULT_LISTEN_ADDRESS,
685
                      metavar="LISTEN_ADDRESS",
686
                      help=("Address to listen for control connections"
687
                            "(default: 127.0.0.1)"))
688
    parser.add_option("--listen-port", dest="listen_port",
689
                      default=DEFAULT_LISTEN_PORT,
690
                      metavar="LISTEN_PORT",
691
                      help=("Port to listen for control connections"
692
                            "(default: %d)" % DEFAULT_LISTEN_PORT))
693
    parser.add_option("--server-timeout", dest="server_timeout",
694
                      default=DEFAULT_SERVER_TIMEOUT, type="float",
695
                      metavar="N",
696
                      help=("Wait for N seconds for the VNC server RFB "
697
                            "handshake (default %s)" % DEFAULT_SERVER_TIMEOUT))
698
    parser.add_option("--connect-retries", dest="connect_retries",
699
                      default=DEFAULT_CONNECT_RETRIES, type="int",
700
                      metavar="N",
701
                      help=("Retry N times to connect to the "
702
                            "server (default: %d)" %
703
                            DEFAULT_CONNECT_RETRIES))
704
    parser.add_option("--retry-wait", dest="retry_wait",
705
                      default=DEFAULT_RETRY_WAIT, type="float",
706
                      metavar="N",
707
                      help=("Wait N seconds before retrying "
708
                            "to connect to the server (default: %s)" %
709
                            DEFAULT_RETRY_WAIT))
710
    parser.add_option("--connect-timeout", dest="connect_timeout",
711
                      default=DEFAULT_CONNECT_TIMEOUT, type="int",
712
                      metavar="N",
713
                      help=("Wait N seconds for a client "
714
                            "to connect (default: %d)"
715
                            % DEFAULT_CONNECT_TIMEOUT))
716
    parser.add_option("-p", "--min-port", dest="min_port",
717
                      default=DEFAULT_MIN_PORT, type="int", metavar="MIN_PORT",
718
                      help=("The minimum port number to use for automatically-"
719
                            "allocated ephemeral ports (default: %s)" %
720
                            DEFAULT_MIN_PORT))
721
    parser.add_option("-P", "--max-port", dest="max_port",
722
                      default=DEFAULT_MAX_PORT, type="int", metavar="MAX_PORT",
723
                      help=("The maximum port number to use for automatically-"
724
                            "allocated ephemeral ports (default: %s)" %
725
                            DEFAULT_MAX_PORT))
726
    parser.add_option('--enable-ssl', dest="enable_ssl",
727
                      default=False, action='store_true',
728
                      help=("Enable SSL/TLS for control connections "
729
                            "(default: False"))
730
    parser.add_option('--cert-file', dest="cert_file",
731
                      default=DEFAULT_CERT_FILE,
732
                      metavar='CERTFILE',
733
                      help=("SSL certificate (default: %s)" %
734
                            DEFAULT_CERT_FILE))
735
    parser.add_option('--key-file', dest="key_file",
736
                      default=DEFAULT_KEY_FILE,
737
                      metavar='KEYFILE',
738
                      help=("SSL key (default: %s)" %
739
                            DEFAULT_KEY_FILE))
740
    parser.add_option('--auth-file', dest="auth_file",
741
                      default=DEFAULT_AUTH_FILE,
742
                      metavar='AUTHFILE',
743
                      help=("Authentication file (default: %s)" %
744
                            DEFAULT_AUTH_FILE))
745

    
746
    (opts, args) = parser.parse_args(args)
747

    
748
    if args:
749
        parser.print_help()
750
        sys.exit(1)
751

    
752
    return opts
753

    
754

    
755
def main():
756
    """Run the daemon from the command line"""
757

    
758
    opts = parse_arguments(sys.argv[1:])
759

    
760
    # Initialize logger
761
    lvl = logging.DEBUG if opts.debug else logging.INFO
762

    
763
    global logger
764
    logger = logging.getLogger("vncauthproxy")
765
    logger.setLevel(lvl)
766
    formatter = logging.Formatter(("%(asctime)s %(module)s[%(process)d] "
767
                                   " %(levelname)s: %(message)s"),
768
                                  "%Y-%m-%d %H:%M:%S")
769
    handler = logging.FileHandler(opts.log_file)
770
    handler.setFormatter(formatter)
771
    logger.addHandler(handler)
772

    
773
    try:
774
        # Create pidfile
775
        pidf = pidlockfile.TimeoutPIDLockFile(opts.pid_file, 10)
776

    
777
        # Init ephemeral port pool
778
        ports = range(opts.min_port, opts.max_port + 1)
779

    
780
        # Init VncAuthProxy class attributes
781
        VncAuthProxy.server_timeout = opts.server_timeout
782
        VncAuthProxy.connect_retries = opts.connect_retries
783
        VncAuthProxy.retry_wait = opts.retry_wait
784
        VncAuthProxy.connect_timeout = opts.connect_timeout
785
        VncAuthProxy.ports = ports
786

    
787
        VncAuthProxy.authdb = parse_auth_file(opts.auth_file)
788

    
789
        sockets = get_listening_sockets(opts.listen_port, opts.listen_address,
790
                                        reuse_addr=True)
791

    
792
        wrap_ssl = lambda sock: sock
793
        if opts.enable_ssl:
794
            ssl_prot = ssl.PROTOCOL_TLSv1
795
            wrap_ssl = lambda sock: ssl.wrap_socket(sock, server_side=True,
796
                                                    keyfile=opts.key_file,
797
                                                    certfile=opts.cert_file,
798
                                                    ssl_version=ssl_prot)
799

    
800
        # Become a daemon:
801
        # Redirect stdout and stderr to handler.stream to catch
802
        # early errors in the daemonization process [e.g., pidfile creation]
803
        # which will otherwise go to /dev/null.
804
        daemon_context = AllFilesDaemonContext(
805
            pidfile=pidf,
806
            umask=0022,
807
            stdout=handler.stream,
808
            stderr=handler.stream,
809
            files_preserve=[handler.stream])
810

    
811
        # Remove any stale PID files, left behind by previous invocations
812
        if daemon.runner.is_pidfile_stale(pidf):
813
            logger.warning("Removing stale PID lock file %s", pidf.path)
814
            pidf.break_lock()
815

    
816
        try:
817
            daemon_context.open()
818
        except (AlreadyLocked, LockTimeout):
819
            raise InternalError(("Failed to lock PID file %s, another "
820
                                 "instance running?"), pidf.path)
821

    
822
        logger.info("Became a daemon")
823

    
824
        # A fork() has occured while daemonizing,
825
        # we *must* reinit gevent
826
        gevent.reinit()
827

    
828
        # Catch signals to ensure graceful shutdown,
829
        #
830
        # Uses gevent.signal so the handler fires even during
831
        # gevent.socket.accept()
832
        gevent.signal(SIGINT, fatal_signal_handler, "SIGINT")
833
        gevent.signal(SIGTERM, fatal_signal_handler, "SIGTERM")
834
    except InternalError as err:
835
        logger.critical(err)
836
        sys.exit(1)
837
    except Exception as err:
838
        logger.critical("Unexpected error:")
839
        logger.exception(err)
840
        sys.exit(1)
841

    
842
    while True:
843
        try:
844
            client = None
845
            rlist, _, _ = select(sockets, [], [])
846
            for ctrl in rlist:
847
                client, _ = ctrl.accept()
848
                client = wrap_ssl(client)
849
                logger.info("New control connection")
850

    
851
                VncAuthProxy.spawn(logger, client)
852
            continue
853
        except Exception as err:
854
            logger.error("Unexpected error:")
855
            logger.exception(err)
856
            if client:
857
                client.close()
858
            continue
859
        except SystemExit:
860
            break
861

    
862
    try:
863
        logger.info("Closing control sockets")
864
        while sockets:
865
            sock = sockets.pop()
866
            sock.close()
867

    
868
        daemon_context.close()
869
        sys.exit(0)
870
    except Exception as err:
871
        logger.critical("Unexpected error:")
872
        logger.exception(err)
873
        sys.exit(1)