Coverage for /home/antoine/projects/xpra-git/dist/python3/lib64/python/xpra/net/ssh.py : 8%
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) 2018-2020 Antoine Martin <antoine@xpra.org>
3# Xpra is released under the terms of the GNU GPL v2, or, at your option, any
4# later version. See the file COPYING for details.
6import sys
7import os
8import shlex
9import socket
10import signal
11from time import sleep
12from subprocess import PIPE, Popen
14from xpra.scripts.main import InitException, InitExit, shellquote, host_target_string
15from xpra.platform.paths import get_ssh_known_hosts_files
16from xpra.platform import get_username
17from xpra.scripts.config import parse_bool
18from xpra.net.bytestreams import SocketConnection, SOCKET_TIMEOUT, ConnectionClosedException
19from xpra.make_thread import start_thread
20from xpra.exit_codes import (
21 EXIT_SSH_KEY_FAILURE, EXIT_SSH_FAILURE,
22 EXIT_CONNECTION_FAILED,
23 )
24from xpra.os_util import (
25 bytestostr, osexpand, load_binary_file, monotonic_time,
26 nomodule_context, umask_context, is_main_thread,
27 WIN32, OSX, POSIX,
28 )
29from xpra.util import envint, envbool, envfloat, engs, csv
30from xpra.log import Logger, is_debug_enabled
32log = Logger("network", "ssh")
34INITENV_COMMAND = os.environ.get("XPRA_INITENV_COMMAND", "") #"xpra initenv"
35WINDOW_SIZE = envint("XPRA_SSH_WINDOW_SIZE", 2**27-1)
36TIMEOUT = envint("XPRA_SSH_TIMEOUT", 60)
37SKIP_UI = envbool("XPRA_SKIP_UI", False)
39VERIFY_HOSTKEY = envbool("XPRA_SSH_VERIFY_HOSTKEY", True)
40VERIFY_STRICT = envbool("XPRA_SSH_VERIFY_STRICT", False)
41ADD_KEY = envbool("XPRA_SSH_ADD_KEY", True)
42#which authentication mechanisms are enabled with paramiko:
43NONE_AUTH = envbool("XPRA_SSH_NONE_AUTH", True)
44PASSWORD_AUTH = envbool("XPRA_SSH_PASSWORD_AUTH", True)
45AGENT_AUTH = envbool("XPRA_SSH_AGENT_AUTH", True)
46KEY_AUTH = envbool("XPRA_SSH_KEY_AUTH", True)
47PASSWORD_AUTH = envbool("XPRA_SSH_PASSWORD_AUTH", True)
48PASSWORD_RETRY = envint("XPRA_SSH_PASSWORD_RETRY", 2)
49assert PASSWORD_RETRY>=0
50MAGIC_QUOTES = envbool("XPRA_SSH_MAGIC_QUOTES", True)
51TEST_COMMAND_TIMEOUT = envint("XPRA_SSH_TEST_COMMAND_TIMEOUT", 10)
52EXEC_STDOUT_TIMEOUT = envfloat("XPRA_SSH_EXEC_STDOUT_TIMEOUT", 2)
53EXEC_STDERR_TIMEOUT = envfloat("XPRA_SSH_EXEC_STDERR_TIMEOUT", 0)
56def keymd5(k) -> str:
57 import binascii
58 f = bytestostr(binascii.hexlify(k.get_fingerprint()))
59 s = "MD5"
60 while f:
61 s += ":"+f[:2]
62 f = f[2:]
63 return s
66def force_focus():
67 from xpra.platform.gui import force_focus as _force_focus
68 _force_focus()
70def dialog_run(dialog) -> int:
71 from gi.repository import GLib
72 if is_main_thread():
73 force_focus()
74 dialog.show()
75 try:
76 return dialog.run()
77 finally:
78 dialog.destroy()
79 #do a little dance if we're not running in the main thread:
80 #block this thread and wait for the main thread to run the dialog
81 from threading import Event
82 e = Event()
83 code = []
84 def main_thread_run():
85 force_focus()
86 dialog.show()
87 try:
88 r = dialog.run()
89 finally:
90 dialog.destroy()
91 code.append(r)
92 e.set()
93 GLib.idle_add(main_thread_run)
94 e.wait()
95 log("dialog_run(%s) code=%s", dialog, code)
96 return code[0]
98def dialog_pass(title="Password Input", prompt="enter password", icon="") -> str:
99 from xpra.client.gtk_base.pass_dialog import PasswordInputDialogWindow
100 dialog = PasswordInputDialogWindow(title, prompt, icon)
101 try:
102 if dialog_run(dialog)==0:
103 return dialog.get_password()
104 return None
105 finally:
106 dialog.destroy()
108def dialog_confirm(title, prompt, qinfo=(), icon="", buttons=(("OK", 1),)) -> int:
109 from xpra.client.gtk_base.confirm_dialog import ConfirmDialogWindow
110 dialog = ConfirmDialogWindow(title, prompt, qinfo, icon, buttons)
111 try:
112 r = dialog_run(dialog)
113 finally:
114 dialog.destroy()
115 return r
118def confirm_key(info=()) -> bool:
119 if SKIP_UI:
120 return False
121 from xpra.platform.paths import get_icon_filename
122 from xpra.os_util import use_tty
123 if not use_tty():
124 icon = get_icon_filename("authentication", "png") or ""
125 prompt = "Are you sure you want to continue connecting?"
126 code = dialog_confirm("Confirm Key", prompt, info, icon, buttons=[("yes", 200), ("NO", 201)])
127 log("dialog return code=%s", code)
128 r = code==200
129 log.info("host key %sconfirmed", ["not ", ""][r])
130 return r
131 log("confirm_key(%r) will use stdin prompt", info)
132 prompt = "Are you sure you want to continue connecting (yes/NO)? "
133 sys.stderr.write(os.linesep.join(info)+os.linesep+prompt)
134 try:
135 v = sys.stdin.readline().rstrip(os.linesep)
136 except KeyboardInterrupt:
137 sys.exit(128+signal.SIGINT)
138 return v and v.lower() in ("y", "yes")
140def input_pass(prompt) -> str:
141 if SKIP_UI:
142 return None
143 from xpra.platform.paths import get_icon_filename
144 from xpra.os_util import use_tty
145 if not use_tty():
146 icon = get_icon_filename("authentication", "png") or ""
147 return dialog_pass("Password Input", prompt, icon)
148 from getpass import getpass
149 try:
150 return getpass(prompt)
151 except KeyboardInterrupt:
152 sys.exit(128+signal.SIGINT)
155class SSHSocketConnection(SocketConnection):
157 def __init__(self, ssh_channel, sock, sockname, peername, target, info=None, socket_options=None):
158 self._raw_socket = sock
159 super().__init__(ssh_channel, sockname, peername, target, "ssh", info, socket_options)
161 def get_raw_socket(self):
162 return self._raw_socket
164 def start_stderr_reader(self):
165 start_thread(self._stderr_reader, "ssh-stderr-reader", daemon=True)
167 def _stderr_reader(self):
168 #stderr = self._socket.makefile_stderr(mode="rb", bufsize=1)
169 chan = self._socket
170 stderr = chan.makefile_stderr("rb", 1)
171 while self.active:
172 v = stderr.readline()
173 if not v:
174 log.info("SSH EOF on stderr of %s", chan.get_name())
175 break
176 s = bytestostr(v.rstrip(b"\n\r"))
177 if s:
178 log.info(" SSH: %r", s)
180 def peek(self, n):
181 if not self._raw_socket:
182 return None
183 return self._raw_socket.recv(n, socket.MSG_PEEK)
185 def get_socket_info(self) -> dict:
186 if not self._raw_socket:
187 return {}
188 return self.do_get_socket_info(self._raw_socket)
190 def get_info(self) -> dict:
191 i = SocketConnection.get_info(self)
192 s = self._socket
193 if s:
194 i["ssh-channel"] = {
195 "id" : s.get_id(),
196 "name" : s.get_name(),
197 }
198 return i
201class SSHProxyCommandConnection(SSHSocketConnection):
202 def __init__(self, ssh_channel, peername, target, info):
203 super().__init__(ssh_channel, None, None, peername, target, info)
204 self.process = None
206 def error_is_closed(self, e) -> bool:
207 p = self.process
208 if p:
209 #if the process has terminated,
210 #then the connection must be closed:
211 if p[0].poll() is not None:
212 return True
213 return super().error_is_closed(e)
215 def get_socket_info(self) -> dict:
216 p = self.process
217 if not p:
218 return {}
219 proc, _ssh, cmd = p
220 return {
221 "process" : {
222 "pid" : proc.pid,
223 "returncode": proc.returncode,
224 "command" : cmd,
225 }
226 }
228 def close(self):
229 try:
230 super().close()
231 except Exception:
232 #this can happen if the proxy command gets a SIGINT,
233 #it's closed already and we don't care
234 log("SSHProxyCommandConnection.close()", exc_info=True)
237def ssh_paramiko_connect_to(display_desc):
238 #plain socket attributes:
239 dtype = display_desc["type"]
240 host = display_desc["host"]
241 port = display_desc.get("ssh-port", 22)
242 #ssh and command attributes:
243 username = display_desc.get("username") or get_username()
244 if "proxy_host" in display_desc:
245 display_desc.setdefault("proxy_username", get_username())
246 password = display_desc.get("password")
247 remote_xpra = display_desc["remote_xpra"]
248 proxy_command = display_desc["proxy_command"] #ie: "_proxy_start"
249 socket_dir = display_desc.get("socket_dir")
250 display = display_desc.get("display")
251 display_as_args = display_desc["display_as_args"] #ie: "--start=xterm :10"
252 paramiko_config = display_desc.copy()
253 paramiko_config.update(display_desc.get("paramiko-config", {}))
254 socket_info = {
255 "host" : host,
256 "port" : port,
257 }
258 def get_keyfiles(host_config, config_name="key"):
259 keyfiles = (host_config or {}).get("identityfile") or get_default_keyfiles()
260 keyfile = paramiko_config.get(config_name)
261 if keyfile:
262 keyfiles.insert(0, keyfile)
263 return keyfiles
265 with nogssapi_context():
266 from paramiko import SSHConfig, ProxyCommand
267 ssh_config = SSHConfig()
268 user_config_file = os.path.expanduser("~/.ssh/config")
269 sock = None
270 host_config = None
271 if os.path.exists(user_config_file):
272 with open(user_config_file) as f:
273 ssh_config.parse(f)
274 log("parsed user config '%s'", user_config_file)
275 try:
276 log("%i hosts found", len(ssh_config.get_hostnames()))
277 except KeyError:
278 pass
279 host_config = ssh_config.lookup(host)
280 if host_config:
281 log("got host config for '%s': %s", host, host_config)
282 chost = host_config.get("hostname", host)
283 cusername = host_config.get("user", username)
284 cport = host_config.get("port", port)
285 try:
286 port = int(cport)
287 except (TypeError, ValueError):
288 raise InitExit(EXIT_SSH_FAILURE, "invalid ssh port specified: '%s'" % cport) from None
289 proxycommand = host_config.get("proxycommand")
290 if proxycommand:
291 log("found proxycommand='%s' for host '%s'", proxycommand, chost)
292 sock = ProxyCommand(proxycommand)
293 log("ProxyCommand(%s)=%s", proxycommand, sock)
294 from xpra.child_reaper import getChildReaper
295 cmd = getattr(sock, "cmd", [])
296 def proxycommand_ended(proc):
297 log("proxycommand_ended(%s) exit code=%s", proc, proc.poll())
298 getChildReaper().add_process(sock.process, "paramiko-ssh-client", cmd, True, True,
299 callback=proxycommand_ended)
300 proxy_keys = get_keyfiles(host_config, "proxy_key")
301 log("proxy keys=%s", proxy_keys)
302 from paramiko.client import SSHClient
303 ssh_client = SSHClient()
304 ssh_client.load_system_host_keys()
305 log("ssh proxy command connect to %s", (chost, cport, sock))
306 ssh_client.connect(chost, cport, sock=sock)
307 transport = ssh_client.get_transport()
308 do_ssh_paramiko_connect_to(transport, chost,
309 cusername, password,
310 host_config or ssh_config.lookup("*"),
311 proxy_keys,
312 paramiko_config)
313 chan = paramiko_run_remote_xpra(transport, proxy_command, remote_xpra, socket_dir, display_as_args)
314 peername = (chost, cport)
315 conn = SSHProxyCommandConnection(chan, peername, peername, socket_info)
316 conn.target = host_target_string("ssh", cusername, chost, port, display)
317 conn.timeout = SOCKET_TIMEOUT
318 conn.start_stderr_reader()
319 conn.process = (sock.process, "ssh", cmd)
320 from xpra.net import bytestreams
321 from paramiko.ssh_exception import ProxyCommandFailure
322 bytestreams.CLOSED_EXCEPTIONS = tuple(list(bytestreams.CLOSED_EXCEPTIONS)+[ProxyCommandFailure])
323 return conn
325 keys = get_keyfiles(host_config)
326 from xpra.scripts.main import socket_connect
327 from paramiko.transport import Transport
328 from paramiko import SSHException
329 if "proxy_host" in display_desc:
330 proxy_host = display_desc["proxy_host"]
331 proxy_port = display_desc.get("proxy_port", 22)
332 proxy_username = display_desc.get("proxy_username", username)
333 proxy_password = display_desc.get("proxy_password", password)
334 proxy_keys = get_keyfiles(host_config, "proxy_key")
335 sock = socket_connect(dtype, proxy_host, proxy_port)
336 middle_transport = Transport(sock)
337 middle_transport.use_compression(False)
338 try:
339 middle_transport.start_client()
340 except SSHException as e:
341 log("start_client()", exc_info=True)
342 raise InitExit(EXIT_SSH_FAILURE, "SSH negotiation failed: %s" % e) from None
343 proxy_host_config = ssh_config.lookup(host)
344 do_ssh_paramiko_connect_to(middle_transport, proxy_host,
345 proxy_username, proxy_password,
346 proxy_host_config or ssh_config.lookup("*"),
347 proxy_keys,
348 paramiko_config)
349 log("Opening proxy channel")
350 chan_to_middle = middle_transport.open_channel("direct-tcpip", (host, port), ('localhost', 0))
352 transport = Transport(chan_to_middle)
353 transport.use_compression(False)
354 try:
355 transport.start_client()
356 except SSHException as e:
357 log("start_client()", exc_info=True)
358 raise InitExit(EXIT_SSH_FAILURE, "SSH negotiation failed: %s" % e)
359 do_ssh_paramiko_connect_to(transport, host,
360 username, password,
361 host_config or ssh_config.lookup("*"),
362 keys,
363 paramiko_config)
364 chan = paramiko_run_remote_xpra(transport, proxy_command, remote_xpra, socket_dir, display_as_args)
365 peername = (host, port)
366 conn = SSHProxyCommandConnection(chan, peername, peername, socket_info)
367 conn.target = "%s via %s" % (
368 host_target_string("ssh", username, host, port, display),
369 host_target_string("ssh", proxy_username, proxy_host, proxy_port, None),
370 )
371 conn.timeout = SOCKET_TIMEOUT
372 conn.start_stderr_reader()
373 return conn
375 #plain TCP connection to the server,
376 #we open it then give the socket to paramiko:
377 sock = socket_connect(dtype, host, port)
378 sockname = sock.getsockname()
379 peername = sock.getpeername()
380 log("paramiko socket_connect: sockname=%s, peername=%s", sockname, peername)
381 transport = Transport(sock)
382 transport.use_compression(False)
383 try:
384 transport.start_client()
385 except SSHException as e:
386 log("start_client()", exc_info=True)
387 raise InitExit(EXIT_SSH_FAILURE, "SSH negotiation failed: %s" % e) from None
388 do_ssh_paramiko_connect_to(transport, host, username, password,
389 host_config or ssh_config.lookup("*"),
390 keys,
391 paramiko_config)
392 chan = paramiko_run_remote_xpra(transport, proxy_command, remote_xpra, socket_dir, display_as_args)
393 conn = SSHSocketConnection(chan, sock, sockname, peername, (host, port), socket_info)
394 conn.target = host_target_string("ssh", username, host, port, display)
395 conn.timeout = SOCKET_TIMEOUT
396 conn.start_stderr_reader()
397 return conn
400#workaround incompatibility between paramiko and gssapi:
401class nogssapi_context(nomodule_context):
403 def __init__(self):
404 super().__init__("gssapi")
407def get_default_keyfiles():
408 dkf = os.environ.get("XPRA_SSH_DEFAULT_KEYFILES", None)
409 if dkf is not None:
410 return [x for x in dkf.split(":") if x]
411 return [osexpand(os.path.join("~/", ".ssh", keyfile)) for keyfile in ("id_ed25519", "id_ecdsa", "id_rsa", "id_dsa")]
414def do_ssh_paramiko_connect_to(transport, host, username, password, host_config=None, keyfiles=None, paramiko_config=None):
415 from paramiko import SSHException, PasswordRequiredException
416 from paramiko.agent import Agent
417 from paramiko.hostkeys import HostKeys
418 log("do_ssh_paramiko_connect_to%s", (transport, host, username, password, host_config, keyfiles, paramiko_config))
419 log("SSH transport %s", transport)
421 def configvalue(key):
422 #if the paramiko config has a setting, honour it:
423 if paramiko_config and key in paramiko_config:
424 return paramiko_config.get(key)
425 #fallback to the value from the host config:
426 return (host_config or {}).get(key)
427 def configbool(key, default_value=True):
428 return parse_bool(key, configvalue(key), default_value)
429 def configint(key, default_value=0):
430 v = configvalue(key)
431 if v is None:
432 return default_value
433 return int(v)
435 host_key = transport.get_remote_server_key()
436 assert host_key, "no remote server key"
437 log("remote_server_key=%s", keymd5(host_key))
438 if configbool("verify-hostkey", VERIFY_HOSTKEY):
439 host_keys = HostKeys()
440 host_keys_filename = None
441 KNOWN_HOSTS = get_ssh_known_hosts_files()
442 for known_hosts in KNOWN_HOSTS:
443 host_keys.clear()
444 try:
445 path = os.path.expanduser(known_hosts)
446 if os.path.exists(path):
447 host_keys.load(path)
448 log("HostKeys.load(%s) successful", path)
449 host_keys_filename = path
450 break
451 except IOError:
452 log("HostKeys.load(%s)", known_hosts, exc_info=True)
454 log("host keys=%s", host_keys)
455 keys = host_keys.lookup(host)
456 known_host_key = (keys or {}).get(host_key.get_name())
457 def keyname():
458 return host_key.get_name().replace("ssh-", "")
459 if host_key==known_host_key:
460 assert host_key
461 log("%s host key '%s' OK for host '%s'", keyname(), keymd5(host_key), host)
462 else:
463 dnscheck = ""
464 if configbool("verifyhostkeydns"):
465 try:
466 from xpra.net.sshfp import check_host_key
467 dnscheck = check_host_key(host, host_key)
468 except ImportError as e:
469 log("verifyhostkeydns failed", exc_info=True)
470 log.warn("Warning: cannot check SSHFP DNS records")
471 log.warn(" %s", e)
472 log("dnscheck=%s", dnscheck)
473 def adddnscheckinfo(q):
474 if dnscheck is not True:
475 if dnscheck:
476 q += [
477 "SSHFP validation failed:",
478 dnscheck
479 ]
480 else:
481 q += [
482 "SSHFP validation failed"
483 ]
484 if dnscheck is True:
485 #DNSSEC provided a matching record
486 log.info("found a valid SSHFP record for host %s", host)
487 elif known_host_key:
488 log.warn("Warning: SSH server key mismatch")
489 qinfo = [
490"WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED!",
491"IT IS POSSIBLE THAT SOMEONE IS DOING SOMETHING NASTY!",
492"Someone could be eavesdropping on you right now (man-in-the-middle attack)!",
493"It is also possible that a host key has just been changed.",
494"The fingerprint for the %s key sent by the remote host is" % keyname(),
495keymd5(host_key),
496]
497 adddnscheckinfo(qinfo)
498 if configbool("stricthostkeychecking", VERIFY_STRICT):
499 log.warn("Host key verification failed.")
500 #TODO: show alert with no option to accept key
501 qinfo += [
502 "Please contact your system administrator.",
503 "Add correct host key in %s to get rid of this message.",
504 "Offending %s key in %s" % (keyname(), host_keys_filename),
505 "ECDSA host key for %s has changed and you have requested strict checking." % keyname(),
506 ]
507 sys.stderr.write(os.linesep.join(qinfo))
508 transport.close()
509 raise InitExit(EXIT_SSH_KEY_FAILURE, "SSH Host key has changed")
510 if not confirm_key(qinfo):
511 transport.close()
512 raise InitExit(EXIT_SSH_KEY_FAILURE, "SSH Host key has changed")
514 else:
515 assert (not keys) or (host_key.get_name() not in keys)
516 if not keys:
517 log.warn("Warning: unknown SSH host")
518 else:
519 log.warn("Warning: unknown %s SSH host key", keyname())
520 qinfo = [
521 "The authenticity of host '%s' can't be established." % (host,),
522 "%s key fingerprint is" % keyname(),
523 keymd5(host_key),
524 ]
525 adddnscheckinfo(qinfo)
526 if not confirm_key(qinfo):
527 transport.close()
528 raise InitExit(EXIT_SSH_KEY_FAILURE, "Unknown SSH host '%s'" % host)
530 if configbool("addkey", ADD_KEY):
531 try:
532 if not host_keys_filename:
533 #the first one is the default,
534 #ie: ~/.ssh/known_hosts on posix
535 host_keys_filename = os.path.expanduser(KNOWN_HOSTS[0])
536 log("adding %s key for host '%s' to '%s'", keyname(), host, host_keys_filename)
537 if not os.path.exists(host_keys_filename):
538 keys_dir = os.path.dirname(host_keys_filename)
539 if not os.path.exists(keys_dir):
540 log("creating keys directory '%s'", keys_dir)
541 os.mkdir(keys_dir, 0o700)
542 elif not os.path.isdir(keys_dir):
543 log.warn("Warning: '%s' is not a directory")
544 log.warn(" key not saved")
545 if os.path.exists(keys_dir) and os.path.isdir(keys_dir):
546 log("creating known host file '%s'", host_keys_filename)
547 with umask_context(0o133):
548 with open(host_keys_filename, 'a+'):
549 pass
550 host_keys.add(host, host_key.get_name(), host_key)
551 host_keys.save(host_keys_filename)
552 except OSError as e:
553 log("failed to add key to '%s'", host_keys_filename)
554 log.error("Error adding key to '%s'", host_keys_filename)
555 log.error(" %s", e)
556 except Exception as e:
557 log.error("cannot add key", exc_info=True)
558 else:
559 log("ssh host key verification skipped")
562 def auth_agent():
563 agent = Agent()
564 agent_keys = agent.get_keys()
565 log("agent keys: %s", agent_keys)
566 if agent_keys:
567 for agent_key in agent_keys:
568 log("trying ssh-agent key '%s'", keymd5(agent_key))
569 try:
570 transport.auth_publickey(username, agent_key)
571 if transport.is_authenticated():
572 log("authenticated using agent and key '%s'", keymd5(agent_key))
573 break
574 except SSHException:
575 log("agent key '%s' rejected", keymd5(agent_key), exc_info=True)
576 if not transport.is_authenticated():
577 log.info("agent authentication failed, tried %i key%s", len(agent_keys), engs(agent_keys))
579 def auth_publickey():
580 log("trying public key authentication using %s", keyfiles)
581 for keyfile_path in keyfiles:
582 if not os.path.exists(keyfile_path):
583 log("no keyfile at '%s'", keyfile_path)
584 continue
585 log("trying '%s'", keyfile_path)
586 key = None
587 import paramiko
588 for pkey_classname in ("RSA", "DSS", "ECDSA", "Ed25519"):
589 pkey_class = getattr(paramiko, "%sKey" % pkey_classname, None)
590 if pkey_class is None:
591 log("no %s key type", pkey_classname)
592 continue
593 log("trying to load as %s", pkey_classname)
594 try:
595 key = pkey_class.from_private_key_file(keyfile_path)
596 log.info("loaded %s private key from '%s'", pkey_classname, keyfile_path)
597 break
598 except PasswordRequiredException as e:
599 log("%s keyfile requires a passphrase; %s", keyfile_path, e)
600 passphrase = input_pass("please enter the passphrase for %s:" % (keyfile_path,))
601 if passphrase:
602 try:
603 key = pkey_class.from_private_key_file(keyfile_path, passphrase)
604 log.info("loaded %s private key from '%s'", pkey_classname, keyfile_path)
605 except SSHException as e:
606 log("from_private_key_file", exc_info=True)
607 log.info("cannot load key from file '%s':", keyfile_path)
608 log.info(" %s", e)
609 break
610 except Exception as e:
611 log("auth_publickey() loading as %s", pkey_classname, exc_info=True)
612 key_data = load_binary_file(keyfile_path)
613 if key_data and key_data.find(b"BEGIN OPENSSH PRIVATE KEY")>=0 and paramiko.__version__<"2.7":
614 log.warn("Warning: private key '%s'", keyfile_path)
615 log.warn(" this file seems to be using OpenSSH's own format")
616 log.warn(" please convert it to something more standard (ie: PEM)")
617 log.warn(" so it can be used with the paramiko backend")
618 log.warn(" or switch to the OpenSSH backend with '--ssh=ssh'")
619 if key:
620 log("auth_publickey using %s as %s: %s", keyfile_path, pkey_classname, keymd5(key))
621 try:
622 transport.auth_publickey(username, key)
623 except SSHException as e:
624 log("key '%s' rejected", keyfile_path, exc_info=True)
625 log.info("SSH authentication using key '%s' failed:", keyfile_path)
626 log.info(" %s", e)
627 else:
628 if transport.is_authenticated():
629 break
630 else:
631 log.error("Error: cannot load private key '%s'", keyfile_path)
633 def auth_none():
634 log("trying none authentication")
635 try:
636 transport.auth_none(username)
637 except SSHException:
638 log("auth_none()", exc_info=True)
640 def auth_password():
641 log("trying password authentication")
642 try:
643 transport.auth_password(username, password)
644 except SSHException as e:
645 log("auth_password(..)", exc_info=True)
646 log.info("SSH password authentication failed:")
647 log.info(" %s", getattr(e, "message", e))
649 def auth_interactive():
650 log("trying interactive authentication")
651 class iauthhandler:
652 def __init__(self):
653 self.authcount = 0
654 def handlestuff(self, _title, _instructions, prompt_list):
655 p = []
656 for pent in prompt_list:
657 if self.authcount==0 and password:
658 p.append(password)
659 else:
660 p.append(input_pass(pent[0]))
661 self.authcount += 1
662 return p
663 try:
664 myiauthhandler = iauthhandler()
665 transport.auth_interactive(username, myiauthhandler.handlestuff, "")
666 except SSHException as e:
667 log("auth_interactive(..)", exc_info=True)
668 log.info("SSH password authentication failed:")
669 log.info(" %s", getattr(e, "message", e))
671 banner = transport.get_banner()
672 if banner:
673 log.info("SSH server banner:")
674 for x in banner.splitlines():
675 log.info(" %s", x)
677 if paramiko_config and "auth" in paramiko_config:
678 auth = paramiko_config.get("auth", "").split("+")
679 AUTH_OPTIONS = ("none", "agent", "key", "password")
680 if any(a for a in auth if a not in AUTH_OPTIONS):
681 raise InitExit(EXIT_SSH_FAILURE, "invalid ssh authentication module specified: %s" %
682 csv(a for a in auth if a not in AUTH_OPTIONS))
683 else:
684 auth = []
685 if configbool("noneauthentication", NONE_AUTH):
686 auth.append("none")
687 if password and configbool("passwordauthentication", PASSWORD_AUTH):
688 auth.append("password")
689 if configbool("agentauthentication", AGENT_AUTH):
690 auth.append("agent")
691 # Some people do two-factor using KEY_AUTH to kick things off, so this happens first
692 if configbool("keyauthentication", KEY_AUTH):
693 auth.append("key")
694 if not password and configbool("passwordauthentication", PASSWORD_AUTH):
695 auth.append("password")
696 #def doauth(authtype):
697 # return authtype in auth and not transport.is_authenticated()
699 log("starting authentication, authentication methods: %s", auth)
700 # per the RFC we probably should do none first always and read off the supported
701 # methods, however, the current code seems to work fine with OpenSSH
702 while not transport.is_authenticated() and auth:
703 a = auth.pop(0)
704 log("auth=%s", a)
705 if a=="none":
706 auth_none()
707 elif a=="agent":
708 auth_agent()
709 elif a=="key":
710 auth_publickey()
711 elif a=="password":
712 auth_interactive()
713 if not transport.is_authenticated():
714 if password:
715 auth_password()
716 else:
717 tries = configint("numberofpasswordprompts", PASSWORD_RETRY)
718 for _ in range(tries):
719 password = input_pass("please enter the SSH password for %s@%s:" % (username, host))
720 if not password:
721 break
722 auth_password()
723 if transport.is_authenticated():
724 break
725 if not transport.is_authenticated():
726 transport.close()
727 raise InitExit(EXIT_CONNECTION_FAILED, "SSH Authentication on %s failed" % host)
730def paramiko_run_test_command(transport, cmd):
731 from paramiko import SSHException
732 log("paramiko_run_test_command(transport, %r)", cmd)
733 try:
734 chan = transport.open_session(window_size=None, max_packet_size=0, timeout=60)
735 chan.set_name("find %s" % cmd)
736 except SSHException as e:
737 log("open_session", exc_info=True)
738 raise InitExit(EXIT_SSH_FAILURE, "failed to open SSH session: %s" % e) from None
739 chan.exec_command(cmd)
740 log("exec_command returned")
741 start = monotonic_time()
742 while not chan.exit_status_ready():
743 if monotonic_time()-start>TEST_COMMAND_TIMEOUT:
744 chan.close()
745 raise InitException("SSH test command '%s' timed out" % cmd)
746 log("exit status is not ready yet, sleeping")
747 sleep(0.01)
748 code = chan.recv_exit_status()
749 log("exec_command('%s')=%s", cmd, code)
750 def chan_read(read_fn):
751 try:
752 return read_fn()
753 except socket.error:
754 log("chan_read(%s)", read_fn, exc_info=True)
755 return b""
756 #don't wait too long for the data:
757 chan.settimeout(EXEC_STDOUT_TIMEOUT)
758 out = chan_read(chan.makefile().readline)
759 log("exec_command out=%r", out)
760 chan.settimeout(EXEC_STDERR_TIMEOUT)
761 err = chan_read(chan.makefile_stderr().readline)
762 log("exec_command err=%r", err)
763 chan.close()
764 return out, err, code
766def paramiko_run_remote_xpra(transport, xpra_proxy_command=None, remote_xpra=None, socket_dir=None, display_as_args=None):
767 from paramiko import SSHException
768 assert remote_xpra
769 log("will try to run xpra from: %s", remote_xpra)
770 def rtc(cmd):
771 return paramiko_run_test_command(transport, cmd)
772 tried = set()
773 for xpra_cmd in remote_xpra:
774 if xpra_cmd.lower() in ("xpra.exe", "xpra_cmd.exe"):
775 #win32 mode, quick and dirty platform test first:
776 r = rtc("ver")
777 if r[2]!=0:
778 continue
779 #let's find where xpra is installed:
780 r = rtc("FOR /F \"usebackq tokens=3*\" %A IN (`REG QUERY \"HKEY_LOCAL_MACHINE\\Software\\Xpra\" /v InstallPath`) DO (echo %A %B)") #pylint: disable=line-too-long
781 if r[2]==0:
782 #found in registry:
783 lines = r[0].splitlines()
784 installpath = bytestostr(lines[-1])
785 xpra_cmd = "%s\\%s" % (installpath, xpra_cmd)
786 xpra_cmd = xpra_cmd.replace("\\", "\\\\")
787 log("using '%s'", xpra_cmd)
788 elif xpra_cmd.endswith(".exe"):
789 #assume this path exists
790 pass
791 else:
792 #assume Posix and find that command:
793 r = rtc("which %s" % xpra_cmd)
794 if r[2]!=0:
795 continue
796 if r[0]:
797 #use the actual path returned by 'which':
798 try:
799 xpra_cmd = r[0].decode().rstrip("\n\r")
800 except:
801 pass
802 if xpra_cmd in tried:
803 continue
804 tried.add(xpra_cmd)
805 cmd = '"' + xpra_cmd + '" ' + ' '.join(shellquote(x) for x in xpra_proxy_command)
806 if socket_dir:
807 cmd += " \"--socket-dir=%s\"" % socket_dir
808 if display_as_args:
809 cmd += " "
810 cmd += " ".join(shellquote(x) for x in display_as_args)
811 log("cmd(%s, %s)=%s", xpra_proxy_command, display_as_args, cmd)
813 #see https://github.com/paramiko/paramiko/issues/175
814 #WINDOW_SIZE = 2097152
815 log("trying to open SSH session, window-size=%i, timeout=%i", WINDOW_SIZE, TIMEOUT)
816 try:
817 chan = transport.open_session(window_size=WINDOW_SIZE, max_packet_size=0, timeout=TIMEOUT)
818 chan.set_name("run-xpra")
819 except SSHException as e:
820 log("open_session", exc_info=True)
821 raise InitExit(EXIT_SSH_FAILURE, "failed to open SSH session: %s" % e) from None
822 else:
823 log("channel exec_command(%s)" % cmd)
824 chan.exec_command(cmd)
825 return chan
826 raise Exception("all SSH remote proxy commands have failed - is xpra installed on the remote host?")
829def ssh_connect_failed(_message):
830 #by the time ssh fails, we may have entered the gtk main loop
831 #(and more than once thanks to the clipboard code..)
832 if "gi.repository.Gtk" in sys.modules:
833 from gi.repository import Gtk
834 Gtk.main_quit()
837def ssh_exec_connect_to(display_desc, opts=None, debug_cb=None, ssh_fail_cb=ssh_connect_failed):
838 if not ssh_fail_cb:
839 ssh_fail_cb = ssh_connect_failed
840 sshpass_command = None
841 try:
842 cmd = list(display_desc["full_ssh"])
843 kwargs = {}
844 env = display_desc.get("env")
845 kwargs["stderr"] = sys.stderr
846 if WIN32:
847 from subprocess import CREATE_NEW_PROCESS_GROUP, CREATE_NEW_CONSOLE, STARTUPINFO, STARTF_USESHOWWINDOW
848 startupinfo = STARTUPINFO()
849 startupinfo.dwFlags |= STARTF_USESHOWWINDOW
850 startupinfo.wShowWindow = 0 #aka win32.con.SW_HIDE
851 flags = CREATE_NEW_PROCESS_GROUP | CREATE_NEW_CONSOLE
852 kwargs.update({
853 "startupinfo" : startupinfo,
854 "creationflags" : flags,
855 "stderr" : PIPE,
856 })
857 elif not display_desc.get("exit_ssh", False) and not OSX:
858 kwargs["start_new_session"] = True
859 remote_xpra = display_desc["remote_xpra"]
860 assert remote_xpra
861 socket_dir = display_desc.get("socket_dir")
862 proxy_command = display_desc["proxy_command"] #ie: "_proxy_start"
863 display_as_args = display_desc["display_as_args"] #ie: "--start=xterm :10"
864 remote_cmd = ""
865 for x in remote_xpra:
866 if not remote_cmd:
867 check = "if"
868 else:
869 check = "elif"
870 if x=="xpra":
871 #no absolute path, so use "which" to check that the command exists:
872 pc = ['%s which "%s" > /dev/null 2>&1; then' % (check, x)]
873 else:
874 pc = ['%s [ -x %s ]; then' % (check, x)]
875 pc += [x] + proxy_command + [shellquote(x) for x in display_as_args]
876 if socket_dir:
877 pc.append("--socket-dir=%s" % socket_dir)
878 remote_cmd += " ".join(pc)+";"
879 remote_cmd += "else echo \"no run-xpra command found\"; exit 1; fi"
880 if INITENV_COMMAND:
881 remote_cmd = INITENV_COMMAND + ";" + remote_cmd
882 #how many times we need to escape the remote command string
883 #depends on how many times the ssh command is parsed
884 nssh = sum(int(x=="ssh") for x in cmd)
885 if nssh>=2 and MAGIC_QUOTES:
886 for _ in range(nssh):
887 remote_cmd = shlex.quote(remote_cmd)
888 else:
889 remote_cmd = "'%s'" % remote_cmd
890 cmd.append("sh -c %s" % remote_cmd)
891 if debug_cb:
892 debug_cb("starting %s tunnel" % str(cmd[0]))
893 #non-string arguments can make Popen choke,
894 #instead of lazily converting everything to a string, we validate the command:
895 for x in cmd:
896 if not isinstance(x, str):
897 raise InitException("argument is not a string: %s (%s), found in command: %s" % (x, type(x), cmd))
898 password = display_desc.get("password")
899 if password and not display_desc.get("is_putty", False):
900 from xpra.platform.paths import get_sshpass_command
901 sshpass_command = get_sshpass_command()
902 if sshpass_command:
903 #sshpass -e ssh ...
904 cmd.insert(0, sshpass_command)
905 cmd.insert(1, "-e")
906 if env is None:
907 env = os.environ.copy()
908 env["SSHPASS"] = password
909 #the password will be used by ssh via sshpass,
910 #don't try to authenticate again over the ssh-proxy connection,
911 #which would trigger warnings if the server does not require
912 #authentication over unix-domain-sockets:
913 opts.password = None
914 del display_desc["password"]
915 if env:
916 kwargs["env"] = env
917 if is_debug_enabled("ssh"):
918 log.info("executing ssh command: %s" % (" ".join("\"%s\"" % x for x in cmd)))
919 child = Popen(cmd, stdin=PIPE, stdout=PIPE, **kwargs)
920 except OSError as e:
921 raise InitExit(EXIT_SSH_FAILURE,
922 "Error running ssh command '%s': %s" % (" ".join("\"%s\"" % x for x in cmd), e))
923 def abort_test(action):
924 """ if ssh dies, we don't need to try to read/write from its sockets """
925 e = child.poll()
926 if e is not None:
927 had_connected = conn.input_bytecount>0 or conn.output_bytecount>0
928 if had_connected:
929 error_message = "cannot %s using SSH" % action
930 else:
931 error_message = "SSH connection failure"
932 sshpass_error = None
933 if sshpass_command:
934 sshpass_error = {
935 1 : "Invalid command line argument",
936 2 : "Conflicting arguments given",
937 3 : "General runtime error",
938 4 : "Unrecognized response from ssh (parse error)",
939 5 : "Invalid/incorrect password",
940 6 : "Host public key is unknown. sshpass exits without confirming the new key.",
941 }.get(e)
942 if sshpass_error:
943 error_message += ": %s" % sshpass_error
944 if debug_cb:
945 debug_cb(error_message)
946 if ssh_fail_cb:
947 ssh_fail_cb(error_message)
948 if "ssh_abort" not in display_desc:
949 display_desc["ssh_abort"] = True
950 if not had_connected:
951 log.error("Error: SSH connection to the xpra server failed")
952 if sshpass_error:
953 log.error(" %s", sshpass_error)
954 else:
955 log.error(" check your username, hostname, display number, firewall, etc")
956 display_name = display_desc["display_name"]
957 log.error(" for server: %s", display_name)
958 else:
959 log.error("The SSH process has terminated with exit code %s", e)
960 cmd_info = " ".join(display_desc["full_ssh"])
961 log.error(" the command line used was:")
962 log.error(" %s", cmd_info)
963 raise ConnectionClosedException(error_message) from None
964 def stop_tunnel():
965 if POSIX:
966 #on posix, the tunnel may be shared with other processes
967 #so don't kill it... which may leave it behind after use.
968 #but at least make sure we close all the pipes:
969 for name,fd in {
970 "stdin" : child.stdin,
971 "stdout" : child.stdout,
972 "stderr" : child.stderr,
973 }.items():
974 try:
975 if fd:
976 fd.close()
977 except Exception as e:
978 print("error closing ssh tunnel %s: %s" % (name, e))
979 if not display_desc.get("exit_ssh", False):
980 #leave it running
981 return
982 try:
983 if child.poll() is None:
984 child.terminate()
985 except Exception as e:
986 print("error trying to stop ssh tunnel process: %s" % e)
987 host = display_desc["host"]
988 port = display_desc.get("ssh-port", 22)
989 username = display_desc.get("username")
990 display = display_desc.get("display")
991 info = {
992 "host" : host,
993 "port" : port,
994 }
995 from xpra.net.bytestreams import TwoFileConnection
996 conn = TwoFileConnection(child.stdin, child.stdout,
997 abort_test, target=(host, port),
998 socktype="ssh", close_cb=stop_tunnel, info=info)
999 conn.endpoint = host_target_string("ssh", username, host, port, display)
1000 conn.timeout = 0 #taken care of by abort_test
1001 conn.process = (child, "ssh", cmd)
1002 if kwargs.get("stderr")==PIPE:
1003 def stderr_reader():
1004 errs = []
1005 while child.poll() is None:
1006 try:
1007 v = child.stderr.readline()
1008 except OSError:
1009 log("stderr_reader()", exc_info=True)
1010 break
1011 if not v:
1012 log("SSH EOF on stderr of %s", cmd)
1013 break
1014 s = bytestostr(v.rstrip(b"\n\r"))
1015 if s:
1016 errs.append(s)
1017 if errs:
1018 log.warn("remote SSH stderr:")
1019 for e in errs:
1020 log.warn(" %s", e)
1021 start_thread(stderr_reader, "ssh-stderr-reader", daemon=True)
1022 return conn