Index: xpra/client/gtk_base/client_launcher.py =================================================================== --- xpra/client/gtk_base/client_launcher.py (revision 21058) +++ xpra/client/gtk_base/client_launcher.py (working copy) @@ -48,7 +48,9 @@ #what we save in the config file: SAVED_FIELDS = ["username", "password", "host", "port", "mode", "ssh_port", - "encoding", "quality", "min-quality", "speed", "min-speed"] + "encoding", "quality", "min-quality", "speed", "min-speed", + "sshproxy_ssh_port", "proxy_username", "sshproxy_pkey_path", + "sshproxy_password" ] #options not normally found in xpra config file #but which can be present in a launcher config: @@ -56,19 +58,29 @@ "host" : str, "port" : int, "username" : str, + "proxy_username" : str, + "sshproxy_host" : str, + "sshproxy_password" : str, + "sshproxy_pkey_path": str, "password" : str, "mode" : str, "autoconnect" : bool, "ssh_port" : int, + "sshproxy_ssh_port" : int, } LAUNCHER_DEFAULTS = { "host" : "", "port" : -1, "username" : get_username(), + "proxy_username" : get_username(), + "sshproxy_host" : "", + "sshproxy_pkey_path": "", "password" : "", - "mode" : "tcp", #tcp,ssh,.. + "sshproxy_password" : "", + "mode" : "ssh -> ssh", #tcp,ssh,.. "autoconnect" : False, "ssh_port" : 22, + "sshproxy_ssh_port" : 22, } @@ -128,6 +140,19 @@ def get_connection_modes(self): modes = ["ssh"] try: + # the next line will always fail on first call to get_connection_modes() + # because self.config doesn't exist yet + ssh_cmd = parse_ssh_string(self.config.ssh)[0].strip().lower() + is_putty = ssh_cmd.endswith("plink") or ssh_cmd.endswith("plink.exe") + is_paramiko = ssh_cmd=="paramiko" + # this mode not implemented for openssh (it is implemented for plink and paramiko) + if is_putty or is_paramiko: + modes.append("ssh -> ssh") + else: + log("ssh is not putty or paramiko, disabling 'ssh->ssh' mode") + except: + pass + try: import ssl assert ssl modes.append("ssl") @@ -221,19 +246,102 @@ vbox.pack_start(hbox) # Mode: - hbox = gtk.HBox(False, 20) - hbox.set_spacing(20) - hbox.pack_start(gtk.Label("Mode: ")) + hbox = gtk.HBox(False, 5) self.mode_combo = gtk.combo_box_new_text() for x in self.get_connection_modes(): self.mode_combo.append_text(x.upper()) self.mode_combo.connect("changed", self.mode_changed) - hbox.pack_start(self.mode_combo) + self.padding_label = gtk.Label("") + self.padding_label2 = gtk.Label("") + hbox.pack_start(self.padding_label, True, True) + hbox.pack_start(gtk.Label("Mode: "), False, False) + hbox.pack_start(self.mode_combo, False, False) + hbox.pack_start(self.padding_label2, True, True) vbox.pack_start(hbox) - # Username@Host:Port - hbox = gtk.HBox(False, 0) - hbox.set_spacing(5) + # Username@Host:Port (ssh -> ssh, proxy) + vbox_proxy = gtk.VBox(False, 15) + hbox = gtk.HBox(False, 5) + self.sshproxy_vbox = vbox_proxy + self.sshproxy_label = gtk.Label("proxy: ") + self.sshproxy_username_entry = gtk.Entry() + self.sshproxy_username_entry.set_max_length(128) + self.sshproxy_username_entry.set_width_chars(16) + self.sshproxy_username_entry.connect("changed", self.validate) + self.sshproxy_username_entry.connect("activate", self.connect_clicked) + self.sshproxy_username_entry.set_tooltip_text("username") + self.sshproxy_username_label = gtk.Label("@") + self.sshproxy_host_entry = gtk.Entry() + self.sshproxy_host_entry.set_max_length(128) + self.sshproxy_host_entry.set_width_chars(24) + self.sshproxy_host_entry.connect("changed", self.validate) + self.sshproxy_host_entry.connect("activate", self.connect_clicked) + self.sshproxy_host_entry.set_tooltip_text("hostname") + self.sshproxy_ssh_port_entry = gtk.Entry() + self.sshproxy_ssh_port_entry.set_max_length(5) + self.sshproxy_ssh_port_entry.set_width_chars(5) + self.sshproxy_ssh_port_entry.connect("changed", self.validate) + self.sshproxy_ssh_port_entry.connect("activate", self.connect_clicked) + self.sshproxy_ssh_port_entry.set_tooltip_text("SSH port") + hbox.pack_start(self.sshproxy_label, False, False) + hbox.pack_start(self.sshproxy_username_entry, True, True) + hbox.pack_start(self.sshproxy_username_label, False, False) + hbox.pack_start(self.sshproxy_host_entry, True, True) + hbox.pack_start(self.sshproxy_ssh_port_entry, False, False) + vbox_proxy.pack_start(hbox) + + # + # Password + # + hbox = gtk.HBox(False, 5) + self.sshproxy_password_entry = gtk.Entry() + self.sshproxy_password_entry.set_max_length(128) + self.sshproxy_password_entry.set_width_chars(30) + self.sshproxy_password_entry.set_text("") + self.sshproxy_password_entry.set_visibility(False) + self.sshproxy_password_entry.connect("changed", self.password_ok) + self.sshproxy_password_entry.connect("changed", self.validate) + self.sshproxy_password_entry.connect("activate", self.connect_clicked) + self.sshproxy_password_label = gtk.Label("Proxy password:") + hbox.pack_start(self.sshproxy_password_label, False, False) + hbox.pack_start(self.sshproxy_password_entry, True, True) + vbox_proxy.pack_start(hbox) + + hbox = gtk.HBox(False, 5) + self.pkey_hbox = hbox + self.sshproxy_pkey_path_l = gtk.Label("Proxy private key path (PPK):") + self.sshproxy_pkey_path_entry = gtk.Entry() + self.sshproxy_pkey_path_browse = gtk.Button("Browse") + self.sshproxy_pkey_path_browse.connect("clicked", self.sshproxy_pkey_path_browse_clicked) + hbox.pack_start(self.sshproxy_pkey_path_l, False, False) + hbox.pack_start(self.sshproxy_pkey_path_entry, True, True) + hbox.pack_start(self.sshproxy_pkey_path_browse, False, False) + vbox_proxy.pack_start(hbox) + + # + # Same as checkbox + # + hbox = gtk.HBox(False, 5) + self.password_scb = gtk.CheckButton("Server password same as proxy") + self.password_scb.set_mode(True) + self.password_scb.set_active(True) + self.password_scb.connect("toggled", self.validate) + self.username_scb = gtk.CheckButton("Server username same as proxy") + self.username_scb.set_mode(True) + self.username_scb.set_active(True) + self.username_scb.connect("toggled", self.validate) + self.spacer_scb = gtk.Label("") + hbox.pack_start(self.username_scb, False, False) + hbox.pack_start(self.spacer_scb, True, True) + hbox.pack_start(self.password_scb, False, False) + vbox_proxy.pack_start(hbox) + + vbox.pack_start(vbox_proxy) + + + # Username@Host:Port (main) + hbox = gtk.HBox(False, 5) + self.server_label = gtk.Label("Server:") self.username_entry = gtk.Entry() self.username_entry.set_max_length(128) self.username_entry.set_width_chars(16) @@ -260,17 +368,19 @@ self.port_entry.connect("activate", self.connect_clicked) self.port_entry.set_tooltip_text("port/display") - hbox.pack_start(self.username_entry) - hbox.pack_start(self.username_label) - hbox.pack_start(self.host_entry) - hbox.pack_start(self.ssh_port_entry) - hbox.pack_start(gtk.Label(":")) - hbox.pack_start(self.port_entry) + hbox.pack_start(self.server_label, False, False) + hbox.pack_start(self.username_entry, True, True) + hbox.pack_start(self.username_label, False, False) + hbox.pack_start(self.host_entry, True, True) + hbox.pack_start(self.ssh_port_entry, False, False) + hbox.pack_start(gtk.Label(":"), False, False) + hbox.pack_start(self.port_entry, False, False) vbox.pack_start(hbox) + # # Password - hbox = gtk.HBox(False, 0) - hbox.set_spacing(20) + # + hbox = gtk.HBox(False, 5) self.password_entry = gtk.Entry() self.password_entry.set_max_length(128) self.password_entry.set_width_chars(30) @@ -278,14 +388,13 @@ self.password_entry.set_visibility(False) self.password_entry.connect("changed", self.password_ok) self.password_entry.connect("changed", self.validate) - self.password_label = gtk.Label("Password: ") - hbox.pack_start(self.password_label) - hbox.pack_start(self.password_entry) + self.password_label = gtk.Label("Server Password:") + hbox.pack_start(self.password_label, False, False) + hbox.pack_start(self.password_entry, True, True) vbox.pack_start(hbox) - #strict host key check for SSL and SSH - hbox = gtk.HBox(False, 0) - hbox.set_spacing(20) + # strict host key check for SSL and SSH + hbox = gtk.HBox(False, 5) self.nostrict_host_check = gtk.CheckButton("Disable Strict Host Key Check") self.nostrict_host_check.set_active(False) al = gtk.Alignment(xalign=0.5, yalign=0.5, xscale=0.0, yscale=0) @@ -412,10 +521,11 @@ def validate(self, *args): ssh = self.mode_combo.get_active_text()=="SSH" + sshtossh = self.mode_combo.get_active_text()=="SSH -> SSH" errs = [] host = self.host_entry.get_text() errs.append((self.host_entry, not bool(host), "specify the host")) - if ssh: + if ssh or sshtossh: #validate ssh port: ssh_port = self.ssh_port_entry.get_text() try: @@ -424,8 +534,21 @@ ssh_port = -1 errs.append((self.ssh_port_entry, ssh_port<0 or ssh_port>=2**16, "invalid SSH port number")) port = self.port_entry.get_text() - if ssh and not port: - port = 0 #port optional with ssh + if sshtossh: + if self.password_scb.get_active(): + self.password_entry.set_sensitive(False) + self.password_entry.set_text(self.sshproxy_password_entry.get_text()) + else: + self.password_entry.set_sensitive(True) + if self.username_scb.get_active(): + self.username_entry.set_sensitive(False) + self.username_entry.set_text(self.sshproxy_username_entry.get_text()) + else: + self.username_entry.set_sensitive(True) + sshproxy_host = self.sshproxy_host_entry.get_text() + errs.append((self.sshproxy_host_entry, not bool(sshproxy_host), "specify the proxy host")) + if ssh or sshtossh and not port: + port = 0 # port optional with ssh else: try: port = int(port) @@ -460,14 +583,25 @@ def mode_changed(self, *_args): mode = self.mode_combo.get_active_text().lower() ssh = mode=="ssh" - if ssh: + sshtossh = mode=="ssh -> ssh" + if ssh or sshtossh: self.port_entry.set_tooltip_text("Display number (optional)") self.port_entry.set_text("") - self.ssh_port_entry.set_text("22") self.ssh_port_entry.show() self.password_entry.set_tooltip_text("SSH Password") self.username_entry.set_tooltip_text("SSH Username") + if ssh: + self.sshproxy_vbox.hide() + self.password_scb.hide() + self.password_entry.set_sensitive(True) + self.username_entry.set_sensitive(True) + if sshtossh: + self.sshproxy_vbox.show() + self.password_scb.show() else: + self.password_entry.set_sensitive(True) + self.username_entry.set_sensitive(True) + self.sshproxy_vbox.hide() self.ssh_port_entry.hide() self.ssh_port_entry.set_text("") port_str = self.port_entry.get_text() @@ -479,10 +613,15 @@ if self.config.port>0: self.port_entry.set_text("%s" % self.config.port) can_use_password = True - if ssh: + if ssh or sshtossh: ssh_cmd = parse_ssh_string(self.config.ssh)[0].strip().lower() is_putty = ssh_cmd.endswith("plink") or ssh_cmd.endswith("plink.exe") is_paramiko = ssh_cmd=="paramiko" + if is_paramiko: + self.sshproxy_pkey_path_entry.set_text("paramiko automatically searches ~/.ssh/") + self.sshproxy_pkey_path_entry.set_editable(False) + self.sshproxy_pkey_path_entry.set_sensitive(False) + self.sshproxy_pkey_path_browse.hide() if is_putty or is_paramiko: can_use_password = True else: @@ -490,12 +629,19 @@ from xpra.platform.paths import get_sshpass_command sshpass = get_sshpass_command() can_use_password = bool(sshpass) + self.connect_btn.set_sensitive(len(err_text)==0) if can_use_password: self.password_label.show() self.password_entry.show() + if sshtossh: + self.sshproxy_password_label.show() + self.sshproxy_password_entry.show() else: self.password_label.hide() self.password_entry.hide() + if sshtossh: + self.sshproxy_password_label.hide() + self.sshproxy_password_entry.hide() self.validate() if mode=="ssl" or (mode=="ssh" and not WIN32): self.nostrict_host_check.show() @@ -546,6 +692,21 @@ def set_sensitive(self, s): glib.idle_add(self.window.set_sensitive, s) + def choose_pkey_file(self, title, action, action_button, callback): + file_filter = gtk.FileFilter() + file_filter.set_name("All Files") + file_filter.add_pattern("*") + choose_file(self.window, title, action, action_button, callback, file_filter) + + def sshproxy_pkey_path_browse_clicked(self, *args): + log("sshproxy_pkey_path_browse_clicked%s", args) + def do_choose(filename): + #make sure the file extension is .ppk + if os.path.splitext(filename)[-1]!=".ppk": + filename += ".ppk" + self.sshproxy_pkey_path_entry.set_text(filename) + self.choose_pkey_file("Choose SSH private key", FILE_CHOOSER_ACTION_SAVE, gtk.STOCK_SAVE, do_choose) + def connect_clicked(self, *args): log("connect_clicked%s", args) self.update_options_from_gui() @@ -591,7 +752,7 @@ #cooked vars used by connect_to params = {"type" : self.config.mode} username = self.config.username - if self.config.mode=="ssh": + if self.config.mode=="ssh" or self.config.mode=="ssh -> ssh": if self.config.socket_dir: params["socket_dir"] = self.config.socket_dir params["remote_xpra"] = self.config.remote_xpra @@ -627,6 +788,29 @@ params["username"] = username if self.nostrict_host_check.get_active(): full_ssh += ["-o", "StrictHostKeyChecking=no"] + if params["type"] == "ssh -> ssh": + log("2hop") + params["type"] = "ssh" + if is_putty: + proxyline = ssh_cmd + proxyline += " -nc %host:%port" + proxyline += " -P " + str(self.config.sshproxy_ssh_port) + proxyline += " -l " + self.config.sshproxy_username + ptext = self.config.sshproxy_password + if bool(ptext): + proxyline += " -pw " + ptext + ptext = self.config.sshproxy_pkey_path + ptext = ptext.replace("\\", "/") + if bool(ptext): + proxyline += " -i " + '"' + ptext + '"' + proxyline += " " + self.config.sshproxy_host + full_ssh += [ "-proxycmd", proxyline ] + if is_paramiko: + log("setting phost/pport") + params["phost"] = self.config.sshproxy_host + params["pport"] = self.config.sshproxy_ssh_port + params["pusername"] = self.config.sshproxy_username + params["ppassword"] = self.config.sshproxy_password params["host"] = host params["local"] = is_local(self.config.host) params["full_ssh"] = full_ssh @@ -784,8 +968,16 @@ return int(v) except ValueError: return 0 + self.config.sshproxy_ssh_port = pint(self.sshproxy_ssh_port_entry.get_text()) + self.config.sshproxy_host = self.sshproxy_host_entry.get_text() + self.config.sshproxy_username = self.sshproxy_username_entry.get_text() + ssh_cmd = parse_ssh_string(self.config.ssh)[0].strip().lower() + if ssh_cmd != "paramiko": + self.config.sshproxy_pkey_path = self.sshproxy_pkey_path_entry.get_text() self.config.host = self.host_entry.get_text() self.config.ssh_port = pint(self.ssh_port_entry.get_text()) + self.config.sshproxy_ssh_port = pint(self.sshproxy_ssh_port_entry.get_text()) + self.config.sshproxy_password = self.sshproxy_password_entry.get_text() self.config.port = pint(self.port_entry.get_text()) self.config.username = self.username_entry.get_text() self.config.encoding = self.get_selected_encoding() or self.config.encoding @@ -794,7 +986,7 @@ self.config.mode = "tcp" if mode_enc.find("aes")>0: self.config.encryption = "AES" - elif mode_enc in ("ssl", "ssh", "ws", "wss"): + elif mode_enc in ("ssl", "ssh", "ws", "wss", "ssh -> ssh"): self.config.mode = mode_enc self.config.encryption = "" self.config.password = self.password_entry.get_text() @@ -803,9 +995,14 @@ def update_gui_from_config(self): #mode: mode = (self.config.mode or "").lower() + got_one = False for i,e in enumerate(self.get_connection_modes()): if e.lower()==mode: + got_one = True self.mode_combo.set_active(i) + if not got_one: + log("Bad default for mode_combo, applying fallback default") + self.mode_combo.set_active(0) if self.config.encoding and self.encoding_combo: index = self.encoding_combo.get_menu().encoding_to_index.get(self.config.encoding, -1) log("setting encoding combo to %s / %s", self.config.encoding, index) @@ -815,8 +1012,14 @@ #then select it in the combo: if index>=0: self.encoding_combo.set_history(index) + self.sshproxy_username_entry.set_text(self.config.proxy_username) + self.sshproxy_host_entry.set_text(self.config.sshproxy_host) + ssh_cmd = parse_ssh_string(self.config.ssh)[0].strip().lower() + if(ssh_cmd != "paramiko"): + self.sshproxy_pkey_path_entry.set_text(self.config.sshproxy_pkey_path) self.username_entry.set_text(self.config.username) self.password_entry.set_text(self.config.password) + self.sshproxy_password_entry.set_text(self.config.sshproxy_password) self.host_entry.set_text(self.config.host) def get_port(v, default_port=""): try: @@ -827,11 +1030,12 @@ pass return str(default_port) dport = DEFAULT_PORT - if mode=="ssh": + if mode=="ssh" or mode=="ssh -> ssh": #not required, so don't specify one dport = "" self.port_entry.set_text(get_port(self.config.port, dport)) self.ssh_port_entry.set_text(get_port(self.config.ssh_port)) + self.sshproxy_ssh_port_entry.set_text(get_port(self.config.sshproxy_ssh_port)) def close_window(self, *_args): w = self.window @@ -986,7 +1190,6 @@ #file says we should connect, #do that only (not showing UI unless something goes wrong): glib.idle_add(app.do_connect) - if not has_file: app.reset_errors() if not app.config.autoconnect or app.config.debug: if OSX and not has_file: Index: xpra/net/ssh.py =================================================================== --- xpra/net/ssh.py (revision 21058) +++ xpra/net/ssh.py (working copy) @@ -249,16 +251,43 @@ from paramiko.ssh_exception import ProxyCommandFailure bytestreams.CLOSED_EXCEPTIONS = tuple(list(bytestreams.CLOSED_EXCEPTIONS)+[ProxyCommandFailure]) return conn + from xpra.scripts.main import socket_connect + from paramiko.transport import Transport + from paramiko import SSHException + if "phost" in display_desc: + phost = display_desc["phost"] + pport = display_desc.get("pport", 22) + pusername = display_desc.get("pusername", username) + ppassword = display_desc.get("ppassword", password) + sock = socket_connect(dtype, phost, pport, ipv6) + middle_transport = Transport(sock) + middle_transport.use_compression(False) + try: + middle_transport.start_client() + except SSHException as e: + log("start_client()", exc_info=True) + raise InitException("SSH negotiation failed: %s" % e) + chan_to_middle = do_ssh_paramiko_connect_to(middle_transport, phost, pusername, ppassword, dest_host=host, dest_port=port) + transport = Transport(chan_to_middle) + transport.use_compression(False) + try: + transport.start_client() + except SSHException as e: + log("start_client()", exc_info=True) + raise InitException("SSH negotiation failed: %s" % e) + chan = do_ssh_paramiko_connect_to(transport, host, username, password, proxy_command, remote_xpra, socket_dir, display_as_args) + peername = (host, port) + conn = SSHProxyCommandConnection(chan, peername, target, socket_info) + conn.timeout = SOCKET_TIMEOUT + conn.start_stderr_reader() + return conn #plain TCP connection to the server, #we open it then give the socket to paramiko: - from xpra.scripts.main import socket_connect sock = socket_connect(dtype, host, port, ipv6) sockname = sock.getsockname() peername = sock.getpeername() log("paramiko socket_connect: sockname=%s, peername=%s", sockname, peername) - from paramiko.transport import Transport - from paramiko import SSHException transport = Transport(sock) transport.use_compression(False) try: @@ -281,9 +310,11 @@ def __init__(self): nomodule_context.__init__(self, "gssapi") - +# (1) If the arguments after "proxy_command" are "None", then we're opening a port-forward +# (2) If "parachan" is set, that means we're using a port-forward def do_ssh_paramiko_connect_to(transport, host, username, password, - xpra_proxy_command, remote_xpra, socket_dir, display_as_args): + xpra_proxy_command=None, remote_xpra=None, socket_dir=None, display_as_args=None, + dest_host=None, dest_port=None): from paramiko import SSHException, RSAKey, PasswordRequiredException from paramiko.agent import Agent from paramiko.hostkeys import HostKeys @@ -452,6 +483,27 @@ log("auth_password(..)", exc_info=True) log.info("SSH password authentication failed: %s", e) + def auth_interactive(): + log("trying interactive authentication") + class iauthhandler: + def __init__(self): + self.authcount = 0 + def handlestuff(self, title, instructions, prompt_list): + p = [ ] + for pent in prompt_list: + if self.authcount == 0: + p.append(password) + else: + p.append(input_pass(pent[0])) + self.authcount += 1 + return p + try: + myiauthhandler = iauthhandler() + transport.auth_interactive(username, myiauthhandler.handlestuff, '') + except SSHException as e: + log("auth_password(..)", exc_info=True) + log.info("SSH password authentication failed: %s", e) + banner = transport.get_banner() if banner: log.info("SSH server banner:") @@ -459,18 +511,24 @@ log.info(" %s", x) log("starting authentication") + # per the RFC we probably should do none first always and read off the supported + # methods, however, the current code seems to work fine with OpenSSH if not transport.is_authenticated() and NONE_AUTH: auth_none() + # Some people do two-factor using KEY_AUTH to kick things off, so this happens first + if not transport.is_authenticated() and KEY_AUTH: + auth_publickey() + if not transport.is_authenticated() and PASSWORD_AUTH and password: + auth_interactive() + + if not transport.is_authenticated() and PASSWORD_AUTH and password: auth_password() if not transport.is_authenticated() and AGENT_AUTH: auth_agent() - if not transport.is_authenticated() and KEY_AUTH: - auth_publickey() - if not transport.is_authenticated() and PASSWORD_AUTH and not password: for _ in range(1+PASSWORD_RETRY): password = input_pass("please enter the SSH password for %s@%s" % (username, host)) @@ -482,8 +540,13 @@ if not transport.is_authenticated(): transport.close() - raise InitException("SSH Authentication failed") + # added the "host" so the user can figure out errors in ssh -> ssh mode + raise InitException("SSH Authentication on " + host + " failed") + if(remote_xpra == None): + log("Opening proxy channel") + return transport.open_channel("direct-tcpip", (dest_host, dest_port), ('localhost', 0)) + assert len(remote_xpra)>0 log("will try to run xpra from: %s", remote_xpra) for xpra_cmd in remote_xpra: