Statistics
| Branch: | Tag: | Revision:

root / vncauthproxy / proxy.py @ 6149f03e

History | View | Annotate | Download (30.5 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 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
            sock = 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 as err:
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(e, 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
                sock = sockets.pop().close()
612

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

    
616
    return sockets
617

    
618

    
619
def parse_auth_file(auth_file):
620
    supported_ciphers = ('cleartext', 'HA1')
621

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

    
627
            for line in lines:
628
                if not line or line[0][0] == '#':
629
                    continue
630

    
631
                if len(line) != 2:
632
                    raise InternalError("Invaild user entry in auth file")
633

    
634
                user = line[0]
635
                password = line[1]
636

    
637
                split_password = ('{cleartext}', password)
638
                if password[0] == '{':
639
                    split_password = password[1:].split('}')
640
                    if len(split_password) != 2 or not split_password[1] \
641
                            or split_password[0] not in supported_ciphers:
642
                        raise InternalError("Invalid password format "
643
                                            "in auth file")
644

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

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

    
653
    if not users:
654
        logger.warn("No users specified.")
655

    
656
    return users
657

    
658

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

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

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

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

    
744
    return opts
745

    
746

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

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

    
752
    # Create pidfile
753
    pidf = pidlockfile.TimeoutPIDLockFile(opts.pid_file, 10)
754

    
755
    # Initialize logger
756
    lvl = logging.DEBUG if opts.debug else logging.INFO
757

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

    
768
    # Become a daemon:
769
    # Redirect stdout and stderr to handler.stream to catch
770
    # early errors in the daemonization process [e.g., pidfile creation]
771
    # which will otherwise go to /dev/null.
772
    daemon_context = AllFilesDaemonContext(
773
        pidfile=pidf,
774
        umask=0022,
775
        stdout=handler.stream,
776
        stderr=handler.stream,
777
        files_preserve=[handler.stream])
778

    
779
    # Remove any stale PID files, left behind by previous invocations
780
    if daemon.runner.is_pidfile_stale(pidf):
781
        logger.warning("Removing stale PID lock file %s", pidf.path)
782
        pidf.break_lock()
783

    
784
    try:
785
        daemon_context.open()
786
    except (AlreadyLocked, LockTimeout):
787
        logger.critical(("Failed to lock PID file %s, another instance "
788
                         "running?"), pidf.path)
789
        sys.exit(1)
790
    logger.info("Became a daemon")
791

    
792
    # A fork() has occured while daemonizing,
793
    # we *must* reinit gevent
794
    gevent.reinit()
795

    
796
    # Catch signals to ensure graceful shutdown,
797
    #
798
    # Uses gevent.signal so the handler fires even during
799
    # gevent.socket.accept()
800
    gevent.signal(SIGINT, fatal_signal_handler, "SIGINT")
801
    gevent.signal(SIGTERM, fatal_signal_handler, "SIGTERM")
802

    
803
    # Init ephemeral port pool
804
    ports = range(opts.min_port, opts.max_port + 1)
805

    
806
    # Init VncAuthProxy class attributes
807
    VncAuthProxy.server_timeout = opts.server_timeout
808
    VncAuthProxy.connect_retries = opts.connect_retries
809
    VncAuthProxy.retry_wait = opts.retry_wait
810
    VncAuthProxy.connect_timeout = opts.connect_timeout
811
    VncAuthProxy.ports = ports
812

    
813
    try:
814
        VncAuthProxy.authdb = parse_auth_file(opts.auth_file)
815
    except InternalError as err:
816
        logger.critical(err)
817
        sys.exit(1)
818
    except Exception as err:
819
        logger.exception(err)
820
        logger.error("Unexpected error")
821
        sys.exit(1)
822

    
823
    try:
824
        sockets = get_listening_sockets(opts.listen_port, opts.listen_address,
825
                                        reuse_addr=True)
826
    except InternalError as err:
827
        logger.critical("Error binding control socket")
828
        sys.exit(1)
829
    except Exception as err:
830
        logger.exception(err)
831
        logger.error("Unexpected error")
832
        sys.exit(1)
833

    
834
    while True:
835
        try:
836
            client = None
837
            rlist, _, _ = select(sockets, [], [])
838
            for ctrl in rlist:
839
                client, _ = ctrl.accept()
840
                if not opts.no_ssl:
841
                    client = ssl.wrap_socket(client,
842
                                             server_side=True,
843
                                             keyfile=opts.key_file,
844
                                             certfile=opts.cert_file,
845
                                             ssl_version=ssl.PROTOCOL_TLSv1)
846
                logger.info("New control connection")
847

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

    
859
    logger.info("Closing control sockets")
860
    while sockets:
861
        sock = sockets.pop()
862
        sock.close()
863

    
864
    daemon_context.close()
865
    sys.exit(0)