Hide keyboard shortcuts

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. 

6 

7import sys 

8import os 

9import errno 

10import socket 

11 

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 

17 

18log = Logger("network", "protocol") 

19 

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) 

35 

36ABORT = { 

37 errno.ENXIO : "ENXIO", 

38 errno.ECONNRESET : "ECONNRESET", 

39 errno.EPIPE : "EPIPE", 

40 } 

41 

42 

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 

50 

51 

52CAN_RETRY_EXCEPTIONS = () 

53CLOSED_EXCEPTIONS = () 

54 

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) 

63 

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 

73 

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 

86 

87 

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) 

102 

103 

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 

124 

125 def set_nodelay(self, nodelay : bool): 

126 pass 

127 

128 def set_cork(self, cork : bool): 

129 pass 

130 

131 def is_active(self) -> bool: 

132 return self.active 

133 

134 def set_active(self, active : bool): 

135 self.active = active 

136 

137 def close(self): 

138 self.set_active(False) 

139 

140 def can_retry(self, e) -> bool: 

141 return can_retry(e) 

142 

143 def untilConcludes(self, *args): 

144 return untilConcludes(self.is_active, self.can_retry, *args) 

145 

146 def peek(self, _n : int): 

147 #not implemented 

148 return b"" 

149 

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 

156 

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 

163 

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 

182 

183 

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 

197 

198 def may_abort(self, action): 

199 """ if abort_test is defined, run it """ 

200 if self._abort_test: 

201 self._abort_test(action) 

202 

203 def flush(self): 

204 r = self._readable 

205 if r: 

206 r.flush() 

207 w = self._writeable 

208 if w: 

209 w.flush() 

210 

211 def read(self, n): 

212 self.may_abort("read") 

213 return self._read(os.read, self._read_fd, n) 

214 

215 def write(self, buf): 

216 self.may_abort("write") 

217 return self._write(os.write, self._write_fd, buf) 

218 

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) 

237 

238 def __repr__(self): 

239 return "Pipe(%s)" % str(self.target) 

240 

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 

251 

252 

253 

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 

302 

303 def get_raw_socket(self): 

304 return self._socket 

305 

306 def _setsockopt(self, *args): 

307 if self.active: 

308 sock = self.get_raw_socket() 

309 if sock: 

310 sock.setsockopt(*args) 

311 

312 def set_nodelay(self, nodelay : bool): 

313 if self.nodelay is None and self.nodelay_value!=nodelay: 

314 self.do_set_nodelay(nodelay) 

315 

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) 

320 

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) 

326 

327 def peek(self, n : int): 

328 return self._socket.recv(n, socket.MSG_PEEK) 

329 

330 def read(self, n : int): 

331 return self._read(self._socket.recv, n) 

332 

333 def write(self, buf): 

334 return self._write(self._socket.send, buf) 

335 

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) 

364 

365 def error_is_closed(self, e) -> bool: 

366 return isinstance(e, CLOSED_EXCEPTIONS) 

367 

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)) 

375 

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 

387 

388 def get_socket_info(self) -> dict: 

389 return self.do_get_socket_info(self._socket) 

390 

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 

452 

453 

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 

478 

479 

480class SocketPeekFile: 

481 def __init__(self, fileobj, peeked, update_peek): 

482 self.fileobj = fileobj 

483 self.peeked = peeked 

484 self.update_peek = update_peek 

485 

486 def __getattr__(self, attr): 

487 if attr=="readline" and self.peeked: 

488 return self.readline 

489 return getattr(self.fileobj, attr) 

490 

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) 

516 

517 

518class SocketPeekWrapper: 

519 def __init__(self, sock): 

520 self.socket = sock 

521 self.peeked = b"" 

522 

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) 

529 

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 

535 

536 def _update_peek(self, peeked): 

537 self.peeked = peeked 

538 

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) 

556 

557 

558class PeekableSocketConnection(SocketConnection): 

559 

560 def enable_peek(self): 

561 assert not isinstance(self._socket, SocketPeekWrapper) 

562 self._socket = SocketPeekWrapper(self._socket) 

563 

564 

565class SSLSocketConnection(PeekableSocketConnection): 

566 SSL_TIMEOUT_MESSAGES = ("The read operation timed out", "The write operation timed out") 

567 

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) 

580 

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 

605 

606 

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) 

612 

613 

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))