Statistics
| Branch: | Tag: | Revision:

root / vncauthproxy / proxy.py @ 7eb27319

History | View | Annotate | Download (21 kB)

1 138d0e8b Faidon Liambotis
#!/usr/bin/env python
2 138d0e8b Faidon Liambotis
"""
3 138d0e8b Faidon Liambotis
vncauthproxy - a VNC authentication proxy
4 138d0e8b Faidon Liambotis
"""
5 138d0e8b Faidon Liambotis
#
6 138d0e8b Faidon Liambotis
# Copyright (c) 2010-2011 Greek Research and Technology Network S.A.
7 138d0e8b Faidon Liambotis
#
8 138d0e8b Faidon Liambotis
# This program is free software; you can redistribute it and/or modify
9 138d0e8b Faidon Liambotis
# it under the terms of the GNU General Public License as published by
10 138d0e8b Faidon Liambotis
# the Free Software Foundation; either version 2 of the License, or
11 138d0e8b Faidon Liambotis
# (at your option) any later version.
12 138d0e8b Faidon Liambotis
#
13 138d0e8b Faidon Liambotis
# This program is distributed in the hope that it will be useful, but
14 138d0e8b Faidon Liambotis
# WITHOUT ANY WARRANTY; without even the implied warranty of
15 138d0e8b Faidon Liambotis
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
16 138d0e8b Faidon Liambotis
# General Public License for more details.
17 138d0e8b Faidon Liambotis
#
18 138d0e8b Faidon Liambotis
# You should have received a copy of the GNU General Public License
19 138d0e8b Faidon Liambotis
# along with this program; if not, write to the Free Software
20 138d0e8b Faidon Liambotis
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
21 138d0e8b Faidon Liambotis
# 02110-1301, USA.
22 138d0e8b Faidon Liambotis
23 138d0e8b Faidon Liambotis
DEFAULT_CTRL_SOCKET = "/var/run/vncauthproxy/ctrl.sock"
24 138d0e8b Faidon Liambotis
DEFAULT_LOG_FILE = "/var/log/vncauthproxy/vncauthproxy.log"
25 138d0e8b Faidon Liambotis
DEFAULT_PID_FILE = "/var/run/vncauthproxy/vncauthproxy.pid"
26 138d0e8b Faidon Liambotis
DEFAULT_CONNECT_TIMEOUT = 30
27 7eb27319 Stratos Psomadakis
DEFAULT_CONNECT_RETRIES = 3
28 7eb27319 Stratos Psomadakis
DEFAULT_RETRY_WAIT = 0.1
29 138d0e8b Faidon Liambotis
# Default values per http://www.iana.org/assignments/port-numbers
30 138d0e8b Faidon Liambotis
DEFAULT_MIN_PORT = 49152 
31 138d0e8b Faidon Liambotis
DEFAULT_MAX_PORT = 65535
32 138d0e8b Faidon Liambotis
33 138d0e8b Faidon Liambotis
import os
34 138d0e8b Faidon Liambotis
import sys
35 138d0e8b Faidon Liambotis
import logging
36 138d0e8b Faidon Liambotis
import gevent
37 138d0e8b Faidon Liambotis
import daemon
38 138d0e8b Faidon Liambotis
import random
39 138d0e8b Faidon Liambotis
import daemon.pidlockfile
40 138d0e8b Faidon Liambotis
41 138d0e8b Faidon Liambotis
import rfb
42 138d0e8b Faidon Liambotis
 
43 138d0e8b Faidon Liambotis
try:
44 138d0e8b Faidon Liambotis
    import simplejson as json
45 138d0e8b Faidon Liambotis
except ImportError:
46 138d0e8b Faidon Liambotis
    import json
47 138d0e8b Faidon Liambotis
48 138d0e8b Faidon Liambotis
from gevent import socket
49 138d0e8b Faidon Liambotis
from signal import SIGINT, SIGTERM
50 138d0e8b Faidon Liambotis
from gevent import signal
51 138d0e8b Faidon Liambotis
from gevent.select import select
52 512c571e Stratos Psomadakis
from time import sleep
53 138d0e8b Faidon Liambotis
54 88420a63 Faidon Liambotis
logger = None
55 88420a63 Faidon Liambotis
56 376a8634 Vangelis Koukis
# Currently, gevent uses libevent-dns for asynchornous DNS resolution,
57 376a8634 Vangelis Koukis
# which opens a socket upon initialization time. Since we can't get the fd
58 376a8634 Vangelis Koukis
# reliably, We have to maintain all file descriptors open (which won't harm
59 376a8634 Vangelis Koukis
# anyway)
60 376a8634 Vangelis Koukis
61 376a8634 Vangelis Koukis
class AllFilesDaemonContext(daemon.DaemonContext):
62 376a8634 Vangelis Koukis
    """DaemonContext class keeping all file descriptors open"""
63 376a8634 Vangelis Koukis
    def _get_exclude_file_descriptors(self):
64 376a8634 Vangelis Koukis
        class All:
65 376a8634 Vangelis Koukis
            def __contains__(self, value):
66 376a8634 Vangelis Koukis
                return True
67 376a8634 Vangelis Koukis
        return All()
68 376a8634 Vangelis Koukis
69 376a8634 Vangelis Koukis
70 138d0e8b Faidon Liambotis
class VncAuthProxy(gevent.Greenlet):
71 138d0e8b Faidon Liambotis
    """
72 138d0e8b Faidon Liambotis
    Simple class implementing a VNC Forwarder with MITM authentication as a
73 138d0e8b Faidon Liambotis
    Greenlet
74 138d0e8b Faidon Liambotis

75 138d0e8b Faidon Liambotis
    VncAuthProxy forwards VNC traffic from a specified port of the local host
76 138d0e8b Faidon Liambotis
    to a specified remote host:port. Furthermore, it implements VNC
77 138d0e8b Faidon Liambotis
    Authentication, intercepting the client/server handshake and asking the
78 138d0e8b Faidon Liambotis
    client for authentication even if the backend requires none.
79 138d0e8b Faidon Liambotis

80 138d0e8b Faidon Liambotis
    It is primarily intended for use in virtualization environments, as a VNC
81 138d0e8b Faidon Liambotis
    ``switch''.
82 138d0e8b Faidon Liambotis

83 138d0e8b Faidon Liambotis
    """
84 138d0e8b Faidon Liambotis
    id = 1
85 138d0e8b Faidon Liambotis
86 512c571e Stratos Psomadakis
    def __init__(self, logger, listeners, pool, daddr, dport, server, password, connect_timeout):
87 138d0e8b Faidon Liambotis
        """
88 138d0e8b Faidon Liambotis
        @type logger: logging.Logger
89 138d0e8b Faidon Liambotis
        @param logger: the logger to use
90 138d0e8b Faidon Liambotis
        @type listeners: list
91 138d0e8b Faidon Liambotis
        @param listeners: list of listening sockets to use for client connections
92 138d0e8b Faidon Liambotis
        @type pool: list
93 138d0e8b Faidon Liambotis
        @param pool: if not None, return the client port number into this port pool
94 138d0e8b Faidon Liambotis
        @type daddr: str
95 138d0e8b Faidon Liambotis
        @param daddr: destination address (IPv4, IPv6 or hostname)
96 138d0e8b Faidon Liambotis
        @type dport: int
97 138d0e8b Faidon Liambotis
        @param dport: destination port
98 512c571e Stratos Psomadakis
        @type server: socket
99 512c571e Stratos Psomadakis
        @param server: VNC server socket
100 138d0e8b Faidon Liambotis
        @type password: str
101 138d0e8b Faidon Liambotis
        @param password: password to request from the client
102 138d0e8b Faidon Liambotis
        @type connect_timeout: int
103 138d0e8b Faidon Liambotis
        @param connect_timeout: how long to wait for client connections
104 138d0e8b Faidon Liambotis
                                (seconds)
105 138d0e8b Faidon Liambotis

106 138d0e8b Faidon Liambotis
        """
107 138d0e8b Faidon Liambotis
        gevent.Greenlet.__init__(self)
108 138d0e8b Faidon Liambotis
        self.id = VncAuthProxy.id
109 138d0e8b Faidon Liambotis
        VncAuthProxy.id += 1
110 138d0e8b Faidon Liambotis
        self.log = logger
111 138d0e8b Faidon Liambotis
        self.listeners = listeners
112 138d0e8b Faidon Liambotis
        # All listening sockets are assumed to be on the same port
113 138d0e8b Faidon Liambotis
        self.sport = listeners[0].getsockname()[1]
114 138d0e8b Faidon Liambotis
        self.pool = pool
115 138d0e8b Faidon Liambotis
        self.daddr = daddr
116 138d0e8b Faidon Liambotis
        self.dport = dport
117 512c571e Stratos Psomadakis
        self.server = server
118 138d0e8b Faidon Liambotis
        self.password = password
119 138d0e8b Faidon Liambotis
        self.client = None
120 138d0e8b Faidon Liambotis
        self.timeout = connect_timeout
121 138d0e8b Faidon Liambotis
122 138d0e8b Faidon Liambotis
    def _cleanup(self):
123 138d0e8b Faidon Liambotis
        """Close all active sockets and exit gracefully"""
124 138d0e8b Faidon Liambotis
        # Reintroduce the port number of the client socket in
125 138d0e8b Faidon Liambotis
        # the port pool, if applicable.
126 138d0e8b Faidon Liambotis
        if not self.pool is None:
127 138d0e8b Faidon Liambotis
            self.pool.append(self.sport)
128 138d0e8b Faidon Liambotis
            self.log.debug("Returned port %d to port pool, contains %d ports",
129 138d0e8b Faidon Liambotis
                self.sport, len(self.pool))
130 138d0e8b Faidon Liambotis
131 138d0e8b Faidon Liambotis
        while self.listeners:
132 138d0e8b Faidon Liambotis
            self.listeners.pop().close()
133 138d0e8b Faidon Liambotis
        if self.server:
134 138d0e8b Faidon Liambotis
            self.server.close()
135 138d0e8b Faidon Liambotis
        if self.client:
136 138d0e8b Faidon Liambotis
            self.client.close()
137 138d0e8b Faidon Liambotis
138 138d0e8b Faidon Liambotis
        raise gevent.GreenletExit
139 138d0e8b Faidon Liambotis
140 138d0e8b Faidon Liambotis
    def info(self, msg):
141 138d0e8b Faidon Liambotis
        self.log.info("[C%d] %s" % (self.id, msg))
142 138d0e8b Faidon Liambotis
143 138d0e8b Faidon Liambotis
    def debug(self, msg):
144 138d0e8b Faidon Liambotis
        self.log.debug("[C%d] %s" % (self.id, msg))
145 138d0e8b Faidon Liambotis
146 138d0e8b Faidon Liambotis
    def warn(self, msg):
147 138d0e8b Faidon Liambotis
        self.log.warn("[C%d] %s" % (self.id, msg))
148 138d0e8b Faidon Liambotis
149 138d0e8b Faidon Liambotis
    def error(self, msg):
150 138d0e8b Faidon Liambotis
        self.log.error("[C%d] %s" % (self.id, msg))
151 138d0e8b Faidon Liambotis
152 138d0e8b Faidon Liambotis
    def critical(self, msg):
153 138d0e8b Faidon Liambotis
        self.log.critical("[C%d] %s" % (self.id, msg))
154 138d0e8b Faidon Liambotis
155 138d0e8b Faidon Liambotis
    def __str__(self):
156 138d0e8b Faidon Liambotis
        return "VncAuthProxy: %d -> %s:%d" % (self.sport, self.daddr, self.dport)
157 138d0e8b Faidon Liambotis
158 138d0e8b Faidon Liambotis
    def _forward(self, source, dest):
159 138d0e8b Faidon Liambotis
        """
160 138d0e8b Faidon Liambotis
        Forward traffic from source to dest
161 138d0e8b Faidon Liambotis

162 138d0e8b Faidon Liambotis
        @type source: socket
163 138d0e8b Faidon Liambotis
        @param source: source socket
164 138d0e8b Faidon Liambotis
        @type dest: socket
165 138d0e8b Faidon Liambotis
        @param dest: destination socket
166 138d0e8b Faidon Liambotis

167 138d0e8b Faidon Liambotis
        """
168 138d0e8b Faidon Liambotis
169 138d0e8b Faidon Liambotis
        while True:
170 138d0e8b Faidon Liambotis
            d = source.recv(16384)
171 138d0e8b Faidon Liambotis
            if d == '':
172 138d0e8b Faidon Liambotis
                if source == self.client:
173 138d0e8b Faidon Liambotis
                    self.info("Client connection closed")
174 138d0e8b Faidon Liambotis
                else:
175 138d0e8b Faidon Liambotis
                    self.info("Server connection closed")
176 138d0e8b Faidon Liambotis
                break
177 138d0e8b Faidon Liambotis
            dest.sendall(d)
178 138d0e8b Faidon Liambotis
        # No need to close the source and dest sockets here.
179 138d0e8b Faidon Liambotis
        # They are owned by and will be closed by the original greenlet.
180 138d0e8b Faidon Liambotis
181 512c571e Stratos Psomadakis
    def _client_handshake(self):
182 138d0e8b Faidon Liambotis
        """
183 138d0e8b Faidon Liambotis
        Perform handshake/authentication with a connecting client
184 138d0e8b Faidon Liambotis

185 138d0e8b Faidon Liambotis
        Outline:
186 138d0e8b Faidon Liambotis
        1. Client connects
187 138d0e8b Faidon Liambotis
        2. We fake RFB 3.8 protocol and require VNC authentication [also supports RFB 3.3]
188 138d0e8b Faidon Liambotis
        3. Client accepts authentication method
189 138d0e8b Faidon Liambotis
        4. We send an authentication challenge
190 138d0e8b Faidon Liambotis
        5. Client sends the authentication response
191 138d0e8b Faidon Liambotis
        6. We check the authentication
192 138d0e8b Faidon Liambotis

193 512c571e Stratos Psomadakis
        Upon return, self.client socket is connected to the client.
194 138d0e8b Faidon Liambotis

195 138d0e8b Faidon Liambotis
        """
196 138d0e8b Faidon Liambotis
        self.client.send(rfb.RFB_VERSION_3_8 + "\n")
197 138d0e8b Faidon Liambotis
        client_version_str = self.client.recv(1024)
198 138d0e8b Faidon Liambotis
        client_version = rfb.check_version(client_version_str)
199 138d0e8b Faidon Liambotis
        if not client_version:
200 138d0e8b Faidon Liambotis
            self.error("Invalid version: %s" % client_version_str)
201 138d0e8b Faidon Liambotis
            raise gevent.GreenletExit
202 138d0e8b Faidon Liambotis
203 138d0e8b Faidon Liambotis
        # Both for RFB 3.3 and 3.8
204 138d0e8b Faidon Liambotis
        self.debug("Requesting authentication")
205 138d0e8b Faidon Liambotis
        auth_request = rfb.make_auth_request(rfb.RFB_AUTHTYPE_VNC,
206 138d0e8b Faidon Liambotis
            version=client_version)
207 138d0e8b Faidon Liambotis
        self.client.send(auth_request)
208 138d0e8b Faidon Liambotis
209 138d0e8b Faidon Liambotis
        # The client gets to propose an authtype only for RFB 3.8
210 138d0e8b Faidon Liambotis
        if client_version == rfb.RFB_VERSION_3_8:
211 138d0e8b Faidon Liambotis
            res = self.client.recv(1024)
212 138d0e8b Faidon Liambotis
            type = rfb.parse_client_authtype(res)
213 138d0e8b Faidon Liambotis
            if type == rfb.RFB_AUTHTYPE_ERROR:
214 138d0e8b Faidon Liambotis
                self.warn("Client refused authentication: %s" % res[1:])
215 138d0e8b Faidon Liambotis
            else:
216 138d0e8b Faidon Liambotis
                self.debug("Client requested authtype %x" % type)
217 138d0e8b Faidon Liambotis
218 138d0e8b Faidon Liambotis
            if type != rfb.RFB_AUTHTYPE_VNC:
219 138d0e8b Faidon Liambotis
                self.error("Wrong auth type: %d" % type)
220 138d0e8b Faidon Liambotis
                self.client.send(rfb.to_u32(rfb.RFB_AUTH_ERROR))
221 138d0e8b Faidon Liambotis
                raise gevent.GreenletExit
222 138d0e8b Faidon Liambotis
        
223 138d0e8b Faidon Liambotis
        # Generate the challenge
224 138d0e8b Faidon Liambotis
        challenge = os.urandom(16)
225 138d0e8b Faidon Liambotis
        self.client.send(challenge)
226 138d0e8b Faidon Liambotis
        response = self.client.recv(1024)
227 138d0e8b Faidon Liambotis
        if len(response) != 16:
228 138d0e8b Faidon Liambotis
            self.error("Wrong response length %d, should be 16" % len(response))
229 138d0e8b Faidon Liambotis
            raise gevent.GreenletExit
230 138d0e8b Faidon Liambotis
231 5a196d84 Vangelis Koukis
        if rfb.check_password(challenge, response, self.password):
232 138d0e8b Faidon Liambotis
            self.debug("Authentication successful!")
233 138d0e8b Faidon Liambotis
        else:
234 138d0e8b Faidon Liambotis
            self.warn("Authentication failed")
235 138d0e8b Faidon Liambotis
            self.client.send(rfb.to_u32(rfb.RFB_AUTH_ERROR))
236 138d0e8b Faidon Liambotis
            raise gevent.GreenletExit
237 138d0e8b Faidon Liambotis
238 138d0e8b Faidon Liambotis
        # Accept the authentication
239 138d0e8b Faidon Liambotis
        self.client.send(rfb.to_u32(rfb.RFB_AUTH_SUCCESS))
240 138d0e8b Faidon Liambotis
       
241 138d0e8b Faidon Liambotis
    def _run(self):
242 138d0e8b Faidon Liambotis
        try:
243 138d0e8b Faidon Liambotis
            self.log.debug("Waiting for client to connect")
244 5a196d84 Vangelis Koukis
            rlist, _, _ = select(self.listeners, [], [], timeout=self.timeout)
245 138d0e8b Faidon Liambotis
246 138d0e8b Faidon Liambotis
            if not rlist:
247 138d0e8b Faidon Liambotis
                self.info("Timed out, no connection after %d sec" % self.timeout)
248 138d0e8b Faidon Liambotis
                raise gevent.GreenletExit
249 138d0e8b Faidon Liambotis
250 138d0e8b Faidon Liambotis
            for sock in rlist:
251 138d0e8b Faidon Liambotis
                self.client, addrinfo = sock.accept()
252 138d0e8b Faidon Liambotis
                self.info("Connection from %s:%d" % addrinfo[:2])
253 138d0e8b Faidon Liambotis
254 138d0e8b Faidon Liambotis
                # Close all listening sockets, we only want a one-shot connection
255 138d0e8b Faidon Liambotis
                # from a single client.
256 138d0e8b Faidon Liambotis
                while self.listeners:
257 138d0e8b Faidon Liambotis
                    self.listeners.pop().close()
258 138d0e8b Faidon Liambotis
                break
259 138d0e8b Faidon Liambotis
       
260 512c571e Stratos Psomadakis
            # Perform RFB handshake with the client.
261 512c571e Stratos Psomadakis
            self._client_handshake()
262 138d0e8b Faidon Liambotis
263 138d0e8b Faidon Liambotis
            # Bridge both connections through two "forwarder" greenlets.
264 138d0e8b Faidon Liambotis
            self.workers = [gevent.spawn(self._forward, self.client, self.server),
265 138d0e8b Faidon Liambotis
                gevent.spawn(self._forward, self.server, self.client)]
266 138d0e8b Faidon Liambotis
            
267 138d0e8b Faidon Liambotis
            # If one greenlet goes, the other has to go too.
268 138d0e8b Faidon Liambotis
            self.workers[0].link(self.workers[1])
269 138d0e8b Faidon Liambotis
            self.workers[1].link(self.workers[0])
270 138d0e8b Faidon Liambotis
            gevent.joinall(self.workers)
271 138d0e8b Faidon Liambotis
            del self.workers
272 138d0e8b Faidon Liambotis
            raise gevent.GreenletExit
273 138d0e8b Faidon Liambotis
        except Exception, e:
274 138d0e8b Faidon Liambotis
            # Any unhandled exception in the previous block
275 138d0e8b Faidon Liambotis
            # is an error and must be logged accordingly
276 138d0e8b Faidon Liambotis
            if not isinstance(e, gevent.GreenletExit):
277 138d0e8b Faidon Liambotis
                self.log.exception(e)
278 138d0e8b Faidon Liambotis
            raise e
279 138d0e8b Faidon Liambotis
        finally:
280 138d0e8b Faidon Liambotis
            self._cleanup()
281 138d0e8b Faidon Liambotis
282 138d0e8b Faidon Liambotis
283 138d0e8b Faidon Liambotis
def fatal_signal_handler(signame):
284 138d0e8b Faidon Liambotis
    logger.info("Caught %s, will raise SystemExit" % signame)
285 138d0e8b Faidon Liambotis
    raise SystemExit
286 138d0e8b Faidon Liambotis
287 138d0e8b Faidon Liambotis
def get_listening_sockets(sport):
288 138d0e8b Faidon Liambotis
    sockets = []
289 138d0e8b Faidon Liambotis
290 138d0e8b Faidon Liambotis
    # Use two sockets, one for IPv4, one for IPv6. IPv4-to-IPv6 mapped
291 138d0e8b Faidon Liambotis
    # addresses do not work reliably everywhere (under linux it may have
292 138d0e8b Faidon Liambotis
    # been disabled in /proc/sys/net/ipv6/bind_ipv6_only).
293 138d0e8b Faidon Liambotis
    for res in socket.getaddrinfo(None, sport, socket.AF_UNSPEC,
294 138d0e8b Faidon Liambotis
                                  socket.SOCK_STREAM, 0, socket.AI_PASSIVE):
295 138d0e8b Faidon Liambotis
        af, socktype, proto, canonname, sa = res
296 138d0e8b Faidon Liambotis
        try:
297 138d0e8b Faidon Liambotis
            s = None
298 138d0e8b Faidon Liambotis
            s = socket.socket(af, socktype, proto)
299 138d0e8b Faidon Liambotis
            if af == socket.AF_INET6:
300 138d0e8b Faidon Liambotis
                # Bind v6 only when AF_INET6, otherwise either v4 or v6 bind
301 138d0e8b Faidon Liambotis
                # will fail.
302 138d0e8b Faidon Liambotis
                s.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, 1)
303 138d0e8b Faidon Liambotis
            s.bind(sa)
304 138d0e8b Faidon Liambotis
            s.listen(1)
305 138d0e8b Faidon Liambotis
            sockets.append(s)
306 138d0e8b Faidon Liambotis
            logger.debug("Listening on %s:%d" % sa[:2])
307 138d0e8b Faidon Liambotis
        except socket.error, msg:
308 138d0e8b Faidon Liambotis
            logger.error("Error binding to %s:%d: %s" %
309 138d0e8b Faidon Liambotis
                           (sa[0], sa[1], msg[1]))
310 138d0e8b Faidon Liambotis
            if s:
311 138d0e8b Faidon Liambotis
                s.close()
312 138d0e8b Faidon Liambotis
            while sockets:
313 138d0e8b Faidon Liambotis
                sockets.pop().close()
314 138d0e8b Faidon Liambotis
            
315 138d0e8b Faidon Liambotis
            # Make sure we fail immediately if we cannot get a socket
316 138d0e8b Faidon Liambotis
            raise msg
317 138d0e8b Faidon Liambotis
    
318 138d0e8b Faidon Liambotis
    return sockets
319 138d0e8b Faidon Liambotis
320 7eb27319 Stratos Psomadakis
def perform_server_handshake(daddr, dport, tries, retry_wait):
321 512c571e Stratos Psomadakis
    """
322 512c571e Stratos Psomadakis
    Initiate a connection with the backend server and perform basic
323 512c571e Stratos Psomadakis
    RFB 3.8 handshake with it.
324 512c571e Stratos Psomadakis

325 512c571e Stratos Psomadakis
    Returns a socket connected to the backend server.
326 512c571e Stratos Psomadakis

327 512c571e Stratos Psomadakis
    """
328 512c571e Stratos Psomadakis
    server = None
329 512c571e Stratos Psomadakis
330 512c571e Stratos Psomadakis
    while tries:
331 512c571e Stratos Psomadakis
        tries -= 1
332 512c571e Stratos Psomadakis
333 512c571e Stratos Psomadakis
        # Initiate server connection
334 512c571e Stratos Psomadakis
        for res in socket.getaddrinfo(daddr, dport, socket.AF_UNSPEC,
335 512c571e Stratos Psomadakis
                                      socket.SOCK_STREAM, 0, socket.AI_PASSIVE):
336 512c571e Stratos Psomadakis
            af, socktype, proto, canonname, sa = res
337 512c571e Stratos Psomadakis
            try:
338 512c571e Stratos Psomadakis
                server = socket.socket(af, socktype, proto)
339 512c571e Stratos Psomadakis
            except socket.error, msg:
340 512c571e Stratos Psomadakis
                server = None
341 512c571e Stratos Psomadakis
                continue
342 512c571e Stratos Psomadakis
343 512c571e Stratos Psomadakis
            try:
344 512c571e Stratos Psomadakis
                logger.debug("Connecting to %s:%s" % sa[:2])
345 512c571e Stratos Psomadakis
                server.connect(sa)
346 512c571e Stratos Psomadakis
                logger.debug("Connection to %s:%s successful" % sa[:2])
347 512c571e Stratos Psomadakis
            except socket.error, msg:
348 512c571e Stratos Psomadakis
                server.close()
349 512c571e Stratos Psomadakis
                server = None
350 512c571e Stratos Psomadakis
                continue
351 512c571e Stratos Psomadakis
352 512c571e Stratos Psomadakis
            # We succesfully connected to the server
353 512c571e Stratos Psomadakis
            tries = 0
354 512c571e Stratos Psomadakis
            break
355 512c571e Stratos Psomadakis
356 512c571e Stratos Psomadakis
        # Wait and retry
357 7eb27319 Stratos Psomadakis
        sleep(retry_wait)
358 512c571e Stratos Psomadakis
359 512c571e Stratos Psomadakis
    if server is None:
360 512c571e Stratos Psomadakis
        raise Exception("Failed to connect to server")
361 512c571e Stratos Psomadakis
362 512c571e Stratos Psomadakis
    version = server.recv(1024)
363 512c571e Stratos Psomadakis
    if not rfb.check_version(version):
364 512c571e Stratos Psomadakis
        raise Exception("Unsupported RFB version: %s" % version.strip())
365 512c571e Stratos Psomadakis
366 512c571e Stratos Psomadakis
    server.send(rfb.RFB_VERSION_3_8 + "\n")
367 512c571e Stratos Psomadakis
368 512c571e Stratos Psomadakis
    res = server.recv(1024)
369 512c571e Stratos Psomadakis
    types = rfb.parse_auth_request(res)
370 512c571e Stratos Psomadakis
    if not types:
371 512c571e Stratos Psomadakis
        raise Exception("Error handshaking with the server")
372 512c571e Stratos Psomadakis
373 512c571e Stratos Psomadakis
    else:
374 512c571e Stratos Psomadakis
        logger.debug("Supported authentication types: %s" %
375 512c571e Stratos Psomadakis
                       " ".join([str(x) for x in types]))
376 512c571e Stratos Psomadakis
377 512c571e Stratos Psomadakis
    if rfb.RFB_AUTHTYPE_NONE not in types:
378 512c571e Stratos Psomadakis
        raise Exception("Error, server demands authentication")
379 512c571e Stratos Psomadakis
380 512c571e Stratos Psomadakis
    server.send(rfb.to_u8(rfb.RFB_AUTHTYPE_NONE))
381 512c571e Stratos Psomadakis
382 512c571e Stratos Psomadakis
    # Check authentication response
383 512c571e Stratos Psomadakis
    res = server.recv(4)
384 512c571e Stratos Psomadakis
    res = rfb.from_u32(res)
385 512c571e Stratos Psomadakis
386 512c571e Stratos Psomadakis
    if res != 0:
387 512c571e Stratos Psomadakis
        raise Exception("Authentication error")
388 512c571e Stratos Psomadakis
389 512c571e Stratos Psomadakis
    return server
390 512c571e Stratos Psomadakis
391 138d0e8b Faidon Liambotis
def parse_arguments(args):
392 138d0e8b Faidon Liambotis
    from optparse import OptionParser
393 138d0e8b Faidon Liambotis
394 138d0e8b Faidon Liambotis
    parser = OptionParser()
395 138d0e8b Faidon Liambotis
    parser.add_option("-s", "--socket", dest="ctrl_socket",
396 138d0e8b Faidon Liambotis
                      default=DEFAULT_CTRL_SOCKET,
397 138d0e8b Faidon Liambotis
                      metavar="PATH",
398 138d0e8b Faidon Liambotis
                      help="UNIX socket path for control connections (default: %s" %
399 138d0e8b Faidon Liambotis
                          DEFAULT_CTRL_SOCKET)
400 138d0e8b Faidon Liambotis
    parser.add_option("-d", "--debug", action="store_true", dest="debug",
401 138d0e8b Faidon Liambotis
                      help="Enable debugging information")
402 138d0e8b Faidon Liambotis
    parser.add_option("-l", "--log", dest="log_file",
403 138d0e8b Faidon Liambotis
                      default=DEFAULT_LOG_FILE,
404 138d0e8b Faidon Liambotis
                      metavar="FILE",
405 138d0e8b Faidon Liambotis
                      help="Write log to FILE instead of %s" % DEFAULT_LOG_FILE),
406 138d0e8b Faidon Liambotis
    parser.add_option('--pid-file', dest="pid_file",
407 138d0e8b Faidon Liambotis
                      default=DEFAULT_PID_FILE,
408 138d0e8b Faidon Liambotis
                      metavar='PIDFILE',
409 138d0e8b Faidon Liambotis
                      help="Save PID to file (default: %s)" %
410 138d0e8b Faidon Liambotis
                          DEFAULT_PID_FILE)
411 138d0e8b Faidon Liambotis
    parser.add_option("-t", "--connect-timeout", dest="connect_timeout",
412 138d0e8b Faidon Liambotis
                      default=DEFAULT_CONNECT_TIMEOUT, type="int", metavar="SECONDS",
413 138d0e8b Faidon Liambotis
                      help="How long to listen for clients to forward")
414 7eb27319 Stratos Psomadakis
    parser.add_option("-r", "--connect-retries", dest="connect_retries",
415 7eb27319 Stratos Psomadakis
                      default=DEFAULT_CONNECT_RETRIES, type="int",
416 7eb27319 Stratos Psomadakis
                      metavar="RETRIES",
417 7eb27319 Stratos Psomadakis
                      help="How many times to try to connect to the server")
418 7eb27319 Stratos Psomadakis
    parser.add_option("-w", "--retry-wait", dest="retry_wait",
419 7eb27319 Stratos Psomadakis
                      default=DEFAULT_RETRY_WAIT, type="float", metavar="SECONDS",
420 7eb27319 Stratos Psomadakis
                      help="How long to wait between retrying to connect to the server")
421 138d0e8b Faidon Liambotis
    parser.add_option("-p", "--min-port", dest="min_port",
422 138d0e8b Faidon Liambotis
                      default=DEFAULT_MIN_PORT, type="int", metavar="MIN_PORT",
423 138d0e8b Faidon Liambotis
                      help="The minimum port to use for automatically-allocated ephemeral ports")
424 138d0e8b Faidon Liambotis
    parser.add_option("-P", "--max-port", dest="max_port",
425 138d0e8b Faidon Liambotis
                      default=DEFAULT_MAX_PORT, type="int", metavar="MAX_PORT",
426 138d0e8b Faidon Liambotis
                      help="The minimum port to use for automatically-allocated ephemeral ports")
427 138d0e8b Faidon Liambotis
428 138d0e8b Faidon Liambotis
    return parser.parse_args(args)
429 138d0e8b Faidon Liambotis
430 138d0e8b Faidon Liambotis
431 138d0e8b Faidon Liambotis
def main():
432 138d0e8b Faidon Liambotis
    """Run the daemon from the command line."""
433 138d0e8b Faidon Liambotis
434 138d0e8b Faidon Liambotis
    (opts, args) = parse_arguments(sys.argv[1:])
435 138d0e8b Faidon Liambotis
436 138d0e8b Faidon Liambotis
    # Create pidfile
437 138d0e8b Faidon Liambotis
    pidf = daemon.pidlockfile.TimeoutPIDLockFile(
438 138d0e8b Faidon Liambotis
        opts.pid_file, 10)
439 138d0e8b Faidon Liambotis
    
440 138d0e8b Faidon Liambotis
    # Initialize logger
441 138d0e8b Faidon Liambotis
    lvl = logging.DEBUG if opts.debug else logging.INFO
442 88420a63 Faidon Liambotis
443 88420a63 Faidon Liambotis
    global logger
444 138d0e8b Faidon Liambotis
    logger = logging.getLogger("vncauthproxy")
445 138d0e8b Faidon Liambotis
    logger.setLevel(lvl)
446 138d0e8b Faidon Liambotis
    formatter = logging.Formatter("%(asctime)s %(module)s[%(process)d] %(levelname)s: %(message)s",
447 138d0e8b Faidon Liambotis
        "%Y-%m-%d %H:%M:%S")
448 138d0e8b Faidon Liambotis
    handler = logging.FileHandler(opts.log_file)
449 138d0e8b Faidon Liambotis
    handler.setFormatter(formatter)
450 138d0e8b Faidon Liambotis
    logger.addHandler(handler)
451 138d0e8b Faidon Liambotis
452 138d0e8b Faidon Liambotis
    # Become a daemon:
453 138d0e8b Faidon Liambotis
    # Redirect stdout and stderr to handler.stream to catch
454 138d0e8b Faidon Liambotis
    # early errors in the daemonization process [e.g., pidfile creation]
455 138d0e8b Faidon Liambotis
    # which will otherwise go to /dev/null.
456 376a8634 Vangelis Koukis
    daemon_context = AllFilesDaemonContext(
457 138d0e8b Faidon Liambotis
        pidfile=pidf,
458 138d0e8b Faidon Liambotis
        umask=0022,
459 138d0e8b Faidon Liambotis
        stdout=handler.stream,
460 138d0e8b Faidon Liambotis
        stderr=handler.stream,
461 138d0e8b Faidon Liambotis
        files_preserve=[handler.stream])
462 138d0e8b Faidon Liambotis
    daemon_context.open()
463 138d0e8b Faidon Liambotis
    logger.info("Became a daemon")
464 138d0e8b Faidon Liambotis
465 138d0e8b Faidon Liambotis
    # A fork() has occured while daemonizing,
466 138d0e8b Faidon Liambotis
    # we *must* reinit gevent
467 138d0e8b Faidon Liambotis
    gevent.reinit()
468 138d0e8b Faidon Liambotis
469 138d0e8b Faidon Liambotis
    if os.path.exists(opts.ctrl_socket):
470 138d0e8b Faidon Liambotis
        logger.critical("Socket '%s' already exists" % opts.ctrl_socket)
471 138d0e8b Faidon Liambotis
        sys.exit(1)
472 138d0e8b Faidon Liambotis
473 138d0e8b Faidon Liambotis
    # TODO: make this tunable? chgrp as well?
474 1c241b27 Faidon Liambotis
    old_umask = os.umask(0007)
475 138d0e8b Faidon Liambotis
476 138d0e8b Faidon Liambotis
    ctrl = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
477 138d0e8b Faidon Liambotis
    ctrl.bind(opts.ctrl_socket)
478 138d0e8b Faidon Liambotis
479 138d0e8b Faidon Liambotis
    os.umask(old_umask)
480 138d0e8b Faidon Liambotis
481 138d0e8b Faidon Liambotis
    ctrl.listen(1)
482 138d0e8b Faidon Liambotis
    logger.info("Initialized, waiting for control connections at %s" %
483 138d0e8b Faidon Liambotis
                 opts.ctrl_socket)
484 138d0e8b Faidon Liambotis
485 138d0e8b Faidon Liambotis
    # Catch signals to ensure graceful shutdown,
486 138d0e8b Faidon Liambotis
    # e.g., to make sure the control socket gets unlink()ed.
487 138d0e8b Faidon Liambotis
    #
488 138d0e8b Faidon Liambotis
    # Uses gevent.signal so the handler fires even during
489 138d0e8b Faidon Liambotis
    # gevent.socket.accept()
490 138d0e8b Faidon Liambotis
    gevent.signal(SIGINT, fatal_signal_handler, "SIGINT")
491 138d0e8b Faidon Liambotis
    gevent.signal(SIGTERM, fatal_signal_handler, "SIGTERM")
492 138d0e8b Faidon Liambotis
493 138d0e8b Faidon Liambotis
    # Init ephemeral port pool
494 138d0e8b Faidon Liambotis
    ports = range(opts.min_port, opts.max_port + 1) 
495 138d0e8b Faidon Liambotis
496 138d0e8b Faidon Liambotis
    while True:
497 138d0e8b Faidon Liambotis
        try:
498 138d0e8b Faidon Liambotis
            client, addr = ctrl.accept()
499 138d0e8b Faidon Liambotis
            logger.info("New control connection")
500 138d0e8b Faidon Liambotis
           
501 138d0e8b Faidon Liambotis
            # Receive and parse a client request.
502 138d0e8b Faidon Liambotis
            response = {
503 138d0e8b Faidon Liambotis
                "source_port": 0,
504 138d0e8b Faidon Liambotis
                "status": "FAILED"
505 138d0e8b Faidon Liambotis
            }
506 138d0e8b Faidon Liambotis
            try:
507 138d0e8b Faidon Liambotis
                # TODO: support multiple forwardings in the same message?
508 138d0e8b Faidon Liambotis
                # 
509 138d0e8b Faidon Liambotis
                # Control request, in JSON:
510 138d0e8b Faidon Liambotis
                #
511 138d0e8b Faidon Liambotis
                # {
512 138d0e8b Faidon Liambotis
                #     "source_port": <source port or 0 for automatic allocation>,
513 138d0e8b Faidon Liambotis
                #     "destination_address": <destination address of backend server>,
514 138d0e8b Faidon Liambotis
                #     "destination_port": <destination port>
515 138d0e8b Faidon Liambotis
                #     "password": <the password to use for MITM authentication of clients>
516 138d0e8b Faidon Liambotis
                # }
517 138d0e8b Faidon Liambotis
                # 
518 138d0e8b Faidon Liambotis
                # The <password> is used for MITM authentication of clients
519 138d0e8b Faidon Liambotis
                # connecting to <source_port>, who will subsequently be forwarded
520 138d0e8b Faidon Liambotis
                # to a VNC server at <destination_address>:<destination_port>
521 138d0e8b Faidon Liambotis
                #
522 138d0e8b Faidon Liambotis
                # Control reply, in JSON:
523 138d0e8b Faidon Liambotis
                # {
524 138d0e8b Faidon Liambotis
                #     "source_port": <the allocated source port>
525 138d0e8b Faidon Liambotis
                #     "status": <one of "OK" or "FAILED">
526 138d0e8b Faidon Liambotis
                # }
527 138d0e8b Faidon Liambotis
                buf = client.recv(1024)
528 138d0e8b Faidon Liambotis
                req = json.loads(buf)
529 138d0e8b Faidon Liambotis
                
530 138d0e8b Faidon Liambotis
                sport_orig = int(req['source_port'])
531 138d0e8b Faidon Liambotis
                daddr = req['destination_address']
532 138d0e8b Faidon Liambotis
                dport = int(req['destination_port'])
533 138d0e8b Faidon Liambotis
                password = req['password']
534 138d0e8b Faidon Liambotis
            except Exception, e:
535 138d0e8b Faidon Liambotis
                logger.warn("Malformed request: %s" % buf)
536 138d0e8b Faidon Liambotis
                client.send(json.dumps(response))
537 138d0e8b Faidon Liambotis
                client.close()
538 138d0e8b Faidon Liambotis
                continue
539 138d0e8b Faidon Liambotis
            
540 138d0e8b Faidon Liambotis
            # Spawn a new Greenlet to service the request.
541 512c571e Stratos Psomadakis
            server = None
542 138d0e8b Faidon Liambotis
            try:
543 138d0e8b Faidon Liambotis
                # If the client has so indicated, pick an ephemeral source port
544 138d0e8b Faidon Liambotis
                # randomly, and remove it from the port pool.
545 138d0e8b Faidon Liambotis
                if sport_orig == 0:
546 138d0e8b Faidon Liambotis
                    sport = random.choice(ports)
547 138d0e8b Faidon Liambotis
                    ports.remove(sport)
548 138d0e8b Faidon Liambotis
                    logger.debug("Got port %d from port pool, contains %d ports",
549 138d0e8b Faidon Liambotis
                        sport, len(ports))
550 138d0e8b Faidon Liambotis
                    pool = ports
551 138d0e8b Faidon Liambotis
                else:
552 138d0e8b Faidon Liambotis
                    sport = sport_orig
553 138d0e8b Faidon Liambotis
                    pool = None
554 512c571e Stratos Psomadakis
555 138d0e8b Faidon Liambotis
                listeners = get_listening_sockets(sport)
556 7eb27319 Stratos Psomadakis
                server = perform_server_handshake(daddr, dport,
557 7eb27319 Stratos Psomadakis
                    opts.connect_retries, opts.retry_wait)
558 512c571e Stratos Psomadakis
559 138d0e8b Faidon Liambotis
                VncAuthProxy.spawn(logger, listeners, pool, daddr, dport,
560 512c571e Stratos Psomadakis
                    server, password, opts.connect_timeout)
561 512c571e Stratos Psomadakis
562 138d0e8b Faidon Liambotis
                logger.info("New forwarding [%d (req'd by client: %d) -> %s:%d]" %
563 138d0e8b Faidon Liambotis
                    (sport, sport_orig, daddr, dport))
564 138d0e8b Faidon Liambotis
                response = {
565 138d0e8b Faidon Liambotis
                    "source_port": sport,
566 138d0e8b Faidon Liambotis
                    "status": "OK"
567 138d0e8b Faidon Liambotis
                }
568 138d0e8b Faidon Liambotis
            except IndexError:
569 138d0e8b Faidon Liambotis
                logger.error("FAILED forwarding, out of ports for [req'd by "
570 138d0e8b Faidon Liambotis
                    "client: %d -> %s:%d]" % (sport_orig, daddr, dport))
571 512c571e Stratos Psomadakis
            except Exception, msg:
572 512c571e Stratos Psomadakis
                logger.error(msg)
573 138d0e8b Faidon Liambotis
                logger.error("FAILED forwarding [%d (req'd by client: %d) -> %s:%d]" %
574 138d0e8b Faidon Liambotis
                    (sport, sport_orig, daddr, dport))
575 138d0e8b Faidon Liambotis
                if not pool is None:
576 138d0e8b Faidon Liambotis
                    pool.append(sport)
577 138d0e8b Faidon Liambotis
                    logger.debug("Returned port %d to port pool, contains %d ports",
578 138d0e8b Faidon Liambotis
                        sport, len(pool))
579 512c571e Stratos Psomadakis
                if not server is None:
580 512c571e Stratos Psomadakis
                    server.close()
581 138d0e8b Faidon Liambotis
            finally:
582 138d0e8b Faidon Liambotis
                client.send(json.dumps(response))
583 138d0e8b Faidon Liambotis
                client.close()
584 138d0e8b Faidon Liambotis
        except Exception, e:
585 138d0e8b Faidon Liambotis
            logger.exception(e)
586 138d0e8b Faidon Liambotis
            continue
587 138d0e8b Faidon Liambotis
        except SystemExit:
588 138d0e8b Faidon Liambotis
            break
589 138d0e8b Faidon Liambotis
 
590 138d0e8b Faidon Liambotis
    logger.info("Unlinking control socket at %s" %
591 138d0e8b Faidon Liambotis
                 opts.ctrl_socket)
592 138d0e8b Faidon Liambotis
    os.unlink(opts.ctrl_socket)
593 138d0e8b Faidon Liambotis
    daemon_context.close()
594 138d0e8b Faidon Liambotis
    sys.exit(0)