Coverage for /home/antoine/projects/xpra-git/dist/python3/lib64/python/xpra/client/mixins/network_listener.py : 41%
Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1# This file is part of Xpra.
2# Copyright (C) 2020 Antoine Martin <antoine@xpra.org>
3# Xpra is released under the terms of the GNU GPL v2, or, at your option, any
4# later version. See the file COPYING for details.
5#pylint: disable-msg=E1101
7import os
8import sys
10from xpra import __version__ as VERSION
11from xpra.util import envint, envfloat, typedict, DETACH_REQUEST, PROTOCOL_ERROR
12from xpra.os_util import bytestostr, get_machine_id
13from xpra.net.bytestreams import log_new_connection
14from xpra.net.socket_util import create_sockets, add_listen_socket, accept_connection, setup_local_sockets
15from xpra.net.net_util import get_network_caps
16from xpra.net.protocol import Protocol
17from xpra.exit_codes import EXIT_OK, EXIT_FAILURE
18from xpra.client.mixins.stub_client_mixin import StubClientMixin
19from xpra.scripts.config import InitException
20from xpra.log import Logger
22log = Logger("network")
24SOCKET_TIMEOUT = envfloat("XPRA_CLIENT_SOCKET_TIMEOUT", "0.1")
25MAX_CONCURRENT_CONNECTIONS = envint("XPRA_MAX_CONCURRENT_CONNECTIONS", 5)
26REQUEST_TIMEOUT = envint("XPRA_CLIENT_REQUEST_TIMEOUT", 10)
29"""
30Mixin for adding listening sockets to the client,
31those can be used for
32- requesting disconnection
33- info request
34"""
35class NetworkListener(StubClientMixin):
37 def __init__(self):
38 self.sockets = {}
39 self.socket_info = {}
40 self.socket_options = {}
41 self.socket_cleanup = []
42 self._potential_protocols = []
43 self._close_timers = {}
46 def init(self, opts):
47 def err(msg):
48 raise InitException(msg)
49 #don't listen on udp sockets for now:
50 opts.bind_udp = ()
51 self.sockets = create_sockets(opts, err)
52 if opts.bind:
53 local_sockets = setup_local_sockets(opts.bind,
54 None, opts.client_socket_dirs,
55 str(os.getpid()), True,
56 opts.mmap_group, opts.socket_permissions)
57 self.sockets.update(local_sockets)
59 def run(self):
60 self.start_listen_sockets()
62 def cleanup(self):
63 self.cleanup_sockets()
66 def cleanup_sockets(self):
67 ct = dict(self._close_timers)
68 self._close_timers = {}
69 for proto, tid in ct.items():
70 self.source_remove(tid)
71 proto.close()
72 socket_cleanup = self.socket_cleanup
73 log("cleanup_sockets() socket_cleanup=%s", socket_cleanup)
74 self.socket_cleanup = []
75 for c in socket_cleanup:
76 try:
77 c()
78 except Exception:
79 log.error("Error during socket listener cleanup", exc_info=True)
80 sockets = self.sockets
81 self.sockets = {}
82 log("cleanup_sockets() sockets=%s", sockets)
83 for sdef in sockets.keys():
84 c = sdef[-1]
85 try:
86 c()
87 except Exception:
88 log.error("Error during socket cleanup", exc_info=True)
91 def start_listen_sockets(self):
92 for sock_def, options in self.sockets.items():
93 socktype, sock, info, _ = sock_def
94 log("start_listen_sockets() will add %s socket %s (%s)", socktype, sock, info)
95 self.socket_info[sock] = info
96 self.socket_options[sock] = options
97 self.idle_add(self.add_listen_socket, socktype, sock, options)
99 def add_listen_socket(self, socktype, sock, options):
100 info = self.socket_info.get(sock)
101 log("add_listen_socket(%s, %s, %s) info=%s", socktype, sock, options, info)
102 cleanup = add_listen_socket(socktype, sock, info, self._new_connection, None, options)
103 if cleanup:
104 self.socket_cleanup.append(cleanup)
106 def _new_connection(self, socktype, listener, handle=0):
107 """
108 Accept the new connection,
109 verify that there aren't too many,
110 start a thread to dispatch it to the correct handler.
111 """
112 log("_new_connection%s", (listener, socktype, handle))
113 if self.exit_code is not None:
114 log("ignoring new connection during shutdown")
115 return False
116 try:
117 self.handle_new_connection(socktype, listener, handle)
118 except Exception:
119 log.error("Error handling new connection", exc_info=True)
120 return self.exit_code is None
122 def handle_new_connection(self, socktype, listener, handle):
123 assert socktype, "cannot find socket type for %s" % listener
124 socket_options = self.socket_options.get(listener, {})
125 if socktype=="named-pipe":
126 from xpra.platform.win32.namedpipes.connection import NamedPipeConnection
127 conn = NamedPipeConnection(listener.pipe_name, handle, socket_options)
128 log.info("New %s connection received on %s", socktype, conn.target)
129 self.make_protocol(socktype, conn, listener)
130 return
131 conn = accept_connection(socktype, listener, SOCKET_TIMEOUT, socket_options)
132 if conn is None:
133 return
134 #limit number of concurrent network connections:
135 if len(self._potential_protocols)>=MAX_CONCURRENT_CONNECTIONS:
136 log.error("Error: too many connections (%i)", len(self._potential_protocols))
137 log.error(" ignoring new one: %s", conn.endpoint or conn)
138 conn.close()
139 return
140 try:
141 sockname = conn._socket.getsockname()
142 except Exception:
143 sockname = ""
144 log("handle_new_connection%s sockname=%s",
145 (socktype, listener, handle), sockname)
146 socket_info = self.socket_info.get(listener)
147 log_new_connection(conn, socket_info)
148 self.make_protocol(socktype, conn, listener)
150 def make_protocol(self, socktype, conn, listener):
151 socktype = socktype.lower()
152 protocol = Protocol(self, conn, self.process_network_packet)
153 #protocol.large_packets.append(b"info-response")
154 protocol.socket_type = socktype
155 self._potential_protocols.append(protocol)
156 protocol.authenticators = ()
157 protocol.start()
158 #self.schedule_verify_connection_accepted(protocol, self._accept_timeout)
160 def process_network_packet(self, proto, packet):
161 log("process_network_packet: %s", packet)
162 packet_type = bytestostr(packet[0])
163 def close():
164 t = self._close_timers.pop(proto, None)
165 if t:
166 proto.close()
167 try:
168 self._potential_protocols.remove(proto)
169 except ValueError:
170 pass
171 def hello_reply(data):
172 proto.send_now(["hello", data])
173 if packet_type=="hello":
174 caps = typedict(packet[1])
175 proto.parse_remote_caps(caps)
176 proto.enable_compressor_from_caps(caps)
177 proto.enable_encoder_from_caps(caps)
178 request = caps.strget("request")
179 if request=="info":
180 def send_info():
181 info = self.get_info()
182 info["network"] = get_network_caps()
183 hello_reply(info)
184 #run in UI thread:
185 self.idle_add(send_info)
186 elif request=="id":
187 hello_reply(self.get_id_info())
188 elif request=="detach":
189 def protocol_closed():
190 self.disconnect_and_quit(EXIT_OK, "network request")
191 proto.send_disconnect([DETACH_REQUEST], done_callback=protocol_closed)
192 return
193 elif request=="version":
194 hello_reply({"version" : VERSION})
195 elif request=="command":
196 command = caps.strtupleget("command_request")
197 log("command request: %s", command)
198 def process_control():
199 try:
200 self._process_control(["control"]+list(command))
201 code = EXIT_OK
202 response = "done"
203 except Exception as e:
204 code = EXIT_FAILURE
205 response = str(e)
206 hello_reply({"command_response" : (code, response)})
207 self.idle_add(process_control)
208 else:
209 log.info("request '%s' is not handled by this client", request)
210 proto.send_disconnect([PROTOCOL_ERROR])
211 elif packet_type in (Protocol.CONNECTION_LOST, Protocol.GIBBERISH):
212 close()
213 return
214 else:
215 log.info("packet '%s' is not handled by this client", packet_type)
216 proto.send_disconnect([PROTOCOL_ERROR])
217 #make sure the connection is closed:
218 tid = self.timeout_add(REQUEST_TIMEOUT*1000, close)
219 self._close_timers[proto] = tid
221 def get_id_info(self) -> dict:
222 #minimal information for identifying the session
223 return {
224 "session-type" : "client",
225 "session-name" : self.session_name,
226 "platform" : sys.platform,
227 "pid" : os.getpid(),
228 "machine-id" : get_machine_id(),
229 }