xpra icon
Bug tracker and wiki

This bug tracker and wiki are being discontinued
please use https://github.com/Xpra-org/xpra instead.


Ticket #2041: p2.txt

File p2.txt, 33.1 KB (added by Nathan Hallquist, 3 years ago)
Line 
1Index: xpra/client/gtk_base/client_launcher.py
2===================================================================
3--- xpra/client/gtk_base/client_launcher.py     (revision 21127)
4+++ xpra/client/gtk_base/client_launcher.py     (working copy)
5@@ -40,7 +40,8 @@
6 from xpra.client.gtk_base.gtk_tray_menu_base import make_min_auto_menu, make_encodingsmenu, \
7                                     MIN_QUALITY_OPTIONS, QUALITY_OPTIONS, MIN_SPEED_OPTIONS, SPEED_OPTIONS
8 from xpra.gtk_common.about import about
9-from xpra.scripts.main import connect_to, make_client, configure_network, is_local, add_ssh_args, parse_ssh_string
10+from xpra.scripts.main import connect_to, make_client, configure_network, is_local, add_ssh_args, parse_ssh_string, \
11+                                    add_ssh_proxy_args
12 from xpra.platform.paths import get_icon_dir
13 from xpra.platform import get_username
14 from xpra.log import Logger, enable_debug_for
15@@ -48,7 +49,9 @@
16 
17 #what we save in the config file:
18 SAVED_FIELDS = ["username", "password", "host", "port", "mode", "ssh_port",
19-                "encoding", "quality", "min-quality", "speed", "min-speed"]
20+                "encoding", "quality", "min-quality", "speed", "min-speed",
21+                "proxy_ssh_port", "proxy_username", "proxy_pkey_path",
22+                "proxy_password" ]
23 
24 #options not normally found in xpra config file
25 #but which can be present in a launcher config:
26@@ -56,19 +59,29 @@
27                         "host"              : str,
28                         "port"              : int,
29                         "username"          : str,
30+                        "proxy_username"    : str,
31+                        "proxy_host"        : str,
32+                        "proxy_password"    : str,
33+                        "proxy_pkey_path"   : str,
34                         "password"          : str,
35                         "mode"              : str,
36                         "autoconnect"       : bool,
37                         "ssh_port"          : int,
38+                        "proxy_ssh_port"    : int,
39                         }
40 LAUNCHER_DEFAULTS = {
41                         "host"              : "",
42                         "port"              : -1,
43                         "username"          : get_username(),
44+                        "proxy_username"    : get_username(),
45+                        "proxy_host"        : "",
46+                        "proxy_pkey_path"   : "",
47                         "password"          : "",
48-                        "mode"              : "tcp",    #tcp,ssh,..
49+                        "proxy_password"    : "",
50+                        "mode"              : "ssh -> ssh",    #tcp,ssh,..
51                         "autoconnect"       : False,
52                         "ssh_port"          : 22,
53+                        "proxy_ssh_port"    : 22,
54                     }
55 
56 
57@@ -113,6 +126,9 @@
58     def __init__(self):
59         # Default connection options
60         self.config = make_defaults_struct(extras_defaults=LAUNCHER_DEFAULTS, extras_types=LAUNCHER_OPTION_TYPES, extras_validation=self.get_launcher_validation())
61+        ssh_cmd = parse_ssh_string(self.config.ssh)[0].strip().lower()
62+        self.is_putty = ssh_cmd.endswith("plink") or ssh_cmd.endswith("plink.exe")
63+        self.is_paramiko = ssh_cmd=="paramiko"
64         #TODO: the fixup does not belong here?
65         from xpra.scripts.main import fixup_options
66         fixup_options(self.config)
67@@ -127,6 +143,7 @@
68 
69     def get_connection_modes(self):
70         modes = ["ssh"]
71+        modes.append("ssh -> ssh")
72         try:
73             import ssl
74             assert ssl
75@@ -221,19 +238,96 @@
76         vbox.pack_start(hbox)
77 
78         # Mode:
79-        hbox = gtk.HBox(False, 20)
80-        hbox.set_spacing(20)
81-        hbox.pack_start(gtk.Label("Mode: "))
82+        hbox = gtk.HBox(False, 5)
83         self.mode_combo = gtk.combo_box_new_text()
84         for x in self.get_connection_modes():
85             self.mode_combo.append_text(x.upper())
86         self.mode_combo.connect("changed", self.mode_changed)
87-        hbox.pack_start(self.mode_combo)
88-        vbox.pack_start(hbox)
89+        hbox.pack_start(gtk.Label("Mode: "), False, False)
90+        hbox.pack_start(self.mode_combo, False, False)
91+        align_hbox = gtk.Alignment(xalign = .5)
92+        align_hbox.add(hbox)
93+        vbox.pack_start(align_hbox)
94 
95-        # Username@Host:Port
96-        hbox = gtk.HBox(False, 0)
97-        hbox.set_spacing(5)
98+        # Username@Host:Port (ssh -> ssh, proxy)
99+        vbox_proxy = gtk.VBox(False, 15)
100+        hbox = gtk.HBox(False, 5)
101+        self.proxy_vbox = vbox_proxy
102+        self.proxy_username_entry = gtk.Entry()
103+        self.proxy_username_entry.set_max_length(128)
104+        self.proxy_username_entry.set_width_chars(16)
105+        self.proxy_username_entry.connect("changed", self.validate)
106+        self.proxy_username_entry.connect("activate", self.connect_clicked)
107+        self.proxy_username_entry.set_tooltip_text("username")
108+        self.proxy_host_entry = gtk.Entry()
109+        self.proxy_host_entry.set_max_length(128)
110+        self.proxy_host_entry.set_width_chars(24)
111+        self.proxy_host_entry.connect("changed", self.validate)
112+        self.proxy_host_entry.connect("activate", self.connect_clicked)
113+        self.proxy_host_entry.set_tooltip_text("hostname")
114+        self.proxy_ssh_port_entry = gtk.Entry()
115+        self.proxy_ssh_port_entry.set_max_length(5)
116+        self.proxy_ssh_port_entry.set_width_chars(5)
117+        self.proxy_ssh_port_entry.connect("changed", self.validate)
118+        self.proxy_ssh_port_entry.connect("activate", self.connect_clicked)
119+        self.proxy_ssh_port_entry.set_tooltip_text("SSH port")
120+        hbox.pack_start(gtk.Label("Proxy: "), False, False)
121+        hbox.pack_start(self.proxy_username_entry, True, True)
122+        hbox.pack_start(gtk.Label("@"), False, False)
123+        hbox.pack_start(self.proxy_host_entry, True, True)
124+        hbox.pack_start(self.proxy_ssh_port_entry, False, False)
125+        vbox_proxy.pack_start(hbox)
126+
127+        # Password
128+        hbox = gtk.HBox(False, 5)
129+        self.proxy_password_entry = gtk.Entry()
130+        self.proxy_password_entry.set_max_length(128)
131+        self.proxy_password_entry.set_width_chars(30)
132+        self.proxy_password_entry.set_text("")
133+        self.proxy_password_entry.set_visibility(False)
134+        self.proxy_password_entry.connect("changed", self.password_ok)
135+        self.proxy_password_entry.connect("changed", self.validate)
136+        self.proxy_password_entry.connect("activate", self.connect_clicked)
137+        self.proxy_password_label = gtk.Label("Proxy Password")
138+        hbox.pack_start(self.proxy_password_label, False, False)
139+        hbox.pack_start(self.proxy_password_entry, True, True)
140+        vbox_proxy.pack_start(hbox)
141+
142+        # Private key
143+        hbox = gtk.HBox(False,  5)
144+        self.pkey_hbox = hbox
145+        self.proxy_pkey_path_l = gtk.Label("Proxy private key path (PPK):")
146+        self.proxy_pkey_path_entry = gtk.Entry()
147+        self.proxy_pkey_path_browse = gtk.Button("Browse")
148+        self.proxy_pkey_path_browse.connect("clicked", self.proxy_pkey_path_browse_clicked)
149+        hbox.pack_start(self.proxy_pkey_path_l, False, False)
150+        hbox.pack_start(self.proxy_pkey_path_entry, True, True)
151+        hbox.pack_start(self.proxy_pkey_path_browse, False, False)
152+        vbox_proxy.pack_start(hbox)
153+
154+        # Check boxes
155+        hbox = gtk.HBox(False, 5)
156+        self.password_scb = gtk.CheckButton("Server password same as proxy")
157+        self.password_scb.set_mode(True)
158+        self.password_scb.set_active(True)
159+        self.password_scb.connect("toggled", self.validate)
160+        align_password_scb = gtk.Alignment(xalign = 1.0)
161+        align_password_scb.add(self.password_scb)
162+        self.username_scb = gtk.CheckButton("Server username same as proxy")
163+        self.username_scb.set_mode(True)
164+        self.username_scb.set_active(True)
165+        self.username_scb.connect("toggled", self.validate)
166+        align_username_scb = gtk.Alignment(xalign = 0.0)
167+        align_username_scb.add(self.username_scb)
168+        hbox.pack_start(align_username_scb, True, True)
169+        hbox.pack_start(align_password_scb, True, True)
170+        vbox_proxy.pack_start(hbox)
171+
172+        # coniditonal stuff that goes away for "normal" ssh
173+        vbox.pack_start(vbox_proxy)
174+
175+        # Username@Host:Port (main)
176+        hbox = gtk.HBox(False, 5)
177         self.username_entry = gtk.Entry()
178         self.username_entry.set_max_length(128)
179         self.username_entry.set_width_chars(16)
180@@ -240,7 +334,6 @@
181         self.username_entry.connect("changed", self.validate)
182         self.username_entry.connect("activate", self.connect_clicked)
183         self.username_entry.set_tooltip_text("username")
184-        self.username_label = gtk.Label("@")
185         self.host_entry = gtk.Entry()
186         self.host_entry.set_max_length(128)
187         self.host_entry.set_width_chars(24)
188@@ -259,18 +352,17 @@
189         self.port_entry.connect("changed", self.validate)
190         self.port_entry.connect("activate", self.connect_clicked)
191         self.port_entry.set_tooltip_text("port/display")
192-
193-        hbox.pack_start(self.username_entry)
194-        hbox.pack_start(self.username_label)
195-        hbox.pack_start(self.host_entry)
196-        hbox.pack_start(self.ssh_port_entry)
197-        hbox.pack_start(gtk.Label(":"))
198-        hbox.pack_start(self.port_entry)
199+        hbox.pack_start(gtk.Label("Server:"), False, False)
200+        hbox.pack_start(self.username_entry, True, True)
201+        hbox.pack_start(gtk.Label("@"), False, False)
202+        hbox.pack_start(self.host_entry, True, True)
203+        hbox.pack_start(self.ssh_port_entry, False, False)
204+        hbox.pack_start(gtk.Label(":"), False, False)
205+        hbox.pack_start(self.port_entry, False, False)
206         vbox.pack_start(hbox)
207 
208         # Password
209-        hbox = gtk.HBox(False, 0)
210-        hbox.set_spacing(20)
211+        hbox = gtk.HBox(False, 5)
212         self.password_entry = gtk.Entry()
213         self.password_entry.set_max_length(128)
214         self.password_entry.set_width_chars(30)
215@@ -278,14 +370,13 @@
216         self.password_entry.set_visibility(False)
217         self.password_entry.connect("changed", self.password_ok)
218         self.password_entry.connect("changed", self.validate)
219-        self.password_label = gtk.Label("Password: ")
220-        hbox.pack_start(self.password_label)
221-        hbox.pack_start(self.password_entry)
222+        self.password_label = gtk.Label("Server Password:")
223+        hbox.pack_start(self.password_label, False, False)
224+        hbox.pack_start(self.password_entry, True, True)
225         vbox.pack_start(hbox)
226 
227-        #strict host key check for SSL and SSH
228-        hbox = gtk.HBox(False, 0)
229-        hbox.set_spacing(20)
230+        # strict host key check for SSL and SSH
231+        hbox = gtk.HBox(False, 5)
232         self.nostrict_host_check = gtk.CheckButton("Disable Strict Host Key Check")
233         self.nostrict_host_check.set_active(False)
234         al = gtk.Alignment(xalign=0.5, yalign=0.5, xscale=0.0, yscale=0)
235@@ -412,10 +503,11 @@
236 
237     def validate(self, *args):
238         ssh = self.mode_combo.get_active_text()=="SSH"
239+        sshtossh = self.mode_combo.get_active_text()=="SSH -> SSH"
240         errs = []
241         host = self.host_entry.get_text()
242         errs.append((self.host_entry, not bool(host), "specify the host"))
243-        if ssh:
244+        if ssh or sshtossh:
245             #validate ssh port:
246             ssh_port = self.ssh_port_entry.get_text()
247             try:
248@@ -424,8 +516,25 @@
249                 ssh_port = -1
250             errs.append((self.ssh_port_entry, ssh_port<0 or ssh_port>=2**16, "invalid SSH port number"))
251         port = self.port_entry.get_text()
252-        if ssh and not port:
253-            port = 0        #port optional with ssh
254+        if sshtossh:
255+            if self.password_scb.get_active():
256+                self.password_entry.set_sensitive(False)
257+                self.password_entry.set_text(self.proxy_password_entry.get_text())
258+            else:
259+                self.password_entry.set_sensitive(True)
260+            if self.username_scb.get_active():
261+                self.username_entry.set_sensitive(False)
262+                self.username_entry.set_text(self.proxy_username_entry.get_text())
263+            else:
264+                self.username_entry.set_sensitive(True)
265+            errs.append((self.proxy_host_entry, not bool(self.proxy_host_entry.get_text()), "specify the proxy host"))
266+        # check username *after* the checkbox action
267+        if ssh or sshtossh:
268+            errs.append((self.username_entry, not bool(self.username_entry.get_text()), "specify username"))
269+        if sshtossh:
270+            errs.append((self.proxy_username_entry, not bool(self.proxy_username_entry.get_text()), "specify proxy username"))
271+        if ssh or sshtossh and not port:
272+            port = 0        # port optional with ssh
273         else:
274             try:
275                 port = int(port)
276@@ -460,14 +569,25 @@
277     def mode_changed(self, *_args):
278         mode = self.mode_combo.get_active_text().lower()
279         ssh = mode=="ssh"
280-        if ssh:
281+        sshtossh = mode=="ssh -> ssh"
282+        if ssh or sshtossh:
283             self.port_entry.set_tooltip_text("Display number (optional)")
284             self.port_entry.set_text("")
285-            self.ssh_port_entry.set_text("22")
286             self.ssh_port_entry.show()
287             self.password_entry.set_tooltip_text("SSH Password")
288             self.username_entry.set_tooltip_text("SSH Username")
289+            if ssh:
290+                self.proxy_vbox.hide()
291+                self.password_scb.hide()
292+                self.password_entry.set_sensitive(True)
293+                self.username_entry.set_sensitive(True)
294+            if sshtossh:
295+                self.proxy_vbox.show()
296+                self.password_scb.show()
297         else:
298+            self.password_entry.set_sensitive(True)
299+            self.username_entry.set_sensitive(True)
300+            self.proxy_vbox.hide()
301             self.ssh_port_entry.hide()
302             self.ssh_port_entry.set_text("")
303             port_str = self.port_entry.get_text()
304@@ -479,11 +599,13 @@
305             if self.config.port>0:
306                 self.port_entry.set_text("%s" % self.config.port)
307         can_use_password = True
308-        if ssh:
309-            ssh_cmd = parse_ssh_string(self.config.ssh)[0].strip().lower()
310-            is_putty = ssh_cmd.endswith("plink") or ssh_cmd.endswith("plink.exe")
311-            is_paramiko = ssh_cmd=="paramiko"
312-            if is_putty or is_paramiko:
313+        if ssh or sshtossh:
314+            if self.is_paramiko:
315+                self.proxy_pkey_path_entry.set_text("paramiko automatically searches ~/.ssh/")
316+                self.proxy_pkey_path_entry.set_editable(False)
317+                self.proxy_pkey_path_entry.set_sensitive(False)
318+                self.proxy_pkey_path_browse.hide()
319+            if self.is_paramiko or self.is_putty:
320                 can_use_password = True
321             else:
322                 #we can also use password if sshpass is installed:
323@@ -493,9 +615,15 @@
324         if can_use_password:
325             self.password_label.show()
326             self.password_entry.show()
327+            if sshtossh:
328+                self.proxy_password_label.show()
329+                self.proxy_password_entry.show()
330         else:
331             self.password_label.hide()
332             self.password_entry.hide()
333+            if sshtossh:
334+                self.proxy_password_label.hide()
335+                self.proxy_password_entry.hide()
336         self.validate()
337         if mode=="ssl" or (mode=="ssh" and not WIN32):
338             self.nostrict_host_check.show()
339@@ -531,7 +659,9 @@
340     def reset_errors(self):
341         self.set_sensitive(True)
342         self.set_info_text("")
343-        for widget in (self.info, self.password_entry, self.username_entry, self.host_entry, self.port_entry):
344+        for widget in (self.info, self.password_entry, self.username_entry, self.host_entry,
345+                       self.port_entry, self.proxy_password_entry, self.proxy_username_entry,
346+                       self.proxy_host_entry):
347             self.set_widget_fg_color(self.info, False)
348             self.set_widget_bg_color(widget, False)
349 
350@@ -546,6 +676,21 @@
351     def set_sensitive(self, s):
352         glib.idle_add(self.window.set_sensitive, s)
353 
354+    def choose_pkey_file(self, title, action, action_button, callback):
355+        file_filter = gtk.FileFilter()
356+        file_filter.set_name("All Files")
357+        file_filter.add_pattern("*")
358+        choose_file(self.window, title, action, action_button, callback, file_filter)
359+
360+    def proxy_pkey_path_browse_clicked(self, *args):
361+        log("proxy_pkey_path_browse_clicked%s", args)
362+        def do_choose(filename):
363+            #make sure the file extension is .ppk
364+            if os.path.splitext(filename)[-1]!=".ppk":
365+                filename += ".ppk"
366+            self.proxy_pkey_path_entry.set_text(filename)
367+        self.choose_pkey_file("Choose SSH private key", FILE_CHOOSER_ACTION_SAVE, gtk.STOCK_SAVE, do_choose)
368+
369     def connect_clicked(self, *args):
370         log("connect_clicked%s", args)
371         self.update_options_from_gui()
372@@ -591,7 +736,7 @@
373         #cooked vars used by connect_to
374         params = {"type"    : self.config.mode}
375         username = self.config.username
376-        if self.config.mode=="ssh":
377+        if self.config.mode=="ssh" or self.config.mode=="ssh -> ssh":
378             if self.config.socket_dir:
379                 params["socket_dir"] = self.config.socket_dir
380             params["remote_xpra"] = self.config.remote_xpra
381@@ -602,12 +747,9 @@
382             else:
383                 params["display"] = "auto"
384                 params["display_as_args"] = []
385-            full_ssh = parse_ssh_string(self.config.ssh)
386-            ssh_cmd = full_ssh[0].lower()
387-            is_putty = ssh_cmd.endswith("plink") or ssh_cmd.endswith("plink.exe")
388-            is_paramiko = ssh_cmd=="paramiko"
389-            params["is_putty"] = is_putty
390-            params["is_paramiko"] = is_paramiko
391+            params["ssh"] = self.config.ssh
392+            params["is_putty"] = self.is_putty
393+            params["is_paramiko"] = self.is_paramiko
394             password = self.config.password
395             host = self.config.host
396             upos = host.find("@")
397@@ -622,11 +764,23 @@
398                     username = username[:ppos]
399             if self.config.ssh_port and self.config.ssh_port!=22:
400                 params["ssh-port"] = self.config.ssh_port
401-            full_ssh += add_ssh_args(username, password, host, self.config.ssh_port, is_putty, is_paramiko)
402+            ssh_cmd = parse_ssh_string(self.config.ssh)
403+            full_ssh = ssh_cmd[:]
404+            full_ssh += add_ssh_args(username, password, host, self.config.ssh_port, self.is_putty, self.is_paramiko)
405             if username:
406                 params["username"] = username
407             if self.nostrict_host_check.get_active():
408                 full_ssh += ["-o", "StrictHostKeyChecking=no"]
409+            if params["type"] == "ssh -> ssh":
410+                params["type"] = "ssh"
411+                params["proxy_host"] = self.config.proxy_host
412+                params["proxy_ssh_port"] = self.config.proxy_ssh_port
413+                params["proxy_username"] = self.config.proxy_username
414+                params["proxy_password"] = self.config.proxy_password
415+                full_ssh += add_ssh_proxy_args(self.config.proxy_username, self.config.proxy_password,
416+                                               self.config.proxy_host, self.config.proxy_ssh_port,
417+                                               self.config.proxy_pkey_path, ssh_cmd,
418+                                               self.is_putty, self.is_paramiko)
419             params["host"] = host
420             params["local"] = is_local(self.config.host)
421             params["full_ssh"] = full_ssh
422@@ -784,8 +938,15 @@
423                 return int(v)
424             except ValueError:
425                 return 0
426+        self.config.proxy_ssh_port = pint(self.proxy_ssh_port_entry.get_text())
427+        self.config.proxy_host = self.proxy_host_entry.get_text()
428+        self.config.proxy_username = self.proxy_username_entry.get_text()
429+        if not self.is_paramiko:
430+            self.config.proxy_pkey_path = self.proxy_pkey_path_entry.get_text()
431         self.config.host = self.host_entry.get_text()
432         self.config.ssh_port = pint(self.ssh_port_entry.get_text())
433+        self.config.proxy_ssh_port = pint(self.proxy_ssh_port_entry.get_text())
434+        self.config.proxy_password = self.proxy_password_entry.get_text()
435         self.config.port = pint(self.port_entry.get_text())
436         self.config.username = self.username_entry.get_text()
437         self.config.encoding = self.get_selected_encoding() or self.config.encoding
438@@ -794,7 +955,7 @@
439             self.config.mode = "tcp"
440             if mode_enc.find("aes")>0:
441                 self.config.encryption = "AES"
442-        elif mode_enc in ("ssl", "ssh", "ws", "wss"):
443+        elif mode_enc in ("ssl", "ssh", "ws", "wss", "ssh -> ssh"):
444             self.config.mode = mode_enc
445             self.config.encryption = ""
446         self.config.password = self.password_entry.get_text()
447@@ -818,8 +979,13 @@
448             #then select it in the combo:
449             if index>=0:
450                 self.encoding_combo.set_history(index)
451+        self.proxy_username_entry.set_text(self.config.proxy_username)
452+        self.proxy_host_entry.set_text(self.config.proxy_host)
453+        if not self.is_paramiko:
454+            self.proxy_pkey_path_entry.set_text(self.config.proxy_pkey_path)
455         self.username_entry.set_text(self.config.username)
456         self.password_entry.set_text(self.config.password)
457+        self.proxy_password_entry.set_text(self.config.proxy_password)
458         self.host_entry.set_text(self.config.host)
459         def get_port(v, default_port=""):
460             try:
461@@ -830,11 +996,12 @@
462                 pass
463             return str(default_port)
464         dport = DEFAULT_PORT
465-        if mode=="ssh":
466+        if mode=="ssh" or mode=="ssh -> ssh":
467             #not required, so don't specify one
468             dport = ""
469         self.port_entry.set_text(get_port(self.config.port, dport))
470         self.ssh_port_entry.set_text(get_port(self.config.ssh_port))
471+        self.proxy_ssh_port_entry.set_text(get_port(self.config.proxy_ssh_port))
472 
473     def close_window(self, *_args):
474         w = self.window
475Index: xpra/net/ssh.py
476===================================================================
477--- xpra/net/ssh.py     (revision 21127)
478+++ xpra/net/ssh.py     (working copy)
479@@ -79,6 +79,8 @@
480             env["XPRA_LOG_TO_FILE"] = "0"
481             kwargs["env"] = env
482         proc = Popen(cmd, stdin=PIPE, stdout=PIPE, stderr=PIPE, **kwargs)
483+        # without this the return code is always None in WSL (and becomes zero after communicate())
484+        proc.wait()
485         stdout, stderr = proc.communicate()
486         log("exec_dialog_subprocess(%s)", cmd)
487         if stderr:
488@@ -200,9 +202,12 @@
489     ipv6 = display_desc.get("ipv6", False)
490     #ssh and command attributes:
491     username = display_desc.get("username")
492+    if "proxy_host" in display_desc and not "proxy_username" in display_desc:
493+        import getpass
494+        display_desc["proxy_username"] = getpass.getuser("username for " + display_desc["proxy_host"] + ":")
495     if not username:
496         import getpass
497-        username = getpass.getuser()
498+        username = getpass.getuser("username for " + host + ":")
499     password = display_desc.get("password")
500     target = ssh_target_string(display_desc)
501     remote_xpra = display_desc["remote_xpra"]
502@@ -249,16 +254,44 @@
503                     from paramiko.ssh_exception import ProxyCommandFailure
504                     bytestreams.CLOSED_EXCEPTIONS = tuple(list(bytestreams.CLOSED_EXCEPTIONS)+[ProxyCommandFailure])
505                     return conn
506+        from xpra.scripts.main import socket_connect
507+        from paramiko.transport import Transport
508+        from paramiko import SSHException
509+        if "proxy_host" in display_desc:
510+            proxy_host = display_desc["proxy_host"]
511+            proxy_ssh_port = display_desc.get("proxy_ssh_port", 22)
512+            proxy_username = display_desc.get("proxy_username", username)
513+            proxy_password = display_desc.get("proxy_password", password)
514+            proxy_ipv6 = display_desc.get("proxy_ipv6", False)
515+            sock = socket_connect(dtype, proxy_host, proxy_ssh_port, proxy_ipv6)
516+            middle_transport = Transport(sock)
517+            middle_transport.use_compression(False)
518+            try:
519+                middle_transport.start_client()
520+            except SSHException as e:
521+                log("start_client()", exc_info=True)
522+                raise InitException("SSH negotiation failed: %s" % e)
523+            chan_to_middle = do_ssh_paramiko_connect_to(middle_transport, proxy_host, proxy_username, proxy_password, dest_host=host, dest_port=port)
524+            transport = Transport(chan_to_middle)
525+            transport.use_compression(False)
526+            try:
527+                transport.start_client()
528+            except SSHException as e:
529+                log("start_client()", exc_info=True)
530+                raise InitException("SSH negotiation failed: %s" % e)
531+            chan = do_ssh_paramiko_connect_to(transport, host, username, password, proxy_command, remote_xpra, socket_dir, display_as_args)
532+            peername = (host, port)
533+            conn = SSHProxyCommandConnection(chan, peername, target, socket_info)
534+            conn.timeout = SOCKET_TIMEOUT
535+            conn.start_stderr_reader()
536+            return conn
537 
538         #plain TCP connection to the server,
539         #we open it then give the socket to paramiko:
540-        from xpra.scripts.main import socket_connect
541         sock = socket_connect(dtype, host, port, ipv6)
542         sockname = sock.getsockname()
543         peername = sock.getpeername()
544         log("paramiko socket_connect: sockname=%s, peername=%s", sockname, peername)
545-        from paramiko.transport import Transport
546-        from paramiko import SSHException
547         transport = Transport(sock)
548         transport.use_compression(False)
549         try:
550@@ -281,9 +314,11 @@
551     def __init__(self):
552         nomodule_context.__init__(self, "gssapi")
553 
554-
555+# (1) If the arguments after "proxy_command" are "None", then we're opening a port-forward
556+# (2) If "parachan" is set, that means we're using a port-forward
557 def do_ssh_paramiko_connect_to(transport, host, username, password,
558-                               xpra_proxy_command, remote_xpra, socket_dir, display_as_args):
559+                               xpra_proxy_command=None, remote_xpra=None, socket_dir=None, display_as_args=None,
560+                               dest_host=None, dest_port=None):
561     from paramiko import SSHException, RSAKey, PasswordRequiredException
562     from paramiko.agent import Agent
563     from paramiko.hostkeys import HostKeys
564@@ -452,6 +487,27 @@
565             log("auth_password(..)", exc_info=True)
566             log.info("SSH password authentication failed: %s", e)
567 
568+    def auth_interactive():
569+        log("trying interactive authentication")
570+        class iauthhandler:
571+            def __init__(self):
572+                self.authcount = 0
573+            def handlestuff(self, title, instructions, prompt_list):
574+                p = [ ]
575+                for pent in prompt_list:
576+                    if self.authcount == 0 and password:
577+                        p.append(password)
578+                    else:
579+                        p.append(input_pass(pent[0]))
580+                    self.authcount += 1
581+                return p
582+        try:
583+            myiauthhandler = iauthhandler()
584+            transport.auth_interactive(username, myiauthhandler.handlestuff, '')
585+        except SSHException as e:
586+            log("auth_password(..)", exc_info=True)
587+            log.info("SSH password authentication failed: %s", e)
588+
589     banner = transport.get_banner()
590     if banner:
591         log.info("SSH server banner:")
592@@ -459,12 +515,18 @@
593             log.info(" %s", x)
594 
595     log("starting authentication")
596+    # per the RFC we probably should do none first always and read off the supported
597+    # methods, however, the current code seems to work fine with OpenSSH
598     if not transport.is_authenticated() and NONE_AUTH:
599         auth_none()
600 
601+    # Some people do two-factor using KEY_AUTH to kick things off, so this happens first
602     if not transport.is_authenticated() and KEY_AUTH:
603         auth_publickey()
604 
605+    if not transport.is_authenticated() and PASSWORD_AUTH:
606+        auth_interactive()
607+
608     if not transport.is_authenticated() and PASSWORD_AUTH and password:
609         auth_password()
610 
611@@ -482,8 +544,13 @@
612 
613     if not transport.is_authenticated():
614         transport.close()
615-        raise InitException("SSH Authentication failed")
616+        # added the "host" so the user can figure out errors in ssh -> ssh mode
617+        raise InitException("SSH Authentication on " + host + " failed")
618 
619+    if(remote_xpra == None):
620+        log("Opening proxy channel")
621+        return transport.open_channel("direct-tcpip", (dest_host, dest_port), ('localhost', 0))
622+
623     assert len(remote_xpra)>0
624     log("will try to run xpra from: %s", remote_xpra)
625     for xpra_cmd in remote_xpra:
626@@ -614,6 +681,7 @@
627             kwargs["env"] = env
628         if SSH_DEBUG:
629             log.info("executing ssh command: %s" % (" ".join("\"%s\"" % x for x in cmd)))
630+       
631         child = Popen(cmd, stdin=PIPE, stdout=PIPE, **kwargs)
632     except OSError as e:
633         raise InitExit(EXIT_SSH_FAILURE, "Error running ssh command '%s': %s" % (" ".join("\"%s\"" % x for x in cmd), e))
634Index: xpra/scripts/main.py
635===================================================================
636--- xpra/scripts/main.py        (revision 21127)
637+++ xpra/scripts/main.py        (working copy)
638@@ -511,8 +511,55 @@
639         args += ["-T", host]
640     return args
641 
642+def add_ssh_proxy_args(username, password, host, ssh_port, pkey, ssh, is_putty=False, is_paramiko=False):
643+    args = []
644+    proxyline = ssh
645+    if is_putty:
646+        proxyline += ["-nc", "%host:%port"]
647+    elif not is_paramiko:
648+        proxyline += ["-W", "%h:%p"]
649+    if pkey and is_putty:
650+        # tortoise plink works with either slash, backslash needs too much escaping
651+        # because of the weird way it's passed through as a ProxyCommand
652+        proxyline += [ "-i", "\"" + pkey.replace("\\", "/") + "\""]
653+    # the double quotes are in case the password has something like "&"
654+    proxyline += add_ssh_args(username, password, host, ssh_port, is_putty, is_paramiko)
655+    if is_putty:
656+        args += ["-proxycmd", " ".join(proxyline)]
657+    elif not is_paramiko:
658+        from xpra.platform.paths import get_sshpass_command
659+        if password:
660+            sshpass_command = None
661+            sshpass_command = get_sshpass_command()
662+            if sshpass_command:
663+                proxyline.insert(0, sshpass_command)
664+                # is -e forces proxy password to match destination password
665+                proxyline.insert(1, "-e")
666+        args += ["-o", "ProxyCommand " + " ".join(proxyline)]
667+    return args
668+
669 def parse_display_name(error_cb, opts, display_name, session_name_lookup=False):
670     desc = {"display_name" : display_name}
671+    #is first hop to ssh? right now only use for ssh connections, could be quite general
672+    import re
673+    reout = re.match("(?P<base>.*?)\\?proxy=ssh:\/\/(?P<username>[^:@]+)(:(?P<password>[^@]+))?@(?P<host>[^:]+)(:(?P<ssh_port>\d+))?$", display_name)
674+    if reout:
675+        desc["proxy_username"] = reout.group("username")
676+        desc["proxy_host"] = reout.group("host")
677+        if reout.group("ssh_port"):
678+            desc["proxy_ssh_port"] = reout.group("ssh_port")
679+        else:
680+            desc["proxy_ssh_port"] = 22
681+        if reout.group("password"):
682+            desc["proxy_password"] = reout.group("password")
683+        display_name = reout.group("base")
684+    else:
685+        reout2 = re.match(".*?\\?proxy=(?P<p>[^?]*)", display_name)
686+        if reout2:
687+            from xpra.log import Logger
688+            sshlog = Logger("ssh")
689+            sshlog.error("bad proxy argument: " + reout2.group("p"))
690+   
691     #split the display name on ":" or "/"
692     scpos = display_name.find(":")
693     slpos = display_name.find("/")
694@@ -677,7 +724,9 @@
695         #ie: ssh=["/usr/bin/ssh", "-v"]
696         ssh = parse_ssh_string(opts.ssh)
697         desc["ssh"] = ssh
698-        full_ssh = ssh
699+        # python does lists by reference, full_ssh = ssh was probably a bug
700+        full_ssh = [ ]
701+        full_ssh += ssh
702 
703         #maybe restrict to win32 only?
704         ssh_cmd = ssh[0].lower()
705@@ -699,6 +748,14 @@
706         if ssh_port and ssh_port!=22:
707             desc["ssh-port"] = ssh_port
708         full_ssh += add_ssh_args(username, password, host, ssh_port, is_putty, is_paramiko)
709+        if "proxy_host" in desc:
710+            proxy_username = desc["proxy_username"]
711+            proxy_password = desc.get("proxy_password", "")
712+            proxy_host = desc["proxy_host"]
713+            proxy_ssh_port = desc.get("proxy_ssh_port",22)
714+            proxy_pkey_path = desc.get("proxy_pkey_path", "")
715+            full_ssh += add_ssh_proxy_args(proxy_username, proxy_password, proxy_host, proxy_ssh_port,
716+                                           proxy_pkey_path, ssh, is_putty, is_paramiko)
717         desc.update({
718                      "host"     : host,
719                      "full_ssh" : full_ssh