Coverage for /home/antoine/projects/xpra-git/dist/python3/lib64/python/xpra/net/bytestreams.py : 62%
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) 2011-2020 Antoine Martin <antoine@xpra.org>
3# Copyright (C) 2008, 2009, 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.
7import sys
8import os
9import errno
10import socket
12from xpra.net.common import ConnectionClosedException, IP_SOCKTYPES, TCP_SOCKTYPES
13from xpra.util import envint, envbool, csv
14from xpra.os_util import POSIX, LINUX, WIN32, OSX
15from xpra.platform.features import TCP_OPTIONS, IP_OPTIONS, SOCKET_OPTIONS
16from xpra.log import Logger
18log = Logger("network", "protocol")
20SOCKET_CORK = envbool("XPRA_SOCKET_CORK", LINUX)
21if SOCKET_CORK:
22 try:
23 assert socket.TCP_CORK>0 #@UndefinedVariable
24 except (AttributeError, AssertionError) as cork_e:
25 log.warn("Warning: unable to use TCP_CORK on %s", sys.platform)
26 log.warn(" %s", cork_e)
27 SOCKET_CORK = False
28SOCKET_NODELAY = envbool("XPRA_SOCKET_NODELAY", None)
29SOCKET_KEEPALIVE = envbool("XPRA_SOCKET_KEEPALIVE", True)
30VSOCK_TIMEOUT = envint("XPRA_VSOCK_TIMEOUT", 5)
31SOCKET_TIMEOUT = envint("XPRA_SOCKET_TIMEOUT", 20)
32#this is more proper but would break the proxy server:
33SOCKET_SHUTDOWN = envbool("XPRA_SOCKET_SHUTDOWN", False)
34LOG_TIMEOUTS = envint("XPRA_LOG_TIMEOUTS", 1)
36ABORT = {
37 errno.ENXIO : "ENXIO",
38 errno.ECONNRESET : "ECONNRESET",
39 errno.EPIPE : "EPIPE",
40 }
43PROTOCOL_STR = {}
44FAMILY_STR = {}
45for x in dir(socket):
46 if x.startswith("AF_"):
47 PROTOCOL_STR[getattr(socket, x)] = x
48 if x.startswith("SOCK_"):
49 FAMILY_STR[getattr(socket, x)] = x
52CAN_RETRY_EXCEPTIONS = ()
53CLOSED_EXCEPTIONS = ()
55def can_retry(e):
56 if isinstance(e, socket.timeout):
57 return "socket.timeout"
58 if isinstance(e, BrokenPipeError):
59 raise ConnectionClosedException(e) from None
60 if isinstance(e, OSError):
61 if isinstance(e, CAN_RETRY_EXCEPTIONS):
62 return str(e)
64 code = e.args[0]
65 abort = ABORT.get(code, code)
66 if abort is not None:
67 err = getattr(e, "errno", None)
68 log("can_retry: %s, args=%s, errno=%s, code=%s, abort=%s", type(e), e.args, err, code, abort)
69 raise ConnectionClosedException(e) from None
70 if isinstance(e, CLOSED_EXCEPTIONS):
71 raise ConnectionClosedException(e) from None
72 return False
74def untilConcludes(is_active_cb, can_retry_cb, f, *a, **kw):
75 while is_active_cb():
76 try:
77 return f(*a, **kw)
78 except Exception as e:
79 retry = can_retry_cb(e)
80 if LOG_TIMEOUTS>0:
81 log("untilConcludes(%s, %s, %s, %s, %s) %s, retry=%s",
82 is_active_cb, can_retry_cb, f, a, kw, e, retry, exc_info=LOG_TIMEOUTS>=2)
83 e = None
84 if not retry:
85 raise
88def pretty_socket(s):
89 try:
90 if isinstance(s, str):
91 return s
92 if len(s)==2:
93 if s[0].find(":")>=0:
94 #IPv6
95 return "[%s]:%s" % (s[0], s[1])
96 return "%s:%s" % (s[0], s[1])
97 if len(s)==4:
98 return csv(str(x) for x in s)
99 except (ValueError, TypeError):
100 pass
101 return str(s)
104class Connection:
105 def __init__(self, endpoint, socktype, info=None, options=None):
106 log("Connection%s", (endpoint, socktype, info, options))
107 self.endpoint = endpoint
108 try:
109 assert isinstance(endpoint, (tuple, list))
110 self.target = ":".join(str(x) for x in endpoint)
111 except Exception:
112 self.target = str(endpoint)
113 self.socktype_wrapped = socktype
114 self.socktype = socktype
115 self.info = info or {}
116 self.options = options or {}
117 self.input_bytecount = 0
118 self.input_readcount = 0
119 self.output_bytecount = 0
120 self.output_writecount = 0
121 self.filename = None #only used for unix domain sockets!
122 self.active = True
123 self.timeout = 0
125 def set_nodelay(self, nodelay : bool):
126 pass
128 def set_cork(self, cork : bool):
129 pass
131 def is_active(self) -> bool:
132 return self.active
134 def set_active(self, active : bool):
135 self.active = active
137 def close(self):
138 self.set_active(False)
140 def can_retry(self, e) -> bool:
141 return can_retry(e)
143 def untilConcludes(self, *args):
144 return untilConcludes(self.is_active, self.can_retry, *args)
146 def peek(self, _n : int):
147 #not implemented
148 return b""
150 def _write(self, *args):
151 """ wraps do_write with packet accounting """
152 w = self.untilConcludes(*args)
153 self.output_bytecount += w or 0
154 self.output_writecount += int(w is not None)
155 return w
157 def _read(self, *args):
158 """ wraps do_read with packet accounting """
159 r = self.untilConcludes(*args)
160 self.input_bytecount += len(r or "")
161 self.input_readcount += 1
162 return r
164 def get_info(self) -> dict:
165 info = self.info.copy()
166 if self.socktype_wrapped!=self.socktype:
167 info["wrapped"] = self.socktype_wrapped
168 info.update({
169 "type" : self.socktype or "",
170 "endpoint" : self.endpoint or (),
171 "active" : self.active,
172 "input" : {
173 "bytecount" : self.input_bytecount,
174 "readcount" : self.input_readcount,
175 },
176 "output" : {
177 "bytecount" : self.output_bytecount,
178 "writecount" : self.output_writecount,
179 },
180 })
181 return info
184# A simple, portable abstraction for a blocking, low-level
185# (os.read/os.write-style interface) two-way byte stream:
186# client.py relies on self.filename to locate the unix domain
187# socket (if it exists)
188class TwoFileConnection(Connection):
189 def __init__(self, writeable, readable, abort_test=None, target=None, socktype="", close_cb=None, info=None):
190 super().__init__(target, socktype, info)
191 self._writeable = writeable
192 self._readable = readable
193 self._read_fd = self._readable.fileno()
194 self._write_fd = self._writeable.fileno()
195 self._abort_test = abort_test
196 self._close_cb = close_cb
198 def may_abort(self, action):
199 """ if abort_test is defined, run it """
200 if self._abort_test:
201 self._abort_test(action)
203 def flush(self):
204 r = self._readable
205 if r:
206 r.flush()
207 w = self._writeable
208 if w:
209 w.flush()
211 def read(self, n):
212 self.may_abort("read")
213 return self._read(os.read, self._read_fd, n)
215 def write(self, buf):
216 self.may_abort("write")
217 return self._write(os.write, self._write_fd, buf)
219 def close(self):
220 log("%s.close() close callback=%s, readable=%s, writeable=%s",
221 self, self._close_cb, self._readable, self._writeable)
222 Connection.close(self)
223 cc = self._close_cb
224 if cc:
225 self._close_cb = None
226 log("%s.close() calling %s", self, cc)
227 cc()
228 try:
229 self._readable.close()
230 except IOError as e:
231 log("%s.close() %s", self._readable, e)
232 try:
233 self._writeable.close()
234 except IOError as e:
235 log("%s.close() %s", self._writeable, e)
236 log("%s.close() done", self)
238 def __repr__(self):
239 return "Pipe(%s)" % str(self.target)
241 def get_info(self) -> dict:
242 d = Connection.get_info(self)
243 d.update({
244 "type" : "pipe",
245 "pipe" : {
246 "read" : {"fd" : self._read_fd},
247 "write" : {"fd" : self._write_fd},
248 }
249 })
250 return d
254class SocketConnection(Connection):
255 def __init__(self, sock, local, remote, target, socktype, info=None, socket_options=None):
256 log("SocketConnection%s", (sock, local, remote, target, socktype, info, socket_options))
257 super().__init__(target, socktype, info, socket_options)
258 self._socket = sock
259 self.local = local
260 self.remote = remote
261 self.protocol_type = "socket"
262 if self.socktype_wrapped in TCP_SOCKTYPES:
263 def boolget(k, default_value):
264 v = self.options.get(k)
265 if v is None:
266 return default_value
267 try:
268 return bool(int(v))
269 except ValueError:
270 return default_value
271 self.cork = boolget("cork", SOCKET_CORK)
272 self.nodelay = boolget("nodelay", SOCKET_NODELAY)
273 log("%s options: cork=%s, nodelay=%s", self.socktype_wrapped, self.cork, self.nodelay)
274 if self.nodelay:
275 self.do_set_nodelay(self.nodelay)
276 keepalive = boolget("keepalive", SOCKET_KEEPALIVE)
277 try:
278 self._setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, int(keepalive))
279 if keepalive:
280 idletime = 10
281 interval = 3
282 kmax = 5
283 if WIN32:
284 sock = self.get_raw_socket()
285 sock.ioctl(socket.SIO_KEEPALIVE_VALS, (1, idletime*1000, interval*1000)) #@UndefinedVariable pylint: disable=no-member
286 elif OSX:
287 TCP_KEEPALIVE = 0x10
288 self._setsockopt(socket.IPPROTO_TCP, TCP_KEEPALIVE, interval)
289 elif LINUX:
290 self._setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPIDLE, idletime)
291 self._setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPINTVL, interval)
292 self._setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPCNT, kmax)
293 except OSError:
294 log("cannot set KEEPALIVE", exc_info=True)
295 else:
296 self.cork = False
297 self.nodelay = False
298 self.nodelay_value = None
299 self.cork_value = None
300 if isinstance(remote, str):
301 self.filename = remote
303 def get_raw_socket(self):
304 return self._socket
306 def _setsockopt(self, *args):
307 if self.active:
308 sock = self.get_raw_socket()
309 if sock:
310 sock.setsockopt(*args)
312 def set_nodelay(self, nodelay : bool):
313 if self.nodelay is None and self.nodelay_value!=nodelay:
314 self.do_set_nodelay(nodelay)
316 def do_set_nodelay(self, nodelay : bool):
317 self._setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, nodelay)
318 self.nodelay_value = nodelay
319 log("changed %s socket to nodelay=%s", self.socktype, nodelay)
321 def set_cork(self, cork : bool):
322 if self.cork and self.cork_value!=cork:
323 self._setsockopt(socket.IPPROTO_TCP, socket.TCP_CORK, cork) #@UndefinedVariable
324 self.cork_value = cork
325 log("changed %s socket to cork=%s", self.socktype, cork)
327 def peek(self, n : int):
328 return self._socket.recv(n, socket.MSG_PEEK)
330 def read(self, n : int):
331 return self._read(self._socket.recv, n)
333 def write(self, buf):
334 return self._write(self._socket.send, buf)
336 def close(self):
337 s = self._socket
338 try:
339 i = self.get_socket_info()
340 except IOError:
341 i = s
342 log("%s.close() for socket=%s", self, i)
343 Connection.close(self)
344 #meaningless for udp:
345 try:
346 s.settimeout(0)
347 except IOError:
348 pass
349 if SOCKET_SHUTDOWN:
350 try:
351 s.shutdown(socket.SHUT_RDWR)
352 except IOError:
353 log("%s.shutdown(SHUT_RDWR)", s, exc_info=True)
354 try:
355 s.close()
356 except EOFError:
357 log("%s.close()", s, exc_info=True)
358 except IOError as e:
359 if self.error_is_closed(e):
360 log("%s.close() already closed!", s)
361 else:
362 raise
363 log("%s.close() done", self)
365 def error_is_closed(self, e) -> bool:
366 return isinstance(e, CLOSED_EXCEPTIONS)
368 def __repr__(self):
369 if self.remote:
370 return "%s %s: %s <- %s" % (
371 self.socktype, self.protocol_type,
372 pretty_socket(self.local), pretty_socket(self.remote),
373 )
374 return "%s %s:%s" % (self.socktype, self.protocol_type, pretty_socket(self.local))
376 def get_info(self) -> dict:
377 d = Connection.get_info(self)
378 try:
379 d["remote"] = self.remote or ""
380 d["protocol-type"] = self.protocol_type
381 si = self.get_socket_info()
382 if si:
383 d["socket"] = si
384 except socket.error:
385 log.error("Error accessing socket information", exc_info=True)
386 return d
388 def get_socket_info(self) -> dict:
389 return self.do_get_socket_info(self._socket)
391 def do_get_socket_info(self, s) -> dict:
392 if not s:
393 return None
394 info = {}
395 try:
396 info.update({
397 "proto" : s.proto,
398 "family" : FAMILY_STR.get(s.family, int(s.family)),
399 "type" : PROTOCOL_STR.get(s.type, int(s.type)),
400 "cork" : self.cork,
401 })
402 except AttributeError:
403 log("do_get_socket_info()", exc_info=True)
404 if self.nodelay is not None:
405 info["nodelay"] = self.nodelay
406 try:
407 info["timeout"] = int(1000*(s.gettimeout() or 0))
408 except socket.error:
409 pass
410 try:
411 if POSIX:
412 fd = s.fileno()
413 else:
414 fd = 0
415 if fd:
416 info["fileno"] = fd
417 #ie: self.local = ("192.168.1.7", "14500")
418 if self.local and len(self.local)==2:
419 from xpra.net.net_util import get_interface
420 iface = get_interface(self.local[0])
421 #ie: iface = "eth0"
422 if iface and iface!="lo":
423 try:
424 from xpra.platform.netdev_query import get_interface_info
425 except ImportError as e:
426 log("do_get_socket_info() no netdev_query: %s", e)
427 else:
428 i = get_interface_info(fd, iface)
429 if i:
430 info["device"] = i
431 except (OSError, ValueError) as e:
432 log("do_get_socket_info() error querying socket speed", exc_info=True)
433 log.error("Error querying socket speed:")
434 log.error(" %s", e)
435 else:
436 opts = {
437 "SOCKET" : get_socket_options(s, socket.SOL_SOCKET, SOCKET_OPTIONS),
438 }
439 if self.socktype_wrapped in IP_SOCKTYPES:
440 opts["IP"] = get_socket_options(s, socket.SOL_IP, IP_OPTIONS)
441 if self.socktype_wrapped in TCP_SOCKTYPES:
442 opts["TCP"] = get_socket_options(s, socket.IPPROTO_TCP, TCP_OPTIONS)
443 from xpra.platform.netdev_query import get_tcp_info
444 opts["TCP_INFO"] = get_tcp_info(s)
445 #ipv6: IPV6_ADDR_PREFERENCES, IPV6_CHECKSUM, IPV6_DONTFRAG, IPV6_DSTOPTS, IPV6_HOPOPTS,
446 # IPV6_MULTICAST_HOPS, IPV6_MULTICAST_IF, IPV6_MULTICAST_LOOP, IPV6_NEXTHOP, IPV6_PATHMTU,
447 # IPV6_PKTINFO, IPV6_PREFER_TEMPADDR, IPV6_RECVDSTOPTS, IPV6_RECVHOPLIMIT, IPV6_RECVHOPOPTS,
448 # IPV6_RECVPATHMTU, IPV6_RECVPKTINFO, IPV6_RECVRTHDR, IPV6_RECVTCLASS, IPV6_RTHDR,
449 # IPV6_RTHDRDSTOPTS, IPV6_TCLASS, IPV6_UNICAST_HOPS, IPV6_USE_MIN_MTU, IPV6_V6ONLY
450 info["options"] = opts
451 return info
454def get_socket_options(sock, level, options) -> dict:
455 opts = {}
456 errs = []
457 for k in options:
458 opt = getattr(socket, k, None)
459 if opt is None:
460 continue
461 try:
462 v = sock.getsockopt(level, opt)
463 except socket.error:
464 log("sock.getsockopt(%i, %s)", level, k, exc_info=True)
465 errs.append(k)
466 else:
467 if v is not None:
468 opts[k] = v
469 if errs:
470 fileno = getattr(sock, "fileno", None)
471 #if fileno()==-1
472 if fileno and fileno()==-1:
473 log("socket is closed, ignoring: %s", csv(errs))
474 else:
475 log.warn("Warning: failed to query %s", csv(errs))
476 log.warn(" on %s", sock)
477 return opts
480class SocketPeekFile:
481 def __init__(self, fileobj, peeked, update_peek):
482 self.fileobj = fileobj
483 self.peeked = peeked
484 self.update_peek = update_peek
486 def __getattr__(self, attr):
487 if attr=="readline" and self.peeked:
488 return self.readline
489 return getattr(self.fileobj, attr)
491 def readline(self, limit=-1):
492 if self.peeked:
493 newline = self.peeked.find(b"\n")
494 peeked = self.peeked
495 l = len(peeked)
496 if newline==-1:
497 if limit==-1 or limit>l:
498 #we need to read more until we hit a newline:
499 if limit==-1:
500 more = self.fileobj.readline(limit)
501 else:
502 more = self.fileobj.readline(limit-len(self.peeked))
503 self.peeked = b""
504 self.update_peek(self.peeked)
505 return peeked+more
506 read = limit
507 else:
508 if limit<0 or limit>=newline:
509 read = newline+1
510 else:
511 read = limit
512 self.peeked = peeked[read:]
513 self.update_peek(self.peeked)
514 return peeked[:read]
515 return self.fileobj.readline(limit)
518class SocketPeekWrapper:
519 def __init__(self, sock):
520 self.socket = sock
521 self.peeked = b""
523 def __getattr__(self, attr):
524 if attr=="makefile":
525 return self.makefile
526 if attr=="recv":
527 return self.recv
528 return getattr(self.socket, attr)
530 def makefile(self, mode, bufsize=None):
531 fileobj = self.socket.makefile(mode, bufsize)
532 if self.peeked and mode and mode.startswith("r"):
533 return SocketPeekFile(fileobj, self.peeked, self._update_peek)
534 return fileobj
536 def _update_peek(self, peeked):
537 self.peeked = peeked
539 def recv(self, bufsize, flags=0):
540 if flags & socket.MSG_PEEK:
541 l = len(self.peeked)
542 if l>=bufsize:
543 log("patched_recv() peeking using existing data: %i bytes", bufsize)
544 return self.peeked[:bufsize]
545 v = self.socket.recv(bufsize-l)
546 if v:
547 log("patched_recv() peeked more: %i bytes", len(v))
548 self.peeked += v
549 return self.peeked
550 if self.peeked:
551 peeked = self.peeked[:bufsize]
552 self.peeked = self.peeked[bufsize:]
553 log("patched_recv() non peek, returned already read data")
554 return peeked
555 return self.socket.recv(bufsize, flags)
558class PeekableSocketConnection(SocketConnection):
560 def enable_peek(self):
561 assert not isinstance(self._socket, SocketPeekWrapper)
562 self._socket = SocketPeekWrapper(self._socket)
565class SSLSocketConnection(PeekableSocketConnection):
566 SSL_TIMEOUT_MESSAGES = ("The read operation timed out", "The write operation timed out")
568 def can_retry(self, e) -> bool:
569 if getattr(e, "library", None)=="SSL":
570 reason = getattr(e, "reason", None)
571 if reason in ("WRONG_VERSION_NUMBER", "UNEXPECTED_RECORD"):
572 return False
573 message = e.args[0]
574 if message in SSLSocketConnection.SSL_TIMEOUT_MESSAGES:
575 return True
576 code = getattr(e, "code", None)
577 if code in SSLSocketConnection.SSL_TIMEOUT_MESSAGES:
578 return True
579 return super().can_retry(e)
581 def get_info(self) -> dict:
582 i = SocketConnection.get_info(self)
583 i["ssl"] = True
584 for k,fn in {
585 "compression" : "compression",
586 "alpn-protocol" : "selected_alpn_protocol",
587 "npn-protocol" : "selected_npn_protocol",
588 "version" : "version",
589 }.items():
590 sfn = getattr(self._socket, fn, None)
591 if sfn:
592 v = sfn()
593 if v is not None:
594 i[k] = v
595 cipher_fn = getattr(self._socket, "cipher", None)
596 if cipher_fn:
597 cipher = cipher_fn()
598 if cipher:
599 i["cipher"] = {
600 "name" : cipher[0],
601 "protocol" : cipher[1],
602 "bits" : cipher[2],
603 }
604 return i
607def set_socket_timeout(conn, timeout=None):
608 #FIXME: this is ugly, but less intrusive than the alternative?
609 log("set_socket_timeout(%s, %s)", conn, timeout)
610 if isinstance(conn, SocketConnection):
611 conn._socket.settimeout(timeout)
614def log_new_connection(conn, socket_info=""):
615 """ logs the new connection message """
616 sock = conn._socket
617 address = conn.remote
618 socktype = conn.socktype
619 try:
620 peername = sock.getpeername()
621 except socket.error:
622 peername = address
623 try:
624 sockname = sock.getsockname()
625 except AttributeError:
626 #ie: ssh channel
627 sockname = ""
628 log("log_new_connection(%s, %s) type=%s, sock=%s, sockname=%s, address=%s, peername=%s",
629 conn, socket_info, type(conn), sock, sockname, address, peername)
630 if peername:
631 frominfo = pretty_socket(peername)
632 log.info("New %s connection received", socktype)
633 log.info(" from '%s'", pretty_socket(frominfo))
634 if socket_info:
635 log.info(" on '%s'", pretty_socket(socket_info))
636 elif socktype=="unix-domain":
637 frominfo = sockname
638 log.info("New %s connection received", socktype)
639 log.info(" on '%s'", frominfo)
640 else:
641 log.info("New %s connection received")
642 if socket_info:
643 log.info(" on %s", pretty_socket(socket_info))