Statistics
| Branch: | Tag: | Revision:

root / vncauthproxy / proxy.py @ 4a1dd7af

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
    users = {}
602
    with open(auth_file) as f:
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
    if not users:
628
        raise "No users defined"
629

    
630
    return users
631

    
632

    
633
def parse_arguments(args):
634
    from optparse import OptionParser
635

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

    
712
    (opts, args) = parser.parse_args(args)
713

    
714
    if args:
715
        parser.print_help()
716
        sys.exit(1)
717

    
718
    return opts
719

    
720

    
721
def main():
722
    """Run the daemon from the command line"""
723

    
724
    opts = parse_arguments(sys.argv[1:])
725

    
726
    # Create pidfile
727
    pidf = pidlockfile.TimeoutPIDLockFile(opts.pid_file, 10)
728

    
729
    # Initialize logger
730
    lvl = logging.DEBUG if opts.debug else logging.INFO
731

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

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

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

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

    
766
    # A fork() has occured while daemonizing,
767
    # we *must* reinit gevent
768
    gevent.reinit()
769

    
770
    # Catch signals to ensure graceful shutdown,
771
    #
772
    # Uses gevent.signal so the handler fires even during
773
    # gevent.socket.accept()
774
    gevent.signal(SIGINT, fatal_signal_handler, "SIGINT")
775
    gevent.signal(SIGTERM, fatal_signal_handler, "SIGTERM")
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
    try:
788
        VncAuthProxy.authdb = parse_auth_file(opts.auth_file)
789
        sockets = get_listening_sockets(logger, opts.listen_port,
790
                                        opts.listen_address, reuse_addr=True)
791
    except socket.error as err:
792
        logger.exception(err)
793
        logger.critical("Error binding control socket")
794
        sys.exit(1)
795
    except Exception as err:
796
        logger.exception(err)
797
        logger.critical("Unexpected error: %s", err.args)
798
        sys.exit(1)
799

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

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

    
824
    logger.info("Closing control sockets")
825
    while sockets:
826
        sock = sockets.pop()
827
        sock.close()
828

    
829
    daemon_context.close()
830
    sys.exit(0)