Statistics
| Branch: | Tag: | Revision:

root / vncauthproxy / proxy.py @ d49bd2fb

History | View | Annotate | Download (29.1 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 *:24999
28
# (both IPv4 and IPv6)
29
DEFAULT_LISTEN_ADDRESS = None
30
DEFAULT_LISTEN_PORT = 24999
31

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

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

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

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

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

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

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

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

    
76
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:
92
    from daemon import pidlockfile
93

    
94

    
95
logger = None
96

    
97

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

    
110

    
111
class VncAuthProxy(gevent.Greenlet):
112
    """
113
    Simple class implementing a VNC Forwarder with MITM authentication as a
114
    Greenlet
115

116
    VncAuthProxy forwards VNC traffic from a specified port of the local host
117
    to a specified remote host:port. Furthermore, it implements VNC
118
    Authentication, intercepting the client/server handshake and asking the
119
    client for authentication even if the backend requires none.
120

121
    It is primarily intended for use in virtualization environments, as a VNC
122
    ``switch''.
123

124
    """
125
    id = 1
126

    
127
    def __init__(self, logger, client):
128
        """
129
        @type logger: logging.Logger
130
        @param logger: the logger to use
131
        @type client: socket.socket
132
        @param listeners: the client control connection socket
133

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

    
149
    def _cleanup(self):
150
        """Cleanup everything: workers, sockets, ports
151

152
        Kill all remaining forwarder greenlets, close all active sockets,
153
        return the source port to the pool if applicable, then exit
154
        gracefully.
155

156
        """
157
        # Make sure all greenlets are dead, then clean them up
158
        self.debug("Cleaning up %d workers", len(self.workers))
159
        for g in self.workers:
160
            g.kill()
161
        gevent.joinall(self.workers)
162
        del self.workers
163

    
164
        self.debug("Cleaning up sockets")
165
        while self.listeners:
166
            sock = self.listeners.pop().close()
167

    
168
        if self.server:
169
            self.server.close()
170

    
171
        if self.client:
172
            self.client.close()
173

    
174
        # Reintroduce the port number of the client socket in
175
        # the port pool, if applicable.
176
        if not self.pool is None:
177
            self.pool.append(self.sport)
178
            self.debug("Returned port %d to port pool, contains %d ports",
179
                       self.sport, len(self.pool))
180

    
181
        self.info("Cleaned up connection, all done")
182
        raise gevent.GreenletExit
183

    
184
    def __str__(self):
185
        return "VncAuthProxy: %d -> %s:%d" % (self.sport, self.daddr,
186
                                              self.dport)
187

    
188
    def _forward(self, source, dest):
189
        """
190
        Forward traffic from source to dest
191

192
        @type source: socket
193
        @param source: source socket
194
        @type dest: socket
195
        @param dest: destination socket
196

197
        """
198

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

    
211
    def _perform_server_handshake(self):
212
        """
213
        Initiate a connection with the backend server and perform basic
214
        RFB 3.8 handshake with it.
215

216
        Return a socket connected to the backend server.
217

218
        """
219
        server = None
220

    
221
        tries = VncAuthProxy.connect_retries
222
        while tries:
223
            tries -= 1
224

    
225
            # Initiate server connection
226
            for res in socket.getaddrinfo(self.daddr, self.dport,
227
                                          socket.AF_UNSPEC,
228
                                          socket.SOCK_STREAM, 0,
229
                                          socket.AI_PASSIVE):
230
                af, socktype, proto, canonname, sa = res
231
                try:
232
                    server = socket.socket(af, socktype, proto)
233
                except socket.error:
234
                    server = None
235
                    continue
236

    
237
                # Set socket timeout for the initial handshake
238
                server.settimeout(VncAuthProxy.server_timeout)
239

    
240
                try:
241
                    self.debug("Connecting to %s:%s", *sa[:2])
242
                    server.connect(sa)
243
                    self.debug("Connection to %s:%s successful", *sa[:2])
244
                except socket.error:
245
                    server.close()
246
                    server = None
247
                    continue
248

    
249
                # We succesfully connected to the server
250
                tries = 0
251
                break
252

    
253
            # Wait and retry
254
            gevent.sleep(VncAuthProxy.retry_wait)
255

    
256
        if server is None:
257
            raise Exception("Failed to connect to server")
258

    
259
        version = server.recv(1024)
260
        if not rfb.check_version(version):
261
            raise Exception("Unsupported RFB version: %s" % version.strip())
262

    
263
        server.send(rfb.RFB_VERSION_3_8 + "\n")
264

    
265
        res = server.recv(1024)
266
        types = rfb.parse_auth_request(res)
267
        if not types:
268
            raise Exception("Error handshaking with the server")
269

    
270
        else:
271
            self.debug("Supported authentication types: %s",
272
                         " ".join([str(x) for x in types]))
273

    
274
        if rfb.RFB_AUTHTYPE_NONE not in types:
275
            raise Exception("Error, server demands authentication")
276

    
277
        server.send(rfb.to_u8(rfb.RFB_AUTHTYPE_NONE))
278

    
279
        # Check authentication response
280
        res = server.recv(4)
281
        res = rfb.from_u32(res)
282

    
283
        if res != 0:
284
            raise Exception("Authentication error")
285

    
286
        # Reset the timeout for the rest of the session
287
        server.settimeout(None)
288

    
289
        self.server = server
290

    
291
    def _establish_connection(self):
292
        client = self.client
293
        ports = VncAuthProxy.ports
294

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

    
334
            auth_user = req['auth_user']
335
            auth_password = req['auth_password']
336
            sport_orig = int(req['source_port'])
337
            self.daddr = req['destination_address']
338
            self.dport = int(req['destination_port'])
339
            self.password = req['password']
340

    
341
            if auth_user not in VncAuthProxy.authdb:
342
                msg = "Authentication failure: user not found"
343
                raise Exception(msg)
344

    
345
            (cipher, authdb_password) = VncAuthProxy.authdb[auth_user]
346
            if cipher == 'HA1':
347
                message = auth_user + ':vncauthproxy:' + auth_password
348
                auth_password = hashlib.md5(message).hexdigest()
349

    
350
            if auth_password != authdb_password:
351
                msg = "Authentication failure: wrong password"
352
                raise Exception(msg)
353
        except KeyError:
354
            msg = "Malformed request: %s" % buf
355
            raise Exception(msg)
356
        except Exception as err:
357
            logger.exception(err)
358
            msg = err.args
359
            self.warn(msg)
360
            response['reason'] = msg
361
            client.send(json.dumps(response))
362
            client.close()
363
            raise gevent.GreenletExit
364

    
365
        server = None
366
        try:
367
            # If the client has so indicated, pick an ephemeral source port
368
            # randomly, and remove it from the port pool.
369
            if sport_orig == 0:
370
                while True:
371
                    try:
372
                        sport = random.choice(ports)
373
                        ports.remove(sport)
374
                        break
375
                    except ValueError:
376
                        self.debug("Port %d already taken", sport)
377

    
378
                self.debug("Got port %d from pool, %d remaining",
379
                             sport, len(ports))
380
                pool = ports
381
            else:
382
                sport = sport_orig
383
                pool = None
384

    
385
            self.sport = sport
386
            self.pool = pool
387

    
388
            self.listeners = get_listening_sockets(self, sport)
389
            self._perform_server_handshake()
390

    
391
            self.info("New forwarding: %d (client req'd: %d) -> %s:%d",
392
                        sport, sport_orig, self.daddr, self.dport)
393
            response = {"source_port": sport,
394
                        "status": "OK"}
395
        except IndexError:
396
            self.error(("FAILED forwarding, out of ports for [req'd by "
397
                          "client: %d -> %s:%d]"),
398
                         sport_orig, self.daddr, self.dport)
399
            raise gevent.GreenletExit
400
        except Exception, msg:
401
            self.error(msg)
402
            self.error(("FAILED forwarding: %d (client req'd: %d) -> "
403
                          "%s:%d"), sport, sport_orig, self.daddr, self.dport)
404
            if not pool is None:
405
                pool.append(sport)
406
                self.debug("Returned port %d to pool, %d remanining",
407
                             sport, len(pool))
408
            if not server is None:
409
                server.close()
410
            raise gevent.GreenletExit
411
        finally:
412
            client.send(json.dumps(response))
413
            client.close()
414

    
415
    def _client_handshake(self):
416
        """
417
        Perform handshake/authentication with a connecting client
418

419
        Outline:
420
        1. Client connects
421
        2. We fake RFB 3.8 protocol and require VNC authentication
422
           [processing also supports RFB 3.3]
423
        3. Client accepts authentication method
424
        4. We send an authentication challenge
425
        5. Client sends the authentication response
426
        6. We check the authentication
427

428
        Upon return, self.client socket is connected to the client.
429

430
        """
431
        self.client.send(rfb.RFB_VERSION_3_8 + "\n")
432
        client_version_str = self.client.recv(1024)
433
        client_version = rfb.check_version(client_version_str)
434
        if not client_version:
435
            self.error("Invalid version: %s", client_version_str)
436
            raise gevent.GreenletExit
437

    
438
        # Both for RFB 3.3 and 3.8
439
        self.debug("Requesting authentication")
440
        auth_request = rfb.make_auth_request(rfb.RFB_AUTHTYPE_VNC,
441
                                             version=client_version)
442
        self.client.send(auth_request)
443

    
444
        # The client gets to propose an authtype only for RFB 3.8
445
        if client_version == rfb.RFB_VERSION_3_8:
446
            res = self.client.recv(1024)
447
            type = rfb.parse_client_authtype(res)
448
            if type == rfb.RFB_AUTHTYPE_ERROR:
449
                self.warn("Client refused authentication: %s", res[1:])
450
            else:
451
                self.debug("Client requested authtype %x", type)
452

    
453
            if type != rfb.RFB_AUTHTYPE_VNC:
454
                self.error("Wrong auth type: %d", type)
455
                self.client.send(rfb.to_u32(rfb.RFB_AUTH_ERROR))
456
                raise gevent.GreenletExit
457

    
458
        # Generate the challenge
459
        challenge = os.urandom(16)
460
        self.client.send(challenge)
461
        response = self.client.recv(1024)
462
        if len(response) != 16:
463
            self.error("Wrong response length %d, should be 16", len(response))
464
            raise gevent.GreenletExit
465

    
466
        if rfb.check_password(challenge, response, self.password):
467
            self.debug("Authentication successful")
468
        else:
469
            self.warn("Authentication failed")
470
            self.client.send(rfb.to_u32(rfb.RFB_AUTH_ERROR))
471
            raise gevent.GreenletExit
472

    
473
        # Accept the authentication
474
        self.client.send(rfb.to_u32(rfb.RFB_AUTH_SUCCESS))
475

    
476
    def _proxy(self):
477
        try:
478
            self.info("Waiting for a client to connect at %s",
479
                      ", ".join(["%s:%d" % s.getsockname()[:2]
480
                                 for s in self.listeners]))
481
            rlist, _, _ = select(self.listeners, [], [],
482
                          timeout=VncAuthProxy.connect_timeout)
483
            if not rlist:
484
                self.info("Timed out, no connection after %d sec",
485
                          VncAuthProxy.connect_timeout)
486
                raise gevent.GreenletExit
487

    
488
            for sock in rlist:
489
                self.client, addrinfo = sock.accept()
490
                self.info("Connection from %s:%d", *addrinfo[:2])
491

    
492
                # Close all listening sockets, we only want a one-shot
493
                # connection from a single client.
494
                while self.listeners:
495
                    sock = self.listeners.pop().close()
496
                break
497

    
498
            # Perform RFB handshake with the client.
499
            self._client_handshake()
500

    
501
            # Bridge both connections through two "forwarder" greenlets.
502
            # This greenlet will wait until any of the workers dies.
503
            # Final cleanup will take place in _cleanup().
504
            dead = gevent.event.Event()
505
            dead.clear()
506

    
507
            # This callback will get called if any of the two workers dies.
508
            def callback(g):
509
                self.debug("Worker %d/%d died", self.workers.index(g),
510
                           len(self.workers))
511
                dead.set()
512

    
513
            self.workers.append(gevent.spawn(self._forward,
514
                                             self.client, self.server))
515
            self.workers.append(gevent.spawn(self._forward,
516
                                             self.server, self.client))
517
            for g in self.workers:
518
                g.link(callback)
519

    
520
            # Wait until any of the workers dies
521
            self.debug("Waiting for any of %d workers to die",
522
                       len(self.workers))
523
            dead.wait()
524

    
525
            # We can go now, _cleanup() will take care of
526
            # all worker, socket and port cleanup
527
            self.debug("A forwarder died, our work here is done")
528
            raise gevent.GreenletExit
529
        except Exception, e:
530
            # Any unhandled exception in the previous block
531
            # is an error and must be logged accordingly
532
            if not isinstance(e, gevent.GreenletExit):
533
                self.exception(e)
534
            raise e
535
        finally:
536
            self._cleanup()
537

    
538
    def _run(self):
539
        self._establish_connection()
540
        self._proxy()
541

    
542
# Logging support inside VncAuthproxy
543
# Wrap all common logging functions in logging-specific methods
544
for funcname in ["info", "debug", "warn", "error", "critical",
545
                 "exception"]:
546

    
547
    def gen(funcname):
548
        def wrapped_log_func(self, *args, **kwargs):
549
            func = getattr(self.log, funcname)
550
            func("[C%d] %s" % (self.id, args[0]), *args[1:], **kwargs)
551
        return wrapped_log_func
552
    setattr(VncAuthProxy, funcname, gen(funcname))
553

    
554

    
555
def fatal_signal_handler(signame):
556
    logger.info("Caught %s, will raise SystemExit", signame)
557
    raise SystemExit
558

    
559

    
560
def get_listening_sockets(logger, sport, saddr=None, reuse_addr=False):
561
    sockets = []
562

    
563
    # Use two sockets, one for IPv4, one for IPv6. IPv4-to-IPv6 mapped
564
    # addresses do not work reliably everywhere (under linux it may have
565
    # been disabled in /proc/sys/net/ipv6/bind_ipv6_only).
566
    for res in socket.getaddrinfo(saddr, sport, socket.AF_UNSPEC,
567
                                  socket.SOCK_STREAM, 0, socket.AI_PASSIVE):
568
        af, socktype, proto, canonname, sa = res
569
        try:
570
            s = None
571
            s = socket.socket(af, socktype, proto)
572

    
573
            if af == socket.AF_INET6:
574
                # Bind v6 only when AF_INET6, otherwise either v4 or v6 bind
575
                # will fail.
576
                s.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, 1)
577

    
578
            if reuse_addr:
579
                s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
580

    
581
            s.bind(sa)
582
            s.listen(1)
583
            sockets.append(s)
584
            logger.debug("Listening on %s:%d", *sa[:2])
585
        except socket.error, msg:
586
            logger.error("Error binding to %s:%d: %s", sa[0], sa[1], msg[1])
587
            if s:
588
                s.close()
589
            while sockets:
590
                sock = sockets.pop().close()
591

    
592
            # Make sure we fail immediately if we cannot get a socket
593
            raise msg
594

    
595
    return sockets
596

    
597

    
598
def parse_auth_file(auth_file):
599
    supported_ciphers = ('cleartext', 'HA1')
600

    
601
    with open(auth_file) as f:
602
        users = {}
603
        lines = [l.strip().split() for l in f.readlines()]
604

    
605
        for line in lines:
606
            if not line or line[0][0] == '#':
607
                continue
608

    
609
            if len(line) != 2:
610
                raise Exception("Invaild user entry in auth file")
611

    
612
            user = line[0]
613
            password = line[1]
614

    
615
            split_password = ('cleartext', password)
616
            if password[0] == '{':
617
                split_password = password[1:].split('}')
618
                if len(split_password) != 2 or not split_password[1] \
619
                        or split_password[0] not in supported_ciphers:
620
                    raise Exception("Invalid password format in auth file")
621

    
622
            if user in users:
623
                raise Exception("Duplicate user entry in auth file")
624

    
625
            users[user] = password
626

    
627
    return users
628

    
629

    
630
def parse_arguments(args):
631
    from optparse import OptionParser
632

    
633
    parser = OptionParser()
634
    parser.add_option("-d", "--debug", action="store_true", dest="debug",
635
                      help="Enable debugging information")
636
    parser.add_option("--log", dest="log_file",
637
                      default=DEFAULT_LOG_FILE,
638
                      metavar="FILE",
639
                      help=("Write log to FILE (default: %s)" %
640
                            DEFAULT_LOG_FILE))
641
    parser.add_option('--pid-file', dest="pid_file",
642
                      default=DEFAULT_PID_FILE,
643
                      metavar='PIDFILE',
644
                      help=("Save PID to file (default: %s)" %
645
                            DEFAULT_PID_FILE))
646
    parser.add_option("--listen-address", dest="listen_address",
647
                      default=DEFAULT_LISTEN_ADDRESS,
648
                      metavar="LISTEN_ADDRESS",
649
                      help=("Address to listen for control connections"
650
                            "(default: *)"))
651
    parser.add_option("--listen-port", dest="listen_port",
652
                      default=DEFAULT_LISTEN_PORT,
653
                      metavar="LISTEN_PORT",
654
                      help=("Port to listen for control connections"
655
                            "(default: %d)" % DEFAULT_LISTEN_PORT))
656
    parser.add_option("--server-timeout", dest="server_timeout",
657
                      default=DEFAULT_SERVER_TIMEOUT, type="float",
658
                      metavar="N",
659
                      help=("Wait for N seconds for the VNC server RFB "
660
                            "handshake (default %s)" % DEFAULT_SERVER_TIMEOUT))
661
    parser.add_option("--connect-retries", dest="connect_retries",
662
                      default=DEFAULT_CONNECT_RETRIES, type="int",
663
                      metavar="N",
664
                      help=("Retry N times to connect to the "
665
                            "server (default: %d)" %
666
                            DEFAULT_CONNECT_RETRIES))
667
    parser.add_option("--retry-wait", dest="retry_wait",
668
                      default=DEFAULT_RETRY_WAIT, type="float",
669
                      metavar="N",
670
                      help=("Wait N seconds before retrying "
671
                            "to connect to the server (default: %s)" %
672
                            DEFAULT_RETRY_WAIT))
673
    parser.add_option("--connect-timeout", dest="connect_timeout",
674
                      default=DEFAULT_CONNECT_TIMEOUT, type="int",
675
                      metavar="N",
676
                      help=("Wait N seconds for a client "
677
                            "to connect (default: %d)"
678
                            % DEFAULT_CONNECT_TIMEOUT))
679
    parser.add_option("-p", "--min-port", dest="min_port",
680
                      default=DEFAULT_MIN_PORT, type="int", metavar="MIN_PORT",
681
                      help=("The minimum port number to use for automatically-"
682
                            "allocated ephemeral ports (default: %s)" %
683
                            DEFAULT_MIN_PORT))
684
    parser.add_option("-P", "--max-port", dest="max_port",
685
                      default=DEFAULT_MAX_PORT, type="int", metavar="MAX_PORT",
686
                      help=("The maximum port number to use for automatically-"
687
                            "allocated ephemeral ports (default: %s)" %
688
                            DEFAULT_MAX_PORT))
689
    parser.add_option('--no-ssl', dest="no_ssl",
690
                      default=False, action='store_true',
691
                      help=("Disable SSL/TLS for control connections "
692
                            "(default: False"))
693
    parser.add_option('--cert-file', dest="cert_file",
694
                      default=DEFAULT_CERT_FILE,
695
                      metavar='CERTFILE',
696
                      help=("SSL certificate (default: %s)" %
697
                            DEFAULT_CERT_FILE))
698
    parser.add_option('--key-file', dest="key_file",
699
                      default=DEFAULT_KEY_FILE,
700
                      metavar='KEYFILE',
701
                      help=("SSL key (default: %s)" %
702
                            DEFAULT_KEY_FILE))
703
    parser.add_option('--auth-file', dest="auth_file",
704
                      default=DEFAULT_AUTH_FILE,
705
                      metavar='AUTHFILE',
706
                      help=("Authentication file (default: %s)" %
707
                            DEFAULT_AUTH_FILE))
708

    
709
    (opts, args) = parser.parse_args(args)
710

    
711
    if args:
712
        parser.print_help()
713
        sys.exit(1)
714

    
715
    return opts
716

    
717

    
718
def main():
719
    """Run the daemon from the command line"""
720

    
721
    opts = parse_arguments(sys.argv[1:])
722

    
723
    # Create pidfile
724
    pidf = pidlockfile.TimeoutPIDLockFile(opts.pid_file, 10)
725

    
726
    # Initialize logger
727
    lvl = logging.DEBUG if opts.debug else logging.INFO
728

    
729
    global logger
730
    logger = logging.getLogger("vncauthproxy")
731
    logger.setLevel(lvl)
732
    formatter = logging.Formatter(("%(asctime)s %(module)s[%(process)d] "
733
                                   " %(levelname)s: %(message)s"),
734
                                  "%Y-%m-%d %H:%M:%S")
735
    handler = logging.FileHandler(opts.log_file)
736
    handler.setFormatter(formatter)
737
    logger.addHandler(handler)
738

    
739
    # Become a daemon:
740
    # Redirect stdout and stderr to handler.stream to catch
741
    # early errors in the daemonization process [e.g., pidfile creation]
742
    # which will otherwise go to /dev/null.
743
    daemon_context = AllFilesDaemonContext(
744
        pidfile=pidf,
745
        umask=0022,
746
        stdout=handler.stream,
747
        stderr=handler.stream,
748
        files_preserve=[handler.stream])
749

    
750
    # Remove any stale PID files, left behind by previous invocations
751
    if daemon.runner.is_pidfile_stale(pidf):
752
        logger.warning("Removing stale PID lock file %s", pidf.path)
753
        pidf.break_lock()
754

    
755
    try:
756
        daemon_context.open()
757
    except (AlreadyLocked, LockTimeout):
758
        logger.critical(("Failed to lock PID file %s, another instance "
759
                         "running?"), pidf.path)
760
        sys.exit(1)
761
    logger.info("Became a daemon")
762

    
763
    # A fork() has occured while daemonizing,
764
    # we *must* reinit gevent
765
    gevent.reinit()
766

    
767
    # Catch signals to ensure graceful shutdown,
768
    #
769
    # Uses gevent.signal so the handler fires even during
770
    # gevent.socket.accept()
771
    gevent.signal(SIGINT, fatal_signal_handler, "SIGINT")
772
    gevent.signal(SIGTERM, fatal_signal_handler, "SIGTERM")
773

    
774
    # Init ephemeral port pool
775
    ports = range(opts.min_port, opts.max_port + 1)
776

    
777
    # Init VncAuthProxy class attributes
778
    VncAuthProxy.server_timeout = opts.server_timeout
779
    VncAuthProxy.connect_retries = opts.connect_retries
780
    VncAuthProxy.retry_wait = opts.retry_wait
781
    VncAuthProxy.connect_timeout = opts.connect_timeout
782
    VncAuthProxy.ports = ports
783

    
784
    try:
785
        VncAuthProxy.authdb = parse_auth_file(opts.auth_file)
786
        sockets = get_listening_sockets(logger, opts.listen_port,
787
                                        opts.listen_address, reuse_addr=True)
788
    except socket.error as err:
789
        logger.exception(err)
790
        logger.critical("Error binding control socket")
791
        sys.exit(1)
792
    except Exception as err:
793
        logger.exception(err)
794
        logger.critical("Unexpected error: %s", err.args)
795
        sys.exit(1)
796

    
797
    while True:
798
        try:
799
            client = None
800
            rlist, _, _ = select(sockets, [], [])
801
            for ctrl in rlist:
802
                client, _ = ctrl.accept()
803
                if not no_ssl:
804
                    client = ssl.wrap_socket(client,
805
                                             server_side=True,
806
                                             keyfile=opts.key_file,
807
                                             certfile=opts.cert_file,
808
                                             ssl_version=ssl.PROTOCOL_TLSv1)
809
                logger.info("New control connection")
810

    
811
                VncAuthProxy.spawn(logger, client)
812
            continue
813
        except Exception, e:
814
            logger.exception(e)
815
            if client:
816
                client.close()
817
            continue
818
        except SystemExit:
819
            break
820

    
821
    logger.info("Closing control sockets")
822
    while sockets:
823
        sock = sockets.pop()
824
        sock.close()
825

    
826
    daemon_context.close()
827
    sys.exit(0)