Statistics
| Branch: | Tag: | Revision:

root / vncauthproxy / proxy.py @ 8d766971

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
import re
76

    
77
import rfb
78

    
79
try:
80
    import simplejson as json
81
except ImportError:
82
    import json
83

    
84
from gevent import socket, ssl
85
from signal import SIGINT, SIGTERM
86
from gevent.select import select
87

    
88
from lockfile import LockTimeout, AlreadyLocked
89
# Take care of differences between python-daemon versions.
90
try:
91
    from daemon import pidfile as pidlockfile
92
except ImportError:
93
    from daemon import pidlockfile
94

    
95

    
96
logger = None
97

    
98

    
99
class InternalError(Exception):
100
    """Exception for internal vncauthproxy errors"""
101
    pass
102

    
103

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

    
116

    
117
class VncAuthProxy(gevent.Greenlet):
118
    """
119
    Simple class implementing a VNC Forwarder with MITM authentication as a
120
    Greenlet
121

122
    VncAuthProxy forwards VNC traffic from a specified port of the local host
123
    to a specified remote host:port. Furthermore, it implements VNC
124
    Authentication, intercepting the client/server handshake and asking the
125
    client for authentication even if the backend requires none.
126

127
    It is primarily intended for use in virtualization environments, as a VNC
128
    ``switch''.
129

130
    """
131
    id = 1
132

    
133
    def __init__(self, logger, client):
134
        """
135
        @type logger: logging.Logger
136
        @param logger: the logger to use
137
        @type client: socket.socket
138
        @param listeners: the client control connection socket
139

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

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

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

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

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

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

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

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

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

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

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

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

203
        """
204

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

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

222
        Return a socket connected to the backend server.
223

224
        """
225
        server = None
226

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
297
        self.server = server
298

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

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

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

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

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

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

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

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

    
394
            self.sport = sport
395
            self.pool = pool
396

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
576

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

    
581

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

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

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

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

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

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

    
617
    return sockets
618

    
619

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

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

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

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

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

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

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

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

    
655
    if not users:
656
        logger.warn("No users specified.")
657

    
658
    return users
659

    
660

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

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

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

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

    
746
    return opts
747

    
748

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

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

    
754
    # Create pidfile
755
    pidf = pidlockfile.TimeoutPIDLockFile(opts.pid_file, 10)
756

    
757
    # Initialize logger
758
    lvl = logging.DEBUG if opts.debug else logging.INFO
759

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

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

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

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

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

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

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

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

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

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

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

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

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

    
866
    daemon_context.close()
867
    sys.exit(0)