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) 2010-2021 Antoine Martin <antoine@xpra.org> 

3# Copyright (C) 2008, 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 os 

8import sys 

9import uuid 

10import signal 

11import socket 

12import string 

13 

14from xpra.log import Logger 

15from xpra.scripts.config import InitExit 

16from xpra.common import SPLASH_EXIT_DELAY 

17from xpra.child_reaper import getChildReaper, reaper_cleanup 

18from xpra.net import compression 

19from xpra.net.common import may_log_packet, PACKET_TYPES 

20from xpra.net.protocol_classes import get_client_protocol_class 

21from xpra.net.protocol import Protocol, sanity_checks 

22from xpra.net.net_util import get_network_caps 

23from xpra.net.digest import get_salt, gendigest 

24from xpra.net.crypto import ( 

25 crypto_backend_init, get_iterations, get_iv, choose_padding, 

26 ENCRYPTION_CIPHERS, ENCRYPT_FIRST_PACKET, DEFAULT_IV, DEFAULT_SALT, 

27 DEFAULT_ITERATIONS, INITIAL_PADDING, DEFAULT_PADDING, ALL_PADDING_OPTIONS, PADDING_OPTIONS, 

28 ) 

29from xpra.version_util import get_version_info, XPRA_VERSION 

30from xpra.platform.info import get_name 

31from xpra.os_util import ( 

32 get_machine_id, get_user_uuid, register_SIGUSR_signals, 

33 filedata_nocrlf, force_quit, 

34 SIGNAMES, BITS, 

35 strtobytes, bytestostr, hexstr, monotonic_time, use_tty, 

36 parse_encoded_bin_data, 

37 ) 

38from xpra.util import ( 

39 flatten_dict, typedict, updict, parse_simple_dict, noerr, 

40 repr_ellipsized, ellipsizer, nonl, 

41 envbool, envint, disconnect_is_an_error, dump_all_frames, engs, csv, obsc, 

42 ) 

43from xpra.client.mixins.serverinfo_mixin import ServerInfoMixin 

44from xpra.client.mixins.fileprint_mixin import FilePrintMixin 

45from xpra.exit_codes import (EXIT_OK, EXIT_CONNECTION_LOST, EXIT_TIMEOUT, EXIT_UNSUPPORTED, 

46 EXIT_PASSWORD_REQUIRED, EXIT_PASSWORD_FILE_ERROR, EXIT_INCOMPATIBLE_VERSION, 

47 EXIT_ENCRYPTION, EXIT_FAILURE, EXIT_PACKET_FAILURE, 

48 EXIT_NO_AUTHENTICATION, EXIT_INTERNAL_ERROR) 

49 

50log = Logger("client") 

51netlog = Logger("network") 

52authlog = Logger("auth") 

53mouselog = Logger("mouse") 

54cryptolog = Logger("crypto") 

55bandwidthlog = Logger("bandwidth") 

56 

57EXTRA_TIMEOUT = 10 

58ALLOW_UNENCRYPTED_PASSWORDS = envbool("XPRA_ALLOW_UNENCRYPTED_PASSWORDS", False) 

59ALLOW_LOCALHOST_PASSWORDS = envbool("XPRA_ALLOW_LOCALHOST_PASSWORDS", True) 

60DETECT_LEAKS = envbool("XPRA_DETECT_LEAKS", False) 

61LEGACY_SALT_DIGEST = envbool("XPRA_LEGACY_SALT_DIGEST", False) 

62MOUSE_DELAY = envint("XPRA_MOUSE_DELAY", 0) 

63SPLASH_LOG = envbool("XPRA_SPLASH_LOG", False) 

64 

65 

66def noop(): 

67 pass 

68 

69 

70""" Base class for Xpra clients. 

71 Provides the glue code for: 

72 * sending packets via Protocol 

73 * handling packets received via _process_packet 

74 For an actual implementation, look at: 

75 * GObjectXpraClient 

76 * xpra.client.gtk2.client 

77 * xpra.client.gtk3.client 

78""" 

79class XpraClientBase(ServerInfoMixin, FilePrintMixin): 

80 

81 INSTALL_SIGNAL_HANDLERS = True 

82 

83 def __init__(self): 

84 #this may be called more than once, 

85 #skip doing internal init again: 

86 if not hasattr(self, "exit_code"): 

87 self.defaults_init() 

88 ServerInfoMixin.__init__(self) 

89 FilePrintMixin.__init__(self) 

90 self._init_done = False 

91 

92 def defaults_init(self): 

93 #skip warning when running the client 

94 from xpra import child_reaper 

95 child_reaper.POLL_WARNING = False 

96 getChildReaper() 

97 log("XpraClientBase.defaults_init() os.environ:") 

98 for k,v in os.environ.items(): 

99 log(" %s=%r", k, v) 

100 #client state: 

101 self.exit_code = None 

102 self.exit_on_signal = False 

103 self.display_desc = {} 

104 self.progress_process = None 

105 self.progress_timer = None 

106 #connection attributes: 

107 self.hello_extra = {} 

108 self.compression_level = 0 

109 self.display = None 

110 self.challenge_handlers = [] 

111 self.username = None 

112 self.password = None 

113 self.password_file = () 

114 self.password_index = 0 

115 self.password_sent = False 

116 self.encryption = None 

117 self.encryption_keyfile = None 

118 self.server_padding_options = [DEFAULT_PADDING] 

119 self.server_client_shutdown = True 

120 self.server_compressors = [] 

121 #protocol stuff: 

122 self._protocol = None 

123 self._priority_packets = [] 

124 self._ordinary_packets = [] 

125 self._mouse_position = None 

126 self._mouse_position_pending = None 

127 self._mouse_position_send_time = 0 

128 self._mouse_position_delay = MOUSE_DELAY 

129 self._mouse_position_timer = 0 

130 self._aliases = {} 

131 #server state and caps: 

132 self.connection_established = False 

133 self.completed_startup = False 

134 self.uuid = get_user_uuid() 

135 self.session_id = uuid.uuid4().hex 

136 self.init_packet_handlers() 

137 self.have_more = noop 

138 sanity_checks() 

139 

140 def init(self, opts): 

141 if self._init_done: 

142 #the gtk client classes can inherit this method 

143 #from multiple parents, skip initializing twice 

144 return 

145 self._init_done = True 

146 for c in XpraClientBase.__bases__: 

147 c.init(self, opts) 

148 self.compression_level = opts.compression_level 

149 self.display = opts.display 

150 self.username = opts.username 

151 self.password = opts.password 

152 self.password_file = opts.password_file 

153 self.encryption = opts.encryption or opts.tcp_encryption 

154 self.encryption_keyfile = opts.encryption_keyfile or opts.tcp_encryption_keyfile 

155 self.init_challenge_handlers(opts.challenge_handlers) 

156 self.init_aliases() 

157 if self.INSTALL_SIGNAL_HANDLERS: 

158 self.install_signal_handlers() 

159 

160 

161 def show_progress(self, pct, text=""): 

162 log("progress(%s, %s)", pct, text) 

163 if SPLASH_LOG: 

164 log.info("%3i %s", pct, text) 

165 pp = self.progress_process 

166 if not pp: 

167 return 

168 if pp.poll(): 

169 self.progress_process = None 

170 return 

171 noerr(pp.stdin.write, ("%i:%s\n" % (pct, text)).encode("latin1")) 

172 noerr(pp.stdin.flush) 

173 if pct==100: 

174 #it should exit on its own, but just in case: 

175 #kill it if it's still running after 2 seconds 

176 self.cancel_progress_timer() 

177 def stop_progress(): 

178 self.progress_timer = None 

179 self.stop_progress_process() 

180 self.progress_timer = self.timeout_add(SPLASH_EXIT_DELAY*1000+500, stop_progress) 

181 

182 def cancel_progress_timer(self): 

183 pt = self.progress_timer 

184 if pt: 

185 self.progress_timer = None 

186 self.source_remove(pt) 

187 

188 

189 def init_challenge_handlers(self, challenge_handlers): 

190 #register the authentication challenge handlers: 

191 authlog("init_challenge_handlers(%s)", challenge_handlers) 

192 ch = tuple(x.strip() for x in (challenge_handlers or "".split(","))) 

193 for ch_name in ch: 

194 if ch_name=="none": 

195 continue 

196 if ch_name=="all": 

197 items = ( 

198 "uri", "file", "env", 

199 "kerberos", "gss", 

200 "u2f", 

201 "prompt", "prompt", "prompt", "prompt", 

202 ) 

203 ierror = authlog 

204 else: 

205 items = (ch_name, ) 

206 ierror = authlog.warn 

207 for auth in items: 

208 instance = self.get_challenge_handler(auth, ierror) 

209 if instance: 

210 self.challenge_handlers.append(instance) 

211 if DETECT_LEAKS: 

212 from xpra.util import detect_leaks 

213 print_leaks = detect_leaks() 

214 self.timeout_add(10*1000, print_leaks) 

215 

216 def get_challenge_handler(self, auth, import_error_logger): 

217 #the module may have attributes, 

218 #ie: file:filename=password.txt 

219 parts = auth.split(":", 1) 

220 mod_name = parts[0] 

221 kwargs = {} 

222 if len(parts)==2: 

223 kwargs = parse_simple_dict(parts[1]) 

224 auth_mod_name = "xpra.client.auth.%s_handler" % mod_name 

225 authlog("auth module name for '%s': '%s'", auth, auth_mod_name) 

226 try: 

227 auth_module = __import__(auth_mod_name, {}, {}, ["Handler"]) 

228 auth_class = auth_module.Handler 

229 instance = auth_class(self, **kwargs) 

230 return instance 

231 except ImportError as e: 

232 import_error_logger("Error: authentication handler %s not available", mod_name) 

233 import_error_logger(" %s", e) 

234 except Exception as e: 

235 authlog("get_challenge_handler(%s)", auth, exc_info=True) 

236 authlog.error("Error: cannot instantiate authentication handler") 

237 authlog.error(" '%s': %s", mod_name, e) 

238 return None 

239 

240 

241 def may_notify(self, nid, summary, body, *args, **kwargs): 

242 notifylog = Logger("notify") 

243 notifylog("may_notify(%s, %s, %s, %s, %s)", nid, summary, body, args, kwargs) 

244 notifylog.info("%s", summary) 

245 if body: 

246 for x in body.splitlines(): 

247 notifylog.info(" %s", x) 

248 

249 

250 def handle_deadly_signal(self, signum, _frame=None): 

251 sys.stderr.write("\ngot deadly signal %s, exiting\n" % SIGNAMES.get(signum, signum)) 

252 sys.stderr.flush() 

253 self.cleanup() 

254 force_quit(128 + signum) 

255 

256 def handle_app_signal(self, signum, _frame=None): 

257 try: 

258 log.info("exiting") 

259 except Exception: 

260 pass 

261 signal.signal(signal.SIGINT, self.handle_deadly_signal) 

262 signal.signal(signal.SIGTERM, self.handle_deadly_signal) 

263 self.signal_cleanup() 

264 reason = "exit on signal %s" % SIGNAMES.get(signum, signum) 

265 self.timeout_add(0, self.signal_disconnect_and_quit, 128 + signum, reason) 

266 

267 def install_signal_handlers(self): 

268 def os_signal(signum, _frame=None): 

269 try: 

270 sys.stderr.write("\n") 

271 sys.stderr.flush() 

272 log.info("client got signal %s", SIGNAMES.get(signum, signum)) 

273 except Exception: 

274 pass 

275 self.handle_app_signal(signum) 

276 signal.signal(signal.SIGINT, os_signal) 

277 signal.signal(signal.SIGTERM, os_signal) 

278 register_SIGUSR_signals(self.idle_add) 

279 

280 def signal_disconnect_and_quit(self, exit_code, reason): 

281 log("signal_disconnect_and_quit(%s, %s) exit_on_signal=%s", exit_code, reason, self.exit_on_signal) 

282 if not self.exit_on_signal: 

283 #if we get another signal, we'll try to exit without idle_add... 

284 self.exit_on_signal = True 

285 self.idle_add(self.disconnect_and_quit, exit_code, reason) 

286 self.idle_add(self.quit, exit_code) 

287 self.idle_add(self.exit) 

288 return 

289 #warning: this will run cleanup code from the signal handler 

290 self.disconnect_and_quit(exit_code, reason) 

291 self.quit(exit_code) 

292 self.exit() 

293 force_quit(exit_code) 

294 

295 def signal_cleanup(self): 

296 #placeholder for stuff that can be cleaned up from the signal handler 

297 #(non UI thread stuff) 

298 pass 

299 

300 def disconnect_and_quit(self, exit_code, reason): 

301 #make sure that we set the exit code early, 

302 #so the protocol shutdown won't set a different one: 

303 if self.exit_code is None: 

304 self.exit_code = exit_code 

305 #try to tell the server we're going, then quit 

306 log("disconnect_and_quit(%s, %s)", exit_code, reason) 

307 p = self._protocol 

308 if p is None or p.is_closed(): 

309 self.quit(exit_code) 

310 return 

311 def protocol_closed(): 

312 log("disconnect_and_quit: protocol_closed()") 

313 self.idle_add(self.quit, exit_code) 

314 if p: 

315 p.send_disconnect([reason], done_callback=protocol_closed) 

316 self.timeout_add(1000, self.quit, exit_code) 

317 

318 def exit(self): 

319 log("XpraClientBase.exit() calling %s", sys.exit) 

320 sys.exit() 

321 

322 

323 def client_type(self) -> str: 

324 #overriden in subclasses! 

325 return "Python" 

326 

327 def get_scheduler(self): 

328 raise NotImplementedError() 

329 

330 def setup_connection(self, conn): 

331 netlog("setup_connection(%s) timeout=%s, socktype=%s", conn, conn.timeout, conn.socktype) 

332 if conn.socktype=="udp": 

333 self.add_packet_handler("udp-control", self._process_udp_control, False) 

334 protocol_class = get_client_protocol_class(conn.socktype) 

335 protocol = protocol_class(self.get_scheduler(), conn, self.process_packet, self.next_packet) 

336 self._protocol = protocol 

337 for x in (b"keymap-changed", b"server-settings", b"logging", b"input-devices"): 

338 protocol.large_packets.append(x) 

339 protocol.set_compression_level(self.compression_level) 

340 protocol.receive_aliases.update(self._aliases) 

341 protocol.enable_default_encoder() 

342 protocol.enable_default_compressor() 

343 encryption = self.get_encryption() 

344 if encryption and ENCRYPT_FIRST_PACKET: 

345 key = self.get_encryption_key() 

346 protocol.set_cipher_out(encryption, 

347 DEFAULT_IV, key, DEFAULT_SALT, DEFAULT_ITERATIONS, INITIAL_PADDING) 

348 self.have_more = protocol.source_has_more 

349 if conn.timeout>0: 

350 self.timeout_add((conn.timeout + EXTRA_TIMEOUT) * 1000, self.verify_connected) 

351 process = getattr(conn, "process", None) #ie: ssh is handled by another process 

352 if process: 

353 proc, name, command = process 

354 if proc: 

355 getChildReaper().add_process(proc, name, command, ignore=True, forget=False) 

356 netlog("setup_connection(%s) protocol=%s", conn, protocol) 

357 return protocol 

358 

359 def _process_udp_control(self, packet): 

360 #send it back to the protocol object: 

361 self._protocol.process_control(*packet[1:]) 

362 

363 

364 def init_aliases(self): 

365 i = 1 

366 for key in PACKET_TYPES: 

367 self._aliases[i] = key 

368 i += 1 

369 

370 def has_password(self) -> bool: 

371 return self.password or self.password_file or os.environ.get('XPRA_PASSWORD') 

372 

373 def send_hello(self, challenge_response=None, client_salt=None): 

374 if not self._protocol: 

375 log("send_hello(..) skipped, no protocol (listen mode?)") 

376 return 

377 try: 

378 hello = self.make_hello_base() 

379 if self.has_password() and not challenge_response: 

380 #avoid sending the full hello: tell the server we want 

381 #a packet challenge first 

382 hello["challenge"] = True 

383 else: 

384 hello.update(self.make_hello()) 

385 except InitExit as e: 

386 log.error("error preparing connection:") 

387 log.error(" %s", e) 

388 self.quit(EXIT_INTERNAL_ERROR) 

389 return 

390 except Exception as e: 

391 log.error("error preparing connection: %s", e, exc_info=True) 

392 self.quit(EXIT_INTERNAL_ERROR) 

393 return 

394 if challenge_response: 

395 hello["challenge_response"] = challenge_response 

396 #make it harder for a passive attacker to guess the password length 

397 #by observing packet sizes (only relevant for wss and ssl) 

398 hello["challenge_padding"] = get_salt(max(32, 512-len(challenge_response))) 

399 if client_salt: 

400 hello["challenge_client_salt"] = client_salt 

401 log("send_hello(%s) packet=%s", hexstr(challenge_response or ""), hello) 

402 self.send("hello", hello) 

403 

404 def verify_connected(self): 

405 if not self.connection_established: 

406 #server has not said hello yet 

407 self.warn_and_quit(EXIT_TIMEOUT, "connection timed out") 

408 

409 

410 def make_hello_base(self): 

411 capabilities = flatten_dict(get_network_caps()) 

412 #add "kerberos", "gss" and "u2f" digests if enabled: 

413 for handler in self.challenge_handlers: 

414 digest = handler.get_digest() 

415 if digest: 

416 capabilities["digest"].append(digest) 

417 capabilities.update(FilePrintMixin.get_caps(self)) 

418 capabilities.update({ 

419 "version" : XPRA_VERSION, 

420 "websocket.multi-packet": True, 

421 "hostname" : socket.gethostname(), 

422 "uuid" : self.uuid, 

423 "session-id" : self.session_id, 

424 "username" : self.username, 

425 "name" : get_name(), 

426 "client_type" : self.client_type(), 

427 "python.version" : sys.version_info[:3], 

428 "python.bits" : BITS, 

429 "compression_level" : self.compression_level, 

430 "argv" : sys.argv, 

431 }) 

432 capabilities.update(self.get_file_transfer_features()) 

433 if self.display: 

434 capabilities["display"] = self.display 

435 def up(prefix, d): 

436 updict(capabilities, prefix, d) 

437 up("build", self.get_version_info()) 

438 mid = get_machine_id() 

439 if mid: 

440 capabilities["machine_id"] = mid 

441 encryption = self.get_encryption() 

442 if encryption: 

443 crypto_backend_init() 

444 assert encryption in ENCRYPTION_CIPHERS, "invalid encryption '%s', options: %s" % (encryption, csv(ENCRYPTION_CIPHERS)) 

445 iv = get_iv() 

446 key_salt = get_salt() 

447 iterations = get_iterations() 

448 padding = choose_padding(self.server_padding_options) 

449 up("cipher", { 

450 "" : encryption, 

451 "iv" : iv, 

452 "key_salt" : key_salt, 

453 "key_stretch_iterations": iterations, 

454 "padding" : padding, 

455 "padding.options" : PADDING_OPTIONS, 

456 }) 

457 key = self.get_encryption_key() 

458 self._protocol.set_cipher_in(encryption, iv, key, key_salt, iterations, padding) 

459 netlog("encryption capabilities: %s", dict((k,v) for k,v in capabilities.items() if k.startswith("cipher"))) 

460 capabilities.update(self.hello_extra) 

461 return capabilities 

462 

463 def get_version_info(self) -> dict: 

464 return get_version_info() 

465 

466 def make_hello(self): 

467 capabilities = { 

468 "randr_notify" : False, #only client.py cares about this 

469 "windows" : False, #only client.py cares about this 

470 } 

471 if self._aliases: 

472 reverse_aliases = {} 

473 for i, packet_type in self._aliases.items(): 

474 reverse_aliases[packet_type] = i 

475 capabilities["aliases"] = reverse_aliases 

476 return capabilities 

477 

478 def compressed_wrapper(self, datatype, data, level=5): 

479 if level>0 and len(data)>=256: 

480 #ugly assumptions here, should pass by name 

481 zlib = "zlib" in self.server_compressors 

482 lz4 = "lz4" in self.server_compressors 

483 lzo = "lzo" in self.server_compressors 

484 #never use brotli as a generic compressor 

485 #brotli = "brotli" in self.server_compressors and compression.use_brotli 

486 cw = compression.compressed_wrapper(datatype, data, level=level, 

487 zlib=zlib, lz4=lz4, lzo=lzo, 

488 brotli=False, none=True, 

489 can_inline=False) 

490 if len(cw)<len(data): 

491 #the compressed version is smaller, use it: 

492 return cw 

493 #we can't compress, so at least avoid warnings in the protocol layer: 

494 return compression.Compressed("raw %s" % datatype, data, can_inline=True) 

495 

496 

497 def send(self, *parts): 

498 self._ordinary_packets.append(parts) 

499 self.have_more() 

500 

501 def send_now(self, *parts): 

502 self._priority_packets.append(parts) 

503 self.have_more() 

504 

505 def send_positional(self, packet): 

506 #packets that include the mouse position in them 

507 #we can cancel the pending position packets 

508 self._ordinary_packets.append(packet) 

509 self._mouse_position = None 

510 self._mouse_position_pending = None 

511 self.cancel_send_mouse_position_timer() 

512 self.have_more() 

513 

514 def send_mouse_position(self, packet): 

515 if self._mouse_position_timer: 

516 self._mouse_position_pending = packet 

517 return 

518 self._mouse_position_pending = packet 

519 now = monotonic_time() 

520 elapsed = int(1000*(now-self._mouse_position_send_time)) 

521 delay = self._mouse_position_delay-elapsed 

522 mouselog("send_mouse_position(%s) elapsed=%i, delay left=%i", packet, elapsed, delay) 

523 if delay>0: 

524 self._mouse_position_timer = self.timeout_add(delay, self.do_send_mouse_position) 

525 else: 

526 self.do_send_mouse_position() 

527 

528 def do_send_mouse_position(self): 

529 self._mouse_position_timer = 0 

530 self._mouse_position_send_time = monotonic_time() 

531 self._mouse_position = self._mouse_position_pending 

532 mouselog("do_send_mouse_position() position=%s", self._mouse_position) 

533 self.have_more() 

534 

535 def cancel_send_mouse_position_timer(self): 

536 mpt = self._mouse_position_timer 

537 if mpt: 

538 self._mouse_position_timer = 0 

539 self.source_remove(mpt) 

540 

541 

542 def next_packet(self): 

543 netlog("next_packet() packets in queues: priority=%i, ordinary=%i, mouse=%s", 

544 len(self._priority_packets), len(self._ordinary_packets), bool(self._mouse_position)) 

545 synchronous = True 

546 if self._priority_packets: 

547 packet = self._priority_packets.pop(0) 

548 elif self._ordinary_packets: 

549 packet = self._ordinary_packets.pop(0) 

550 elif self._mouse_position is not None: 

551 packet = self._mouse_position 

552 synchronous = False 

553 self._mouse_position = None 

554 else: 

555 packet = None 

556 has_more = packet is not None and \ 

557 (bool(self._priority_packets) or bool(self._ordinary_packets) \ 

558 or self._mouse_position is not None) 

559 return packet, None, None, None, synchronous, has_more 

560 

561 

562 def stop_progress_process(self): 

563 pp = self.progress_process 

564 if not pp: 

565 return 

566 self.progress_process = None 

567 if pp.poll(): 

568 return 

569 try: 

570 pp.terminate() 

571 except Exception: 

572 pass 

573 

574 def cleanup(self): 

575 self.cancel_progress_timer() 

576 self.stop_progress_process() 

577 reaper_cleanup() 

578 try: 

579 FilePrintMixin.cleanup(self) 

580 except Exception: 

581 log.error("%s", FilePrintMixin.cleanup, exc_info=True) 

582 p = self._protocol 

583 log("XpraClientBase.cleanup() protocol=%s", p) 

584 if p: 

585 log("calling %s", p.close) 

586 p.close() 

587 self._protocol = None 

588 log("cleanup done") 

589 self.cancel_send_mouse_position_timer() 

590 dump_all_frames() 

591 

592 

593 def glib_init(self): 

594 #this will take care of calling threads_init if needed: 

595 from gi.repository import GLib 

596 register_SIGUSR_signals(GLib.idle_add) 

597 

598 def run(self): 

599 self.start_protocol() 

600 

601 def start_protocol(self): 

602 #protocol may be None in "listen" mode 

603 if self._protocol: 

604 self._protocol.start() 

605 

606 def quit(self, exit_code=0): 

607 raise Exception("override me!") 

608 

609 def warn_and_quit(self, exit_code, message): 

610 log.warn(message) 

611 self.quit(exit_code) 

612 

613 

614 def send_shutdown_server(self): 

615 assert self.server_client_shutdown 

616 self.send("shutdown-server") 

617 

618 def _process_disconnect(self, packet): 

619 #ie: ("disconnect", "version error", "incompatible version") 

620 info = tuple(nonl(bytestostr(x)) for x in packet[1:]) 

621 reason = info[0] 

622 if not self.connection_established: 

623 #server never sent hello to us - so disconnect is an error 

624 #(but we don't know which one - the info message may help) 

625 self.server_disconnect_warning("disconnected before the session could be established", *info) 

626 elif disconnect_is_an_error(reason): 

627 self.server_disconnect_warning(*info) 

628 elif self.exit_code is None: 

629 #we're not in the process of exiting already, 

630 #tell the user why the server is disconnecting us 

631 self.server_disconnect(*info) 

632 

633 def server_disconnect_warning(self, reason, *extra_info): 

634 log.warn("Warning: server connection failure:") 

635 log.warn(" %s", reason) 

636 for x in extra_info: 

637 log.warn(" %s", x) 

638 self.quit(EXIT_FAILURE) 

639 

640 def server_disconnect(self, reason, *extra_info): 

641 log.info("server requested disconnect:") 

642 log.info(" %s", reason) 

643 for x in extra_info: 

644 log.info(" %s", x) 

645 self.quit(EXIT_OK) 

646 

647 

648 def _process_connection_lost(self, _packet): 

649 p = self._protocol 

650 if p and p.input_raw_packetcount==0: 

651 props = p.get_info() 

652 c = props.get("compression", "unknown") 

653 e = props.get("encoder", "rencode") 

654 netlog.error("Error: failed to receive anything, not an xpra server?") 

655 netlog.error(" could also be the wrong protocol, username, password or port") 

656 netlog.error(" or the session was not found") 

657 if c!="unknown" or e!="rencode": 

658 netlog.error(" or maybe this server does not support '%s' compression or '%s' packet encoding?", c, e) 

659 if self.exit_code is None: 

660 self.warn_and_quit(EXIT_CONNECTION_LOST, "Connection lost") 

661 

662 

663 ######################################## 

664 # Authentication 

665 def _process_challenge(self, packet): 

666 authlog("processing challenge: %s", packet[1:]) 

667 if not self.validate_challenge_packet(packet): 

668 return 

669 authlog("challenge handlers: %s", self.challenge_handlers) 

670 digest = bytestostr(packet[3]) 

671 while self.challenge_handlers: 

672 handler = self.pop_challenge_handler(digest) 

673 try: 

674 authlog("calling challenge handler %s", handler) 

675 r = handler.handle(packet) 

676 authlog("%s(%s)=%s", handler.handle, packet, r) 

677 if r: 

678 #the challenge handler claims to have handled authentication 

679 return 

680 except Exception as e: 

681 authlog("%s(%s)", handler.handle, packet, exc_info=True) 

682 authlog.error("Error in %r challenge handler:", handler) 

683 authlog.error(" %s", str(e) or type(e)) 

684 continue 

685 authlog.warn("Warning: failed to connect, authentication required") 

686 self.quit(EXIT_PASSWORD_REQUIRED) 

687 

688 def pop_challenge_handler(self, digest): 

689 #find the challenge handler most suitable for this digest type, 

690 #otherwise take the first one 

691 digest_type = digest.split(":")[0] #ie: "kerberos:value" -> "kerberos" 

692 index = 0 

693 for i, handler in enumerate(self.challenge_handlers): 

694 if handler.get_digest()==digest_type: 

695 index = i 

696 break 

697 return self.challenge_handlers.pop(index) 

698 

699 

700 #utility method used by some authentication handlers, 

701 #and overriden in UI client to provide a GUI dialog 

702 def do_process_challenge_prompt(self, packet, prompt="password"): 

703 authlog("do_process_challenge_prompt() use_tty=%s", use_tty()) 

704 if use_tty(): 

705 import getpass 

706 authlog("stdin isatty, using password prompt") 

707 password = getpass.getpass("%s :" % self.get_challenge_prompt(prompt)) 

708 authlog("password read from tty via getpass: %s", obsc(password)) 

709 self.send_challenge_reply(packet, password) 

710 return True 

711 else: 

712 from xpra.platform.paths import get_nodock_command 

713 cmd = get_nodock_command()+["_pass", prompt] 

714 try: 

715 from subprocess import Popen, PIPE 

716 proc = Popen(cmd, stdout=PIPE) 

717 getChildReaper().add_process(proc, "password-prompt", cmd, True, True) 

718 out, err = proc.communicate(None, 60) 

719 authlog("err(%s)=%s", cmd, err) 

720 password = out.decode() 

721 self.send_challenge_reply(packet, password) 

722 return True 

723 except Exception: 

724 log("Error: failed to show GUi for password prompt", exc_info=True) 

725 return False 

726 

727 def auth_error(self, code, message, server_message="authentication failed"): 

728 authlog.error("Error: authentication failed:") 

729 authlog.error(" %s", message) 

730 self.disconnect_and_quit(code, server_message) 

731 

732 def validate_challenge_packet(self, packet): 

733 digest = bytestostr(packet[3]) 

734 #don't send XORed password unencrypted: 

735 if digest=="xor": 

736 encrypted = self._protocol.cipher_out or self._protocol.get_info().get("type") in ("ssl", "wss") 

737 local = self.display_desc.get("local", False) 

738 authlog("xor challenge, encrypted=%s, local=%s", encrypted, local) 

739 if local and ALLOW_LOCALHOST_PASSWORDS: 

740 return True 

741 if not encrypted and not ALLOW_UNENCRYPTED_PASSWORDS: 

742 self.auth_error(EXIT_ENCRYPTION, 

743 "server requested '%s' digest, cowardly refusing to use it without encryption" % digest, 

744 "invalid digest") 

745 return False 

746 salt_digest = "xor" 

747 if len(packet)>=5: 

748 salt_digest = bytestostr(packet[4]) 

749 if salt_digest in ("xor", "des"): 

750 if not LEGACY_SALT_DIGEST: 

751 self.auth_error(EXIT_INCOMPATIBLE_VERSION, "server uses legacy salt digest '%s'" % salt_digest) 

752 return False 

753 log.warn("Warning: server using legacy support for '%s' salt digest", salt_digest) 

754 return True 

755 

756 def get_challenge_prompt(self, prompt="password"): 

757 text = "Please enter the %s" % (prompt,) 

758 try: 

759 from xpra.net.bytestreams import pretty_socket 

760 conn = self._protocol._conn 

761 text += " for user '%s',\n connecting to %s server %s" % ( 

762 self.username, conn.socktype, pretty_socket(conn.remote), 

763 ) 

764 except Exception: 

765 pass 

766 return text 

767 

768 def send_challenge_reply(self, packet, password): 

769 if not password: 

770 if self.password_file: 

771 self.auth_error(EXIT_PASSWORD_FILE_ERROR, 

772 "failed to load password from file%s %s" % (engs(self.password_file), csv(self.password_file)), 

773 "no password available") 

774 else: 

775 self.auth_error(EXIT_PASSWORD_REQUIRED, 

776 "this server requires authentication and no password is available") 

777 return 

778 encryption = self.get_encryption() 

779 if encryption: 

780 assert len(packet)>=3, "challenge does not contain encryption details to use for the response" 

781 server_cipher = typedict(packet[2]) 

782 key = self.get_encryption_key() 

783 if not self.set_server_encryption(server_cipher, key): 

784 return 

785 #all server versions support a client salt, 

786 #they also tell us which digest to use: 

787 server_salt = bytestostr(packet[1]) 

788 digest = bytestostr(packet[3]) 

789 actual_digest = digest.split(":", 1)[0] 

790 l = len(server_salt) 

791 salt_digest = "xor" 

792 if len(packet)>=5: 

793 salt_digest = bytestostr(packet[4]) 

794 if salt_digest=="xor": 

795 #with xor, we have to match the size 

796 assert l>=16, "server salt is too short: only %i bytes, minimum is 16" % l 

797 assert l<=256, "server salt is too long: %i bytes, maximum is 256" % l 

798 else: 

799 #other digest, 32 random bytes is enough: 

800 l = 32 

801 client_salt = get_salt(l) 

802 salt = gendigest(salt_digest, client_salt, server_salt) 

803 authlog("combined %s salt(%s, %s)=%s", salt_digest, hexstr(server_salt), hexstr(client_salt), hexstr(salt)) 

804 

805 challenge_response = gendigest(actual_digest, password, salt) 

806 if not challenge_response: 

807 log("invalid digest module '%s': %s", actual_digest) 

808 self.auth_error(EXIT_UNSUPPORTED, 

809 "server requested '%s' digest but it is not supported" % actual_digest, "invalid digest") 

810 return 

811 authlog("%s(%s, %s)=%s", actual_digest, repr(password), repr(salt), repr(challenge_response)) 

812 self.do_send_challenge_reply(challenge_response, client_salt) 

813 

814 def do_send_challenge_reply(self, challenge_response, client_salt): 

815 self.password_sent = True 

816 self.send_hello(challenge_response, client_salt) 

817 

818 ######################################## 

819 # Encryption 

820 def set_server_encryption(self, caps, key): 

821 cipher = caps.strget("cipher") 

822 cipher_iv = caps.strget("cipher.iv") 

823 key_salt = caps.strget("cipher.key_salt") 

824 iterations = caps.intget("cipher.key_stretch_iterations") 

825 padding = caps.strget("cipher.padding", DEFAULT_PADDING) 

826 #server may tell us what it supports, 

827 #either from hello response or from challenge packet: 

828 self.server_padding_options = caps.strtupleget("cipher.padding.options", (DEFAULT_PADDING,)) 

829 if not cipher or not cipher_iv: 

830 self.warn_and_quit(EXIT_ENCRYPTION, 

831 "the server does not use or support encryption/password, cannot continue") 

832 return False 

833 if cipher not in ENCRYPTION_CIPHERS: 

834 self.warn_and_quit(EXIT_ENCRYPTION, 

835 "unsupported server cipher: %s, allowed ciphers: %s" % (cipher, csv(ENCRYPTION_CIPHERS))) 

836 return False 

837 if padding not in ALL_PADDING_OPTIONS: 

838 self.warn_and_quit(EXIT_ENCRYPTION, 

839 "unsupported server cipher padding: %s, allowed ciphers: %s" % (padding, csv(ALL_PADDING_OPTIONS))) 

840 return False 

841 p = self._protocol 

842 if not p: 

843 return False 

844 p.set_cipher_out(cipher, cipher_iv, key, key_salt, iterations, padding) 

845 return True 

846 

847 

848 def get_encryption(self): 

849 p = self._protocol 

850 if not p: 

851 return None 

852 conn = p._conn 

853 #prefer the socket option, fallback to "--encryption=" option: 

854 encryption = conn.options.get("encryption", self.encryption) 

855 cryptolog("get_encryption() connection options encryption=%s", encryption) 

856 #specifying keyfile or keydata is enough: 

857 if not encryption and any(conn.options.get(x) for x in ("encryption-keyfile", "keyfile", "keydata")): 

858 cryptolog("found keyfile or keydata attribute, enabling 'AES' encryption") 

859 encryption = "AES" 

860 if not encryption and os.environ.get("XPRA_ENCRYPTION_KEY"): 

861 cryptolog("found encryption key environment variable, enabling 'AES' encryption") 

862 encryption = "AES" 

863 return encryption 

864 

865 def get_encryption_key(self): 

866 conn = self._protocol._conn 

867 keydata = parse_encoded_bin_data(conn.options.get("keydata", None)) 

868 cryptolog("get_encryption_key() connection options keydata=%s", ellipsizer(keydata)) 

869 keyfile = conn.options.get("encryption-keyfile") or conn.options.get("keyfile") or self.encryption_keyfile 

870 if keyfile: 

871 if os.path.exists(keyfile): 

872 key = filedata_nocrlf(keyfile) 

873 cryptolog("get_encryption_key() loaded %i bytes from '%s'", 

874 len(key or ""), keyfile) 

875 return key 

876 cryptolog("get_encryption_key() file '%s' does not exist", keyfile) 

877 XPRA_ENCRYPTION_KEY = "XPRA_ENCRYPTION_KEY" 

878 key = strtobytes(os.environ.get(XPRA_ENCRYPTION_KEY, '')) 

879 cryptolog("get_encryption_key() got %i bytes from '%s' environment variable", 

880 len(key or ""), XPRA_ENCRYPTION_KEY) 

881 if key: 

882 return key.strip(b"\n\r") 

883 raise InitExit(1, "no encryption key") 

884 

885 def _process_hello(self, packet): 

886 self.remove_packet_handlers("challenge") 

887 if not self.password_sent and self.has_password(): 

888 self.warn_and_quit(EXIT_NO_AUTHENTICATION, "the server did not request our password") 

889 return 

890 try: 

891 caps = typedict(packet[1]) 

892 netlog("processing hello from server: %s", ellipsizer(caps)) 

893 if not self.server_connection_established(caps): 

894 self.warn_and_quit(EXIT_FAILURE, "failed to establish connection") 

895 else: 

896 self.connection_established = True 

897 except Exception as e: 

898 netlog.info("error in hello packet", exc_info=True) 

899 self.warn_and_quit(EXIT_FAILURE, "error processing hello packet from server: %s" % e) 

900 

901 

902 def server_connection_established(self, caps : typedict) -> bool: 

903 netlog("server_connection_established(..)") 

904 if not self.parse_encryption_capabilities(caps): 

905 netlog("server_connection_established(..) failed encryption capabilities") 

906 return False 

907 if not self.parse_server_capabilities(caps): 

908 netlog("server_connection_established(..) failed server capabilities") 

909 return False 

910 if not self.parse_network_capabilities(caps): 

911 netlog("server_connection_established(..) failed network capabilities") 

912 return False 

913 netlog("server_connection_established(..) adding authenticated packet handlers") 

914 self.init_authenticated_packet_handlers() 

915 return True 

916 

917 

918 def parse_server_capabilities(self, caps : typedict) -> bool: 

919 for c in XpraClientBase.__bases__: 

920 if not c.parse_server_capabilities(self, caps): 

921 return False 

922 self.server_client_shutdown = caps.boolget("client-shutdown", True) 

923 self.server_compressors = caps.strtupleget("compressors", ("zlib",)) 

924 return True 

925 

926 def parse_network_capabilities(self, caps : typedict) -> bool: 

927 p = self._protocol 

928 if not p or not p.enable_encoder_from_caps(caps): 

929 return False 

930 p.enable_compressor_from_caps(caps) 

931 p.accept() 

932 p.parse_remote_caps(caps) 

933 return True 

934 

935 def parse_encryption_capabilities(self, caps : typedict) -> bool: 

936 p = self._protocol 

937 if not p: 

938 return False 

939 encryption = self.get_encryption() 

940 if encryption: 

941 #server uses a new cipher after second hello: 

942 key = self.get_encryption_key() 

943 assert key, "encryption key is missing" 

944 if not self.set_server_encryption(caps, key): 

945 return False 

946 return True 

947 

948 def _process_set_deflate(self, packet): 

949 #legacy, should not be used for anything 

950 pass 

951 

952 def _process_startup_complete(self, packet): 

953 #can be received if we connect with "xpra stop" or other command line client 

954 #as the server is starting up 

955 self.completed_startup = packet 

956 

957 

958 def _process_gibberish(self, packet): 

959 log("process_gibberish(%s)", ellipsizer(packet)) 

960 message, data = packet[1:3] 

961 p = self._protocol 

962 show_as_text = p and p.input_packetcount==0 and len(data)<128 and all(c in string.printable for c in bytestostr(data)) 

963 if show_as_text: 

964 #looks like the first packet back is just text, print it: 

965 data = bytestostr(data) 

966 if data.find("\n")>=0: 

967 for x in data.splitlines(): 

968 netlog.warn(x) 

969 else: 

970 netlog.error("Error: failed to connect, received") 

971 netlog.error(" %s", repr_ellipsized(data)) 

972 else: 

973 from xpra.net.socket_util import guess_packet_type 

974 packet_type = guess_packet_type(data) 

975 log("guess_packet_type(%r)=%s", data, packet_type) 

976 if packet_type and packet_type!="xpra": 

977 netlog.error("Error: received a %s packet", packet_type) 

978 netlog.error(" this is not an xpra server?") 

979 else: 

980 netlog.error("Error: received uninterpretable nonsense: %s", message) 

981 if p: 

982 netlog.error(" packet no %i data: %s", p.input_packetcount, repr_ellipsized(data)) 

983 else: 

984 netlog.error(" data: %s", repr_ellipsized(data)) 

985 self.quit(EXIT_PACKET_FAILURE) 

986 

987 def _process_invalid(self, packet): 

988 message, data = packet[1:3] 

989 netlog.info("Received invalid packet: %s", message) 

990 netlog(" data: %s", ellipsizer(data)) 

991 self.quit(EXIT_PACKET_FAILURE) 

992 

993 

994 ###################################################################### 

995 # packets: 

996 def remove_packet_handlers(self, *keys): 

997 for k in keys: 

998 for d in (self._packet_handlers, self._ui_packet_handlers): 

999 d.pop(k, None) 

1000 

1001 def init_packet_handlers(self): 

1002 self._packet_handlers = {} 

1003 self._ui_packet_handlers = {} 

1004 self.add_packet_handler("hello", self._process_hello, False) 

1005 self.add_packet_handlers({ 

1006 "challenge": self._process_challenge, 

1007 "disconnect": self._process_disconnect, 

1008 "set_deflate": self._process_set_deflate, 

1009 "startup-complete": self._process_startup_complete, 

1010 Protocol.CONNECTION_LOST: self._process_connection_lost, 

1011 Protocol.GIBBERISH: self._process_gibberish, 

1012 Protocol.INVALID: self._process_invalid, 

1013 }) 

1014 

1015 def init_authenticated_packet_handlers(self): 

1016 FilePrintMixin.init_authenticated_packet_handlers(self) 

1017 

1018 def add_packet_handlers(self, defs, main_thread=True): 

1019 for packet_type, handler in defs.items(): 

1020 self.add_packet_handler(packet_type, handler, main_thread) 

1021 

1022 def add_packet_handler(self, packet_type, handler, main_thread=True): 

1023 netlog("add_packet_handler%s", (packet_type, handler, main_thread)) 

1024 self.remove_packet_handlers(packet_type) 

1025 if main_thread: 

1026 handlers = self._ui_packet_handlers 

1027 else: 

1028 handlers = self._packet_handlers 

1029 handlers[packet_type] = handler 

1030 

1031 def process_packet(self, _proto, packet): 

1032 try: 

1033 handler = None 

1034 packet_type = packet[0] 

1035 if packet_type!=int: 

1036 packet_type = bytestostr(packet_type) 

1037 def call_handler(): 

1038 may_log_packet(False, packet_type, packet) 

1039 handler(packet) 

1040 handler = self._packet_handlers.get(packet_type) 

1041 if handler: 

1042 call_handler() 

1043 return 

1044 handler = self._ui_packet_handlers.get(packet_type) 

1045 if not handler: 

1046 netlog.error("unknown packet type: %s", packet_type) 

1047 return 

1048 self.idle_add(call_handler) 

1049 except Exception: 

1050 netlog.error("Unhandled error while processing a '%s' packet from peer using %s", 

1051 packet_type, handler, exc_info=True)