Statistics
| Branch: | Tag: | Revision:

root / vncauthproxy / proxy.py @ d3817d45

History | View | Annotate | Download (30.8 kB)

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

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

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

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

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

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

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

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

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

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

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

    
76
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

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

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

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

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

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

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

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

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

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

    
663
    return users
664

    
665

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

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

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

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

    
751
    return opts
752

    
753

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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