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

5 

6import sys 

7import os 

8import shlex 

9import socket 

10import signal 

11from time import sleep 

12from subprocess import PIPE, Popen 

13 

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 

31 

32log = Logger("network", "ssh") 

33 

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) 

38 

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) 

54 

55 

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 

64 

65 

66def force_focus(): 

67 from xpra.platform.gui import force_focus as _force_focus 

68 _force_focus() 

69 

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] 

97 

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

107 

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 

116 

117 

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

139 

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) 

153 

154 

155class SSHSocketConnection(SocketConnection): 

156 

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) 

160 

161 def get_raw_socket(self): 

162 return self._raw_socket 

163 

164 def start_stderr_reader(self): 

165 start_thread(self._stderr_reader, "ssh-stderr-reader", daemon=True) 

166 

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) 

179 

180 def peek(self, n): 

181 if not self._raw_socket: 

182 return None 

183 return self._raw_socket.recv(n, socket.MSG_PEEK) 

184 

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) 

189 

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 

199 

200 

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 

205 

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) 

214 

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 } 

227 

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) 

235 

236 

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 

264 

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 

324 

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

351 

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 

374 

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 

398 

399 

400#workaround incompatibility between paramiko and gssapi: 

401class nogssapi_context(nomodule_context): 

402 

403 def __init__(self): 

404 super().__init__("gssapi") 

405 

406 

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

412 

413 

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) 

420 

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) 

434 

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) 

453 

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

513 

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) 

529 

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

560 

561 

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

578 

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) 

632 

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) 

639 

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

648 

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

670 

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) 

676 

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

698 

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) 

728 

729 

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 

765 

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) 

812 

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

827 

828 

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

835 

836 

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