Statistics
| Branch: | Tag: | Revision:

root / vncauthproxy / proxy.py @ 3b98303f

History | View | Annotate | Download (30.1 kB)

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

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

    
27
# By default, bind / listen for control connections to TCP(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 crypt
74

    
75
from vncauthproxy import rfb
76

    
77
try:
78
    import simplejson as json
79
except ImportError:
80
    import json
81

    
82
from gevent import socket, ssl
83
from signal import SIGINT, SIGTERM
84
from gevent.select import select
85

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

    
93

    
94
logger = None
95

    
96

    
97
class InternalError(Exception):
98
    """Exception for internal vncauthproxy errors"""
99
    pass
100

    
101

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

    
114

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

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

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

128
    """
129
    id = 1
130

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

138
        """
139
        gevent.Greenlet.__init__(self)
140
        self.id = VncAuthProxy.id
141
        VncAuthProxy.id += 1
142
        self.log = logger
143
        self.client = client
144
        # A list of worker/forwarder greenlets, one for each direction
145
        self.workers = []
146
        self.listeners = []
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 = "vncauthproxy authentication failure: user not found"
348
                raise InternalError(msg)
349

    
350
            (cipher, salt, authdb_hash) = VncAuthProxy.authdb[auth_user]
351
            crypt_result = crypt.crypt(auth_password, '$%s$%s$' %
352
                                                      (cipher, salt))
353
            passhash = crypt_result.lstrip('$').split('$', 2)[-1]
354

    
355
            if passhash != authdb_hash:
356
                msg = "vncauthproxy 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
    users = {}
621

    
622
    if os.path.isfile(auth_file) is False:
623
        logger.warning("Authentication file not found. Continuing without "
624
                       "users")
625
        return users
626

    
627
    try:
628
        with open(auth_file) as f:
629
            lines = [l.strip().split(':', 1) for l in f.readlines()]
630
            for (user, passhash) in lines:
631
                users[user] = passhash.lstrip('$').split('$', 2)
632
    except ValueError as err:
633
        logger.exception(err)
634
        raise InternalError("Malformed auth file")
635

    
636
    if not users:
637
        logger.warning("No users defined")
638

    
639
    return users
640

    
641

    
642
def parse_arguments(args):
643
    from optparse import OptionParser
644

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

    
721
    (opts, args) = parser.parse_args(args)
722

    
723
    if args:
724
        parser.print_help()
725
        sys.exit(1)
726

    
727
    return opts
728

    
729

    
730
def main():
731
    """Run the daemon from the command line"""
732

    
733
    opts = parse_arguments(sys.argv[1:])
734

    
735
    # Initialize logger
736
    lvl = logging.DEBUG if opts.debug else logging.INFO
737

    
738
    global logger
739
    logger = logging.getLogger("vncauthproxy")
740
    logger.setLevel(lvl)
741
    formatter = logging.Formatter(("%(asctime)s %(module)s[%(process)d] "
742
                                   " %(levelname)s: %(message)s"),
743
                                  "%Y-%m-%d %H:%M:%S")
744
    handler = logging.FileHandler(opts.log_file)
745
    handler.setFormatter(formatter)
746
    logger.addHandler(handler)
747

    
748
    try:
749
        # Create pidfile
750
        pidf = pidlockfile.TimeoutPIDLockFile(opts.pid_file, 10)
751

    
752
        # Init ephemeral port pool
753
        ports = range(opts.min_port, opts.max_port + 1)
754

    
755
        # Init VncAuthProxy class attributes
756
        VncAuthProxy.server_timeout = opts.server_timeout
757
        VncAuthProxy.connect_retries = opts.connect_retries
758
        VncAuthProxy.retry_wait = opts.retry_wait
759
        VncAuthProxy.connect_timeout = opts.connect_timeout
760
        VncAuthProxy.ports = ports
761

    
762
        VncAuthProxy.authdb = parse_auth_file(opts.auth_file)
763

    
764
        sockets = get_listening_sockets(opts.listen_port, opts.listen_address,
765
                                        reuse_addr=True)
766

    
767
        wrap_ssl = lambda sock: sock
768
        if opts.enable_ssl:
769
            ssl_prot = ssl.PROTOCOL_TLSv1
770
            wrap_ssl = lambda sock: ssl.wrap_socket(sock, server_side=True,
771
                                                    keyfile=opts.key_file,
772
                                                    certfile=opts.cert_file,
773
                                                    ssl_version=ssl_prot)
774

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

    
786
        # Remove any stale PID files, left behind by previous invocations
787
        if daemon.runner.is_pidfile_stale(pidf):
788
            logger.warning("Removing stale PID lock file %s", pidf.path)
789
            pidf.break_lock()
790

    
791
        try:
792
            daemon_context.open()
793
        except (AlreadyLocked, LockTimeout):
794
            raise InternalError(("Failed to lock PID file %s, another "
795
                                 "instance running?"), pidf.path)
796

    
797
        logger.info("Became a daemon")
798

    
799
        # A fork() has occured while daemonizing,
800
        # we *must* reinit gevent
801
        gevent.reinit()
802

    
803
        # Catch signals to ensure graceful shutdown,
804
        #
805
        # Uses gevent.signal so the handler fires even during
806
        # gevent.socket.accept()
807
        gevent.signal(SIGINT, fatal_signal_handler, "SIGINT")
808
        gevent.signal(SIGTERM, fatal_signal_handler, "SIGTERM")
809
    except InternalError as err:
810
        logger.critical(err)
811
        sys.exit(1)
812
    except Exception as err:
813
        logger.critical("Unexpected error:")
814
        logger.exception(err)
815
        sys.exit(1)
816

    
817
    while True:
818
        try:
819
            client = None
820
            rlist, _, _ = select(sockets, [], [])
821
            for ctrl in rlist:
822
                client, _ = ctrl.accept()
823
                client = wrap_ssl(client)
824
                logger.info("New control connection")
825

    
826
                VncAuthProxy.spawn(logger, client)
827
            continue
828
        except Exception as err:
829
            logger.error("Unexpected error:")
830
            logger.exception(err)
831
            if client:
832
                client.close()
833
            continue
834
        except SystemExit:
835
            break
836

    
837
    try:
838
        logger.info("Closing control sockets")
839
        while sockets:
840
            sock = sockets.pop()
841
            sock.close()
842

    
843
        daemon_context.close()
844
        sys.exit(0)
845
    except Exception as err:
846
        logger.critical("Unexpected error:")
847
        logger.exception(err)
848
        sys.exit(1)