Statistics
| Branch: | Tag: | Revision:

root / vncauthproxy / proxy.py @ bb8c7e81

History | View | Annotate | Download (30.6 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
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.sport = None
148
        self.pool = None
149
        self.daddr = None
150
        self.dport = None
151
        self.server = None
152
        self.password = None
153

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

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

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

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

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

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

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

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

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

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

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

202
        """
203

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

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

221
        Return a socket connected to the backend server.
222

223
        """
224
        server = None
225

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
296
        self.server = server
297

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

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

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

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

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

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

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

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

    
393
            self.sport = sport
394
            self.pool = pool
395

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
575

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

    
580

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

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

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

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

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

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

    
616
    return sockets
617

    
618

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

    
624
    users = {}
625
    try:
626
        with open(auth_file) as f:
627
            lines = [l.strip() for l in f.readlines()]
628

    
629
            for line in lines:
630
                if not line or line.startswith('#'):
631
                    continue
632

    
633
                m = regexp.match(line)
634
                if not m:
635
                    raise InternalError("Invaild entry in auth file: %s"
636
                                        % line)
637

    
638
                user = m.group('user')
639
                cipher = m.group('cipher')
640
                if cipher not in supported_ciphers:
641
                    raise InternalError("Unsupported cipher in auth file: "
642
                                        "%s" % line)
643

    
644
                password = (cipher, m.group('pass'))
645

    
646
                if user in users:
647
                    raise InternalError("Duplicate user entry in auth file")
648

    
649
                users[user] = password
650
    except IOError as err:
651
        logger.critical("Couldn't read auth file")
652
        raise InternalError(err)
653

    
654
    if not users:
655
        raise InternalError("No users defined")
656

    
657
    return users
658

    
659

    
660
def parse_arguments(args):
661
    from optparse import OptionParser
662

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

    
739
    (opts, args) = parser.parse_args(args)
740

    
741
    if args:
742
        parser.print_help()
743
        sys.exit(1)
744

    
745
    return opts
746

    
747

    
748
def main():
749
    """Run the daemon from the command line"""
750

    
751
    opts = parse_arguments(sys.argv[1:])
752

    
753
    # Initialize logger
754
    lvl = logging.DEBUG if opts.debug else logging.INFO
755

    
756
    global logger
757
    logger = logging.getLogger("vncauthproxy")
758
    logger.setLevel(lvl)
759
    formatter = logging.Formatter(("%(asctime)s %(module)s[%(process)d] "
760
                                   " %(levelname)s: %(message)s"),
761
                                  "%Y-%m-%d %H:%M:%S")
762
    handler = logging.FileHandler(opts.log_file)
763
    handler.setFormatter(formatter)
764
    logger.addHandler(handler)
765

    
766
    try:
767
        # Create pidfile
768
        pidf = pidlockfile.TimeoutPIDLockFile(opts.pid_file, 10)
769

    
770
        # Init ephemeral port pool
771
        ports = range(opts.min_port, opts.max_port + 1)
772

    
773
        # Init VncAuthProxy class attributes
774
        VncAuthProxy.server_timeout = opts.server_timeout
775
        VncAuthProxy.connect_retries = opts.connect_retries
776
        VncAuthProxy.retry_wait = opts.retry_wait
777
        VncAuthProxy.connect_timeout = opts.connect_timeout
778
        VncAuthProxy.ports = ports
779

    
780
        VncAuthProxy.authdb = parse_auth_file(opts.auth_file)
781

    
782
        sockets = get_listening_sockets(opts.listen_port, opts.listen_address,
783
                                        reuse_addr=True)
784

    
785
        wrap_ssl = lambda sock: sock
786
        if opts.enable_ssl:
787
            ssl_prot = ssl.PROTOCOL_TLSv1
788
            wrap_ssl = lambda sock: ssl.wrap_socket(sock, server_side=True,
789
                                                    keyfile=opts.key_file,
790
                                                    certfile=opts.cert_file,
791
                                                    ssl_version=ssl_prot)
792

    
793
        # Become a daemon:
794
        # Redirect stdout and stderr to handler.stream to catch
795
        # early errors in the daemonization process [e.g., pidfile creation]
796
        # which will otherwise go to /dev/null.
797
        daemon_context = AllFilesDaemonContext(
798
            pidfile=pidf,
799
            umask=0022,
800
            stdout=handler.stream,
801
            stderr=handler.stream,
802
            files_preserve=[handler.stream])
803

    
804
        # Remove any stale PID files, left behind by previous invocations
805
        if daemon.runner.is_pidfile_stale(pidf):
806
            logger.warning("Removing stale PID lock file %s", pidf.path)
807
            pidf.break_lock()
808

    
809
        try:
810
            daemon_context.open()
811
        except (AlreadyLocked, LockTimeout):
812
            raise InternalError(("Failed to lock PID file %s, another "
813
                                 "instance running?"), pidf.path)
814

    
815
        logger.info("Became a daemon")
816

    
817
        # A fork() has occured while daemonizing,
818
        # we *must* reinit gevent
819
        gevent.reinit()
820

    
821
        # Catch signals to ensure graceful shutdown,
822
        #
823
        # Uses gevent.signal so the handler fires even during
824
        # gevent.socket.accept()
825
        gevent.signal(SIGINT, fatal_signal_handler, "SIGINT")
826
        gevent.signal(SIGTERM, fatal_signal_handler, "SIGTERM")
827
    except InternalError as err:
828
        logger.critical(err)
829
        sys.exit(1)
830
    except Exception as err:
831
        logger.critical("Unexpected error:")
832
        logger.exception(err)
833
        sys.exit(1)
834

    
835
    while True:
836
        try:
837
            client = None
838
            rlist, _, _ = select(sockets, [], [])
839
            for ctrl in rlist:
840
                client, _ = ctrl.accept()
841
                client = wrap_ssl(client)
842
                logger.info("New control connection")
843

    
844
                VncAuthProxy.spawn(logger, client)
845
            continue
846
        except Exception as err:
847
            logger.error("Unexpected error:")
848
            logger.exception(err)
849
            if client:
850
                client.close()
851
            continue
852
        except SystemExit:
853
            break
854

    
855
    try:
856
        logger.info("Closing control sockets")
857
        while sockets:
858
            sock = sockets.pop()
859
            sock.close()
860

    
861
        daemon_context.close()
862
        sys.exit(0)
863
    except Exception as err:
864
        logger.critical("Unexpected error:")
865
        logger.exception(err)
866
        sys.exit(1)