1# This file is part of Xpra.
2# Copyright (C) 2012-2020 Antoine Martin <antoine@xpra.org>
3# Copyright (C) 2010 Nathaniel Smith <njs@pobox.com>
4# Xpra is released under the terms of the GNU GPL v2, or, at your option, any
5# later version. See the file COPYING for details.
6
7import signal
8import threading
9
10from xpra.net.bytestreams import untilConcludes
11from xpra.util import repr_ellipsized, envint, envbool
12from xpra.os_util import hexstr, force_quit, POSIX
13from xpra.log import Logger
14
15log = Logger("proxy")
16
17SHOW_DATA = envbool("XPRA_PROXY_SHOW_DATA")
18PROXY_BUFFER_SIZE = envint("XPRA_PROXY_BUFFER_SIZE", 65536)
19
20
21def noretry(_e):
22    return False
23
24class XpraProxy:
25    """
26        This is the proxy command that runs
27        when one uses the hidden subcommand
28        "xpra _proxy" or when forwarding data
29        using the tcp-proxy option.
30        It simply forwards stdin/stdout to
31        the server socket.
32    """
33
34    def __repr__(self):
35        return "XpraProxy(%s: %s - %s)" % (self._name, self._client_conn, self._server_conn)
36
37    def __init__(self, name, client_conn, server_conn, quit_cb=None):
38        self._name = name
39        self._client_conn = client_conn
40        self._server_conn = server_conn
41        self._quit_cb = quit_cb or self.do_quit
42        self._closed = False
43        self._to_client = threading.Thread(target=self._to_client_loop)
44        self._to_client.setDaemon(True)
45        self._to_server = threading.Thread(target=self._to_server_loop)
46        self._to_server.setDaemon(True)
47        self._exit_code = 0
48        signal.signal(signal.SIGINT, self.signal_quit)
49        signal.signal(signal.SIGTERM, self.signal_quit)
50        if POSIX:
51            signal.signal(signal.SIGHUP, self.signal_quit)
52            signal.signal(signal.SIGPIPE, self.signal_quit)
53
54    def start_threads(self):
55        self._to_client.start()
56        self._to_server.start()
57
58    def run(self):
59        log("XpraProxy.run() %s", self._name)
60        self.start_threads()
61        self._to_client.join()
62        self._to_server.join()
63        log("XpraProxy.run() %s: all the threads have ended, calling quit() to close the connections", self._name)
64        self.quit()
65
66    def _to_client_loop(self):
67        self._copy_loop("<-server %s" % self._name, self._server_conn, self._client_conn)
68        self._closed = True
69
70    def _to_server_loop(self):
71        self._copy_loop("->server %s" % self._name, self._client_conn, self._server_conn)
72        self._closed = True
73
74    def _copy_loop(self, log_name, from_conn, to_conn):
75        #log("XpraProxy._copy_loop(%s, %s, %s)", log_name, from_conn, to_conn)
76        try:
77            while not self._closed:
78                log("%s: waiting for data", log_name)
79                buf = untilConcludes(self.is_active, noretry, from_conn.read, PROXY_BUFFER_SIZE)
80                if not buf:
81                    log("%s: connection lost", log_name)
82                    return
83                if SHOW_DATA:
84                    log("%s: %s bytes: %s", log_name, len(buf), repr_ellipsized(buf))
85                    log("%s:           %s", log_name, repr_ellipsized(hexstr(buf)))
86                while buf and not self._closed:
87                    log("%s: writing %s bytes", log_name, len(buf))
88                    written = untilConcludes(self.is_active, noretry, to_conn.write, buf)
89                    buf = buf[written:]
90                    log("%s: written %s bytes", log_name, written)
91            log("%s copy loop ended", log_name)
92        except Exception:
93            log("%s", log_name, exc_info=True)
94        finally:
95            self.quit()
96
97    def is_active(self):
98        return not self._closed
99
100    def signal_quit(self, signum, _frame=None):
101        self._exit_code = 128+signum
102        self.quit()
103
104    def quit(self, *args):
105        log("XpraProxy.quit(%s) %s: closing connections", args,  self._name)
106        self._closed = True
107        quit_cb = self._quit_cb
108        try:
109            self._client_conn.close()
110        except OSError:
111            pass
112        try:
113            self._server_conn.close()
114        except OSError:
115            pass
116        if quit_cb:
117            self._quit_cb = None
118            quit_cb(self)
119
120    def do_quit(self, _proxy):
121        force_quit(self._exit_code)
122