Coverage for /home/antoine/projects/xpra-git/dist/python3/lib64/python/xpra/client/client_base.py : 64%
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.
7import os
8import sys
9import uuid
10import signal
11import socket
12import string
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)
50log = Logger("client")
51netlog = Logger("network")
52authlog = Logger("auth")
53mouselog = Logger("mouse")
54cryptolog = Logger("crypto")
55bandwidthlog = Logger("bandwidth")
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)
66def noop():
67 pass
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):
81 INSTALL_SIGNAL_HANDLERS = True
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
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()
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()
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)
182 def cancel_progress_timer(self):
183 pt = self.progress_timer
184 if pt:
185 self.progress_timer = None
186 self.source_remove(pt)
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)
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
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)
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)
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)
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)
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)
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
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)
318 def exit(self):
319 log("XpraClientBase.exit() calling %s", sys.exit)
320 sys.exit()
323 def client_type(self) -> str:
324 #overriden in subclasses!
325 return "Python"
327 def get_scheduler(self):
328 raise NotImplementedError()
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
359 def _process_udp_control(self, packet):
360 #send it back to the protocol object:
361 self._protocol.process_control(*packet[1:])
364 def init_aliases(self):
365 i = 1
366 for key in PACKET_TYPES:
367 self._aliases[i] = key
368 i += 1
370 def has_password(self) -> bool:
371 return self.password or self.password_file or os.environ.get('XPRA_PASSWORD')
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)
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")
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
463 def get_version_info(self) -> dict:
464 return get_version_info()
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
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)
497 def send(self, *parts):
498 self._ordinary_packets.append(parts)
499 self.have_more()
501 def send_now(self, *parts):
502 self._priority_packets.append(parts)
503 self.have_more()
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()
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()
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()
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)
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
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
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()
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)
598 def run(self):
599 self.start_protocol()
601 def start_protocol(self):
602 #protocol may be None in "listen" mode
603 if self._protocol:
604 self._protocol.start()
606 def quit(self, exit_code=0):
607 raise Exception("override me!")
609 def warn_and_quit(self, exit_code, message):
610 log.warn(message)
611 self.quit(exit_code)
614 def send_shutdown_server(self):
615 assert self.server_client_shutdown
616 self.send("shutdown-server")
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)
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)
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)
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")
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)
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)
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
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)
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
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
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))
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)
814 def do_send_challenge_reply(self, challenge_response, client_salt):
815 self.password_sent = True
816 self.send_hello(challenge_response, client_salt)
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
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
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")
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)
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
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
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
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
948 def _process_set_deflate(self, packet):
949 #legacy, should not be used for anything
950 pass
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
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)
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)
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)
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 })
1015 def init_authenticated_packet_handlers(self):
1016 FilePrintMixin.init_authenticated_packet_handlers(self)
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)
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
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)