xpra icon
Bug tracker and wiki

Ticket #2041: ssh-2hop-v4.patch

File ssh-2hop-v4.patch, 35.4 KB (added by Antoine Martin, 18 months ago)

updated patch

  • man/xpra.1

     
    8888Connect using websocket protocol.
    8989.SS wss://[USERNAME[:PASSWORD]@]HOST:PORT/[DISPLAY]
    9090Connect using secure websocket protocol. (websocket with SSL)
    91 .SS ssh://[USERNAME[:PASSWORD]@]HOST[:SSH_PORT]/DISPLAY
     91.SS ssh://[USERNAME[:PASSWORD]@]HOST[:SSH_PORT]/DISPLAY[?proxy=ssh://[USERNAME[:PASSWORD]@]HOST[:SSH_PORT]]
    9292Further options can be specified using the \fB\-\-ssh\fP command line option.
    9393.P
    9494For backwards compatibility, SSH mode also supports the syntax:
     
    101101The password need only be specified when the server authentication module
    102102requires it. (ie: often when authenticating against MS Windows servers,
    103103or with \fBmultifile\fP and \fBsqlite\fP authentication modules)
     104.P
     105When the "?proxy=" option is set XPRA will establish an SSH connection to the
     106specified "proxy" host.  From that host XPRA will set up an SSH connection to
     107the XPRA server.
    104108.\" --------------------------------------------------------------------
    105109.SH EXAMPLES
    106110.TP \w'xpra\ 'u
  • xpra/client/gtk_base/client_launcher.py

     
    4040from xpra.client.gtk_base.gtk_tray_menu_base import make_min_auto_menu, make_encodingsmenu, \
    4141                                    MIN_QUALITY_OPTIONS, QUALITY_OPTIONS, MIN_SPEED_OPTIONS, SPEED_OPTIONS
    4242from xpra.gtk_common.about import about
    43 from xpra.scripts.main import connect_to, make_client, configure_network, is_local, add_ssh_args, parse_ssh_string
     43from xpra.scripts.main import (
     44    connect_to, make_client, configure_network, is_local,
     45    add_ssh_args, parse_ssh_string, add_ssh_proxy_args,
     46    )
    4447from xpra.platform.paths import get_icon_dir
    4548from xpra.platform import get_username
    4649from xpra.log import Logger, enable_debug_for
     
    4750log = Logger("launcher")
    4851
    4952#what we save in the config file:
    50 SAVED_FIELDS = ["username", "password", "host", "port", "mode", "ssh_port",
    51                 "encoding", "quality", "min-quality", "speed", "min-speed"]
     53SAVED_FIELDS = [
     54    "username", "password", "host", "port", "mode", "ssh_port",
     55    "encoding", "quality", "min-quality", "speed", "min-speed",
     56    "proxy_port", "proxy_username", "proxy_key", "proxy_password",
     57    ]
    5258
    5359#options not normally found in xpra config file
    5460#but which can be present in a launcher config:
     
    6066                        "mode"              : str,
    6167                        "autoconnect"       : bool,
    6268                        "ssh_port"          : int,
     69                        "proxy_host"        : str,
     70                        "proxy_port"        : int,
     71                        "proxy_username"    : str,
     72                        "proxy_password"    : str,
     73                        "proxy_key"         : str,
    6374                        }
    6475LAUNCHER_DEFAULTS = {
    6576                        "host"              : "",
     
    6677                        "port"              : -1,
    6778                        "username"          : get_username(),
    6879                        "password"          : "",
    69                         "mode"              : "tcp",    #tcp,ssh,..
     80                        "mode"              : "ssh -> ssh",    #tcp,ssh,..
    7081                        "autoconnect"       : False,
    7182                        "ssh_port"          : 22,
     83                        "proxy_host"        : "",
     84                        "proxy_port"        : 22,
     85                        "proxy_username"    : get_username(),
     86                        "proxy_password"    : "",
     87                        "proxy_key"   : "",
    7288                    }
    7389
    7490
     
    113129    def __init__(self):
    114130        # Default connection options
    115131        self.config = make_defaults_struct(extras_defaults=LAUNCHER_DEFAULTS, extras_types=LAUNCHER_OPTION_TYPES, extras_validation=self.get_launcher_validation())
     132        ssh_cmd = parse_ssh_string(self.config.ssh)[0].strip().lower()
     133        self.is_putty = ssh_cmd.endswith("plink") or ssh_cmd.endswith("plink.exe")
     134        self.is_paramiko = ssh_cmd=="paramiko"
    116135        #TODO: the fixup does not belong here?
    117136        from xpra.scripts.main import fixup_options
    118137        fixup_options(self.config)
     
    127146
    128147    def get_connection_modes(self):
    129148        modes = ["ssh"]
     149        modes.append("ssh -> ssh")
    130150        try:
    131151            import ssl
    132152            assert ssl
     
    221241        vbox.pack_start(hbox)
    222242
    223243        # Mode:
    224         hbox = gtk.HBox(False, 20)
    225         hbox.set_spacing(20)
    226         hbox.pack_start(gtk.Label("Mode: "))
     244        hbox = gtk.HBox(False, 5)
    227245        self.mode_combo = gtk.combo_box_new_text()
    228246        for x in self.get_connection_modes():
    229247            self.mode_combo.append_text(x.upper())
    230248        self.mode_combo.connect("changed", self.mode_changed)
    231         hbox.pack_start(self.mode_combo)
    232         vbox.pack_start(hbox)
     249        hbox.pack_start(gtk.Label("Mode: "), False, False)
     250        hbox.pack_start(self.mode_combo, False, False)
     251        align_hbox = gtk.Alignment(xalign = .5)
     252        align_hbox.add(hbox)
     253        vbox.pack_start(align_hbox)
    233254
    234         # Username@Host:Port
    235         hbox = gtk.HBox(False, 0)
    236         hbox.set_spacing(5)
     255        # Username@Host:Port (ssh -> ssh, proxy)
     256        vbox_proxy = gtk.VBox(False, 15)
     257        hbox = gtk.HBox(False, 5)
     258        self.proxy_vbox = vbox_proxy
     259        self.proxy_username_entry = gtk.Entry()
     260        self.proxy_username_entry.set_max_length(128)
     261        self.proxy_username_entry.set_width_chars(16)
     262        self.proxy_username_entry.connect("changed", self.validate)
     263        self.proxy_username_entry.connect("activate", self.connect_clicked)
     264        self.proxy_username_entry.set_tooltip_text("username")
     265        self.proxy_host_entry = gtk.Entry()
     266        self.proxy_host_entry.set_max_length(128)
     267        self.proxy_host_entry.set_width_chars(24)
     268        self.proxy_host_entry.connect("changed", self.validate)
     269        self.proxy_host_entry.connect("activate", self.connect_clicked)
     270        self.proxy_host_entry.set_tooltip_text("hostname")
     271        self.proxy_port_entry = gtk.Entry()
     272        self.proxy_port_entry.set_max_length(5)
     273        self.proxy_port_entry.set_width_chars(5)
     274        self.proxy_port_entry.connect("changed", self.validate)
     275        self.proxy_port_entry.connect("activate", self.connect_clicked)
     276        self.proxy_port_entry.set_tooltip_text("SSH port")
     277        hbox.pack_start(gtk.Label("Proxy: "), False, False)
     278        hbox.pack_start(self.proxy_username_entry, True, True)
     279        hbox.pack_start(gtk.Label("@"), False, False)
     280        hbox.pack_start(self.proxy_host_entry, True, True)
     281        hbox.pack_start(self.proxy_port_entry, False, False)
     282        vbox_proxy.pack_start(hbox)
     283
     284        # Password
     285        hbox = gtk.HBox(False, 5)
     286        self.proxy_password_entry = gtk.Entry()
     287        self.proxy_password_entry.set_max_length(128)
     288        self.proxy_password_entry.set_width_chars(30)
     289        self.proxy_password_entry.set_text("")
     290        self.proxy_password_entry.set_visibility(False)
     291        self.proxy_password_entry.connect("changed", self.password_ok)
     292        self.proxy_password_entry.connect("changed", self.validate)
     293        self.proxy_password_entry.connect("activate", self.connect_clicked)
     294        self.proxy_password_label = gtk.Label("Proxy Password")
     295        hbox.pack_start(self.proxy_password_label, False, False)
     296        hbox.pack_start(self.proxy_password_entry, True, True)
     297        vbox_proxy.pack_start(hbox)
     298
     299        # Private key
     300        hbox = gtk.HBox(False,  5)
     301        self.pkey_hbox = hbox
     302        self.proxy_key_label = gtk.Label("Proxy private key path (PPK):")
     303        self.proxy_key_entry = gtk.Entry()
     304        self.proxy_key_browse = gtk.Button("Browse")
     305        self.proxy_key_browse.connect("clicked", self.proxy_key_browse_clicked)
     306        hbox.pack_start(self.proxy_key_label, False, False)
     307        hbox.pack_start(self.proxy_key_entry, True, True)
     308        hbox.pack_start(self.proxy_key_browse, False, False)
     309        vbox_proxy.pack_start(hbox)
     310
     311        # Check boxes
     312        hbox = gtk.HBox(False, 5)
     313        self.password_scb = gtk.CheckButton("Server password same as proxy")
     314        self.password_scb.set_mode(True)
     315        self.password_scb.set_active(True)
     316        self.password_scb.connect("toggled", self.validate)
     317        align_password_scb = gtk.Alignment(xalign = 1.0)
     318        align_password_scb.add(self.password_scb)
     319        self.username_scb = gtk.CheckButton("Server username same as proxy")
     320        self.username_scb.set_mode(True)
     321        self.username_scb.set_active(True)
     322        self.username_scb.connect("toggled", self.validate)
     323        align_username_scb = gtk.Alignment(xalign = 0.0)
     324        align_username_scb.add(self.username_scb)
     325        hbox.pack_start(align_username_scb, True, True)
     326        hbox.pack_start(align_password_scb, True, True)
     327        vbox_proxy.pack_start(hbox)
     328
     329        # coniditonal stuff that goes away for "normal" ssh
     330        vbox.pack_start(vbox_proxy)
     331
     332        # Username@Host:Port (main)
     333        hbox = gtk.HBox(False, 5)
    237334        self.username_entry = gtk.Entry()
    238335        self.username_entry.set_max_length(128)
    239336        self.username_entry.set_width_chars(16)
     
    240337        self.username_entry.connect("changed", self.validate)
    241338        self.username_entry.connect("activate", self.connect_clicked)
    242339        self.username_entry.set_tooltip_text("username")
    243         self.username_label = gtk.Label("@")
    244340        self.host_entry = gtk.Entry()
    245341        self.host_entry.set_max_length(128)
    246342        self.host_entry.set_width_chars(24)
     
    259355        self.port_entry.connect("changed", self.validate)
    260356        self.port_entry.connect("activate", self.connect_clicked)
    261357        self.port_entry.set_tooltip_text("port/display")
    262 
    263         hbox.pack_start(self.username_entry)
    264         hbox.pack_start(self.username_label)
    265         hbox.pack_start(self.host_entry)
    266         hbox.pack_start(self.ssh_port_entry)
    267         hbox.pack_start(gtk.Label(":"))
    268         hbox.pack_start(self.port_entry)
     358        hbox.pack_start(gtk.Label("Server:"), False, False)
     359        hbox.pack_start(self.username_entry, True, True)
     360        hbox.pack_start(gtk.Label("@"), False, False)
     361        hbox.pack_start(self.host_entry, True, True)
     362        hbox.pack_start(self.ssh_port_entry, False, False)
     363        hbox.pack_start(gtk.Label(":"), False, False)
     364        hbox.pack_start(self.port_entry, False, False)
    269365        vbox.pack_start(hbox)
    270366
    271367        # Password
    272         hbox = gtk.HBox(False, 0)
    273         hbox.set_spacing(20)
     368        hbox = gtk.HBox(False, 5)
    274369        self.password_entry = gtk.Entry()
    275370        self.password_entry.set_max_length(128)
    276371        self.password_entry.set_width_chars(30)
     
    278373        self.password_entry.set_visibility(False)
    279374        self.password_entry.connect("changed", self.password_ok)
    280375        self.password_entry.connect("changed", self.validate)
    281         self.password_label = gtk.Label("Password: ")
    282         hbox.pack_start(self.password_label)
    283         hbox.pack_start(self.password_entry)
     376        self.password_label = gtk.Label("Server Password:")
     377        hbox.pack_start(self.password_label, False, False)
     378        hbox.pack_start(self.password_entry, True, True)
    284379        vbox.pack_start(hbox)
    285380
    286381        #strict host key check for SSL and SSH
    287         hbox = gtk.HBox(False, 0)
    288         hbox.set_spacing(20)
     382        hbox = gtk.HBox(False, 5)
    289383        self.nostrict_host_check = gtk.CheckButton("Disable Strict Host Key Check")
    290384        self.nostrict_host_check.set_active(False)
    291385        al = gtk.Alignment(xalign=0.5, yalign=0.5, xscale=0.0, yscale=0)
     
    412506
    413507    def validate(self, *args):
    414508        ssh = self.mode_combo.get_active_text()=="SSH"
     509        sshtossh = self.mode_combo.get_active_text()=="SSH -> SSH"
    415510        errs = []
    416511        host = self.host_entry.get_text()
    417512        errs.append((self.host_entry, not bool(host), "specify the host"))
    418         if ssh:
     513        if ssh or sshtossh:
    419514            #validate ssh port:
    420515            ssh_port = self.ssh_port_entry.get_text()
    421516            try:
     
    423518            except:
    424519                ssh_port = -1
    425520            errs.append((self.ssh_port_entry, ssh_port<0 or ssh_port>=2**16, "invalid SSH port number"))
     521        if sshtossh:
     522            #validate ssh port:
     523            proxy_port = self.proxy_port_entry.get_text()
     524            try:
     525                proxy_port = int(proxy_port)
     526            except:
     527                proxy_port = -1
     528            errs.append((self.proxy_port_entry, proxy_port<0 or proxy_port>=2**16, "invalid SSH port number"))
    426529        port = self.port_entry.get_text()
    427         if ssh and not port:
     530        if sshtossh:
     531            if self.password_scb.get_active():
     532                self.password_entry.set_sensitive(False)
     533                self.password_entry.set_text(self.proxy_password_entry.get_text())
     534            else:
     535                self.password_entry.set_sensitive(True)
     536            if self.username_scb.get_active():
     537                self.username_entry.set_sensitive(False)
     538                self.username_entry.set_text(self.proxy_username_entry.get_text())
     539            else:
     540                self.username_entry.set_sensitive(True)
     541            errs.append((self.proxy_host_entry, not bool(self.proxy_host_entry.get_text()), "specify the proxy host"))
     542        # check username *after* the checkbox action
     543        if ssh or sshtossh:
     544            errs.append((self.username_entry, not bool(self.username_entry.get_text()), "specify username"))
     545        if sshtossh:
     546            errs.append((self.proxy_username_entry, not bool(self.proxy_username_entry.get_text()), "specify proxy username"))
     547        if ssh or sshtossh and not port:
    428548            port = 0        #port optional with ssh
    429549        else:
    430550            try:
     
    460580    def mode_changed(self, *_args):
    461581        mode = self.mode_combo.get_active_text().lower()
    462582        ssh = mode=="ssh"
    463         if ssh:
     583        sshtossh = mode=="ssh -> ssh"
     584        if ssh or sshtossh:
    464585            self.port_entry.set_tooltip_text("Display number (optional)")
    465586            self.port_entry.set_text("")
    466             self.ssh_port_entry.set_text("22")
    467587            self.ssh_port_entry.show()
    468588            self.password_entry.set_tooltip_text("SSH Password")
    469589            self.username_entry.set_tooltip_text("SSH Username")
     590            if ssh:
     591                self.proxy_vbox.hide()
     592                self.password_scb.hide()
     593                self.password_entry.set_sensitive(True)
     594                self.username_entry.set_sensitive(True)
     595            if sshtossh:
     596                self.proxy_vbox.show()
     597                self.password_scb.show()
    470598        else:
     599            self.password_entry.set_sensitive(True)
     600            self.username_entry.set_sensitive(True)
     601            self.proxy_vbox.hide()
    471602            self.ssh_port_entry.hide()
    472603            self.ssh_port_entry.set_text("")
    473604            port_str = self.port_entry.get_text()
     
    479610            if self.config.port>0:
    480611                self.port_entry.set_text("%s" % self.config.port)
    481612        can_use_password = True
    482         if ssh:
    483             ssh_cmd = parse_ssh_string(self.config.ssh)[0].strip().lower()
    484             is_putty = ssh_cmd.endswith("plink") or ssh_cmd.endswith("plink.exe")
    485             is_paramiko = ssh_cmd=="paramiko"
    486             if is_putty or is_paramiko:
     613        if ssh or sshtossh:
     614            if not self.is_putty:
     615                self.proxy_key_entry.set_text("OpenSSH/Paramiko use ~/.ssh")
     616                self.proxy_key_entry.set_editable(False)
     617                self.proxy_key_entry.set_sensitive(False)
     618                self.proxy_key_browse.hide()
     619            if self.is_paramiko or self.is_putty:
    487620                can_use_password = True
    488621            else:
    489622                #we can also use password if sshpass is installed:
     
    493626        if can_use_password:
    494627            self.password_label.show()
    495628            self.password_entry.show()
     629            if sshtossh:
     630                self.proxy_password_label.show()
     631                self.proxy_password_entry.show()
    496632        else:
    497633            self.password_label.hide()
    498634            self.password_entry.hide()
     635            if sshtossh:
     636                self.proxy_password_label.hide()
     637                self.proxy_password_entry.hide()
    499638        self.validate()
    500639        if mode=="ssl" or (mode=="ssh" and not WIN32):
    501640            self.nostrict_host_check.show()
     
    531670    def reset_errors(self):
    532671        self.set_sensitive(True)
    533672        self.set_info_text("")
    534         for widget in (self.info, self.password_entry, self.username_entry, self.host_entry, self.port_entry):
     673        for widget in (
     674            self.info, self.password_entry, self.username_entry, self.host_entry,
     675            self.port_entry, self.proxy_password_entry, self.proxy_username_entry,
     676            self.proxy_host_entry, self.proxy_port_entry, self.ssh_port_entry,
     677            ):
    535678            self.set_widget_fg_color(self.info, False)
    536679            self.set_widget_bg_color(widget, False)
    537680
     
    546689    def set_sensitive(self, s):
    547690        glib.idle_add(self.window.set_sensitive, s)
    548691
     692    def choose_pkey_file(self, title, action, action_button, callback):
     693        file_filter = gtk.FileFilter()
     694        file_filter.set_name("All Files")
     695        file_filter.add_pattern("*")
     696        choose_file(self.window, title, action, action_button, callback, file_filter)
     697
     698    def proxy_key_browse_clicked(self, *args):
     699        log("proxy_key_browse_clicked%s", args)
     700        def do_choose(filename):
     701            #make sure the file extension is .ppk
     702            if os.path.splitext(filename)[-1]!=".ppk":
     703                filename += ".ppk"
     704            self.proxy_key_entry.set_text(filename)
     705        self.choose_pkey_file("Choose SSH private key", FILE_CHOOSER_ACTION_OPEN, gtk.STOCK_OPEN, do_choose)
     706
    549707    def connect_clicked(self, *args):
    550708        log("connect_clicked%s", args)
    551709        self.update_options_from_gui()
     
    591749        #cooked vars used by connect_to
    592750        params = {"type"    : self.config.mode}
    593751        username = self.config.username
    594         if self.config.mode=="ssh":
     752        if self.config.mode=="ssh" or self.config.mode=="ssh -> ssh":
    595753            if self.config.socket_dir:
    596754                params["socket_dir"] = self.config.socket_dir
    597755            params["remote_xpra"] = self.config.remote_xpra
     
    602760            else:
    603761                params["display"] = "auto"
    604762                params["display_as_args"] = []
    605             full_ssh = parse_ssh_string(self.config.ssh)
    606             ssh_cmd = full_ssh[0].lower()
    607             is_putty = ssh_cmd.endswith("plink") or ssh_cmd.endswith("plink.exe")
    608             is_paramiko = ssh_cmd=="paramiko"
    609             params["is_putty"] = is_putty
    610             params["is_paramiko"] = is_paramiko
     763            params["ssh"] = self.config.ssh
     764            params["is_putty"] = self.is_putty
     765            params["is_paramiko"] = self.is_paramiko
    611766            password = self.config.password
    612767            host = self.config.host
    613768            upos = host.find("@")
     
    622777                    username = username[:ppos]
    623778            if self.config.ssh_port and self.config.ssh_port!=22:
    624779                params["ssh-port"] = self.config.ssh_port
    625             full_ssh += add_ssh_args(username, password, host, self.config.ssh_port, is_putty, is_paramiko)
     780            ssh_cmd = parse_ssh_string(self.config.ssh)
     781            self.is_putty = ssh_cmd.endswith("plink") or ssh_cmd.endswith("plink.exe")
     782            self.is_paramiko = ssh_cmd=="paramiko"
     783            full_ssh = ssh_cmd[:]
     784            full_ssh += add_ssh_args(username, password, host, self.config.ssh_port, self.is_putty, self.is_paramiko)
    626785            if username:
    627786                params["username"] = username
    628787            if self.nostrict_host_check.get_active():
    629788                full_ssh += ["-o", "StrictHostKeyChecking=no"]
     789            if params["type"] == "ssh -> ssh":
     790                params["type"] = "ssh"
     791                params["proxy_host"] = self.config.proxy_host
     792                params["proxy_port"] = self.config.proxy_port
     793                params["proxy_username"] = self.config.proxy_username
     794                params["proxy_password"] = self.config.proxy_password
     795                full_ssh += add_ssh_proxy_args(self.config.proxy_username, self.config.proxy_password,
     796                                               self.config.proxy_host, self.config.proxy_port,
     797                                               self.config.proxy_key, ssh_cmd,
     798                                               self.is_putty, self.is_paramiko)
    630799            params["host"] = host
    631800            params["local"] = is_local(self.config.host)
    632801            params["full_ssh"] = full_ssh
     
    788957        self.config.ssh_port = pint(self.ssh_port_entry.get_text())
    789958        self.config.port = pint(self.port_entry.get_text())
    790959        self.config.username = self.username_entry.get_text()
     960        self.config.password = self.password_entry.get_text()
     961
     962        self.config.proxy_host = self.proxy_host_entry.get_text()
     963        self.config.proxy_port = pint(self.proxy_port_entry.get_text())
     964        self.config.proxy_username = self.proxy_username_entry.get_text()
     965        self.config.proxy_password = self.proxy_password_entry.get_text()
     966        if self.is_putty:
     967            self.config.proxy_key = self.proxy_key_entry.get_text()
     968
    791969        self.config.encoding = self.get_selected_encoding() or self.config.encoding
    792970        mode_enc = self.mode_combo.get_active_text().lower()
    793971        if mode_enc.startswith("tcp"):
     
    794972            self.config.mode = "tcp"
    795973            if mode_enc.find("aes")>0:
    796974                self.config.encryption = "AES"
    797         elif mode_enc in ("ssl", "ssh", "ws", "wss"):
     975        elif mode_enc in ("ssl", "ssh", "ws", "wss", "ssh -> ssh"):
    798976            self.config.mode = mode_enc
    799977            self.config.encryption = ""
    800         self.config.password = self.password_entry.get_text()
    801978        log("update_options_from_gui() %s", (self.config.username, self.config.password, self.config.mode, self.config.encryption, self.config.host, self.config.port, self.config.ssh_port, self.config.encoding))
    802979
    803980    def update_gui_from_config(self):
     
    8301007                pass
    8311008            return str(default_port)
    8321009        dport = DEFAULT_PORT
    833         if mode=="ssh":
     1010        if mode=="ssh" or mode=="ssh -> ssh":
    8341011            #not required, so don't specify one
    8351012            dport = ""
    8361013        self.port_entry.set_text(get_port(self.config.port, dport))
    8371014        self.ssh_port_entry.set_text(get_port(self.config.ssh_port))
     1015        #proxy bits:
     1016        self.proxy_host_entry.set_text(self.config.proxy_host)
     1017        self.proxy_port_entry.set_text(get_port(self.config.proxy_port))
     1018        self.proxy_username_entry.set_text(self.config.proxy_username)
     1019        self.proxy_password_entry.set_text(self.config.proxy_password)
     1020        if self.is_putty:
     1021            self.proxy_key_entry.set_text(self.config.proxy_key)
    8381022
    8391023    def close_window(self, *_args):
    8401024        w = self.window
  • xpra/net/ssh.py

     
    1515
    1616from xpra.scripts.main import InitException, InitExit, shellquote
    1717from xpra.platform.paths import get_xpra_command, get_ssh_known_hosts_files
     18from xpra.platform import get_username
    1819from xpra.net.bytestreams import SocketConnection, SOCKET_TIMEOUT, ConnectionClosedException
    1920from xpra.exit_codes import EXIT_SSH_KEY_FAILURE, EXIT_SSH_FAILURE
    20 from xpra.os_util import bytestostr, osexpand, monotonic_time, setsid, nomodule_context, umask_context, is_WSL, WIN32, OSX, POSIX
     21from xpra.os_util import bytestostr, osexpand, monotonic_time, setsid, nomodule_context, umask_context, WIN32, OSX, POSIX
    2122from xpra.util import envint, envbool, nonl, engs
    2223
    2324INITENV_COMMAND = os.environ.get("XPRA_INITENV_COMMAND", "xpra initenv")
     
    202203    port = display_desc.get("ssh-port", 22)
    203204    ipv6 = display_desc.get("ipv6", False)
    204205    #ssh and command attributes:
    205     username = display_desc.get("username")
    206     if not username:
    207         import getpass
    208         username = getpass.getuser()
     206    username = display_desc.get("username") or get_username()
     207    if "proxy_host" in display_desc:
     208        display_desc.setdefault("proxy_username", get_username())
    209209    password = display_desc.get("password")
    210210    target = ssh_target_string(display_desc)
    211211    remote_xpra = display_desc["remote_xpra"]
     
    252252                    from paramiko.ssh_exception import ProxyCommandFailure
    253253                    bytestreams.CLOSED_EXCEPTIONS = tuple(list(bytestreams.CLOSED_EXCEPTIONS)+[ProxyCommandFailure])
    254254                    return conn
     255        from xpra.scripts.main import socket_connect
     256        from paramiko.transport import Transport
     257        from paramiko import SSHException
     258        if "proxy_host" in display_desc:
     259            proxy_host = display_desc["proxy_host"]
     260            proxy_port = display_desc.get("proxy_port", 22)
     261            proxy_username = display_desc.get("proxy_username", username)
     262            proxy_password = display_desc.get("proxy_password", password)
     263            proxy_ipv6 = display_desc.get("proxy_ipv6", False)
     264            sock = socket_connect(dtype, proxy_host, proxy_port, proxy_ipv6)
     265            middle_transport = Transport(sock)
     266            middle_transport.use_compression(False)
     267            try:
     268                middle_transport.start_client()
     269            except SSHException as e:
     270                log("start_client()", exc_info=True)
     271                raise InitException("SSH negotiation failed: %s" % e)
     272            chan_to_middle = do_ssh_paramiko_connect_to(middle_transport, proxy_host, proxy_username, proxy_password, dest_host=host, dest_port=port)
     273            transport = Transport(chan_to_middle)
     274            transport.use_compression(False)
     275            try:
     276                transport.start_client()
     277            except SSHException as e:
     278                log("start_client()", exc_info=True)
     279                raise InitException("SSH negotiation failed: %s" % e)
     280            chan = do_ssh_paramiko_connect_to(transport, host, username, password, proxy_command, remote_xpra, socket_dir, display_as_args)
     281            peername = (host, port)
     282            conn = SSHProxyCommandConnection(chan, peername, target, socket_info)
     283            conn.timeout = SOCKET_TIMEOUT
     284            conn.start_stderr_reader()
     285            return conn
    255286
    256287        #plain TCP connection to the server,
    257288        #we open it then give the socket to paramiko:
    258         from xpra.scripts.main import socket_connect
    259289        sock = socket_connect(dtype, host, port, ipv6)
    260290        sockname = sock.getsockname()
    261291        peername = sock.getpeername()
    262292        log("paramiko socket_connect: sockname=%s, peername=%s", sockname, peername)
    263         from paramiko.transport import Transport
    264         from paramiko import SSHException
    265293        transport = Transport(sock)
    266294        transport.use_compression(False)
    267295        try:
     
    284312    def __init__(self):
    285313        nomodule_context.__init__(self, "gssapi")
    286314
    287 
     315# (1) If the arguments after "proxy_command" are "None", then we're opening a port-forward
     316# (2) If "parachan" is set, that means we're using a port-forward
    288317def do_ssh_paramiko_connect_to(transport, host, username, password,
    289                                xpra_proxy_command, remote_xpra, socket_dir, display_as_args):
     318                               xpra_proxy_command=None, remote_xpra=None, socket_dir=None, display_as_args=None,
     319                               dest_host=None, dest_port=None):
    290320    from paramiko import SSHException, RSAKey, PasswordRequiredException
    291321    from paramiko.agent import Agent
    292322    from paramiko.hostkeys import HostKeys
     
    455485            log("auth_password(..)", exc_info=True)
    456486            log.info("SSH password authentication failed: %s", e)
    457487
     488    def auth_interactive():
     489        log("trying interactive authentication")
     490        class iauthhandler:
     491            def __init__(self):
     492                self.authcount = 0
     493            def handlestuff(self, title, instructions, prompt_list):
     494                p = []
     495                for pent in prompt_list:
     496                    if self.authcount==0 and password:
     497                        p.append(password)
     498                    else:
     499                        p.append(input_pass(pent[0]))
     500                    self.authcount += 1
     501                return p
     502        try:
     503            myiauthhandler = iauthhandler()
     504            transport.auth_interactive(username, myiauthhandler.handlestuff, "")
     505        except SSHException as e:
     506            log("auth_interactive(..)", exc_info=True)
     507            log.info("SSH password authentication failed: %s", e)
     508
    458509    banner = transport.get_banner()
    459510    if banner:
    460511        log.info("SSH server banner:")
     
    462513            log.info(" %s", x)
    463514
    464515    log("starting authentication")
     516    # per the RFC we probably should do none first always and read off the supported
     517    # methods, however, the current code seems to work fine with OpenSSH
    465518    if not transport.is_authenticated() and NONE_AUTH:
    466519        auth_none()
    467520
     521    # Some people do two-factor using KEY_AUTH to kick things off, so this happens first
    468522    if not transport.is_authenticated() and KEY_AUTH:
    469523        auth_publickey()
    470524
     525    if not transport.is_authenticated() and PASSWORD_AUTH:
     526        auth_interactive()
     527
    471528    if not transport.is_authenticated() and PASSWORD_AUTH and password:
    472529        auth_password()
    473530
     
    485542
    486543    if not transport.is_authenticated():
    487544        transport.close()
    488         raise InitException("SSH Authentication failed")
     545        raise InitException("SSH Authentication on %s failed" % host)
    489546
     547    if remote_xpra is None:
     548        log("Opening proxy channel")
     549        return transport.open_channel("direct-tcpip", (dest_host, dest_port), ('localhost', 0))
     550
    490551    assert len(remote_xpra)>0
    491552    log("will try to run xpra from: %s", remote_xpra)
    492553    for xpra_cmd in remote_xpra:
  • xpra/scripts/main.py

     
    511511        args += ["-T", host]
    512512    return args
    513513
     514def add_ssh_proxy_args(username, password, host, ssh_port, pkey, ssh, is_putty=False, is_paramiko=False):
     515    args = []
     516    proxyline = ssh
     517    if is_putty:
     518        proxyline += ["-nc", "%host:%port"]
     519        if pkey:
     520            # tortoise plink works with either slash, backslash needs too much escaping
     521            # because of the weird way it's passed through as a ProxyCommand
     522            proxyline += [ "-i", "\"" + pkey.replace("\\", "/") + "\""]
     523    elif not is_paramiko:
     524        proxyline += ["-W", "%h:%p"]
     525    # the double quotes are in case the password has something like "&"
     526    proxyline += add_ssh_args(username, password, host, ssh_port, is_putty, is_paramiko)
     527    if is_putty:
     528        args += ["-proxycmd", " ".join(proxyline)]
     529    elif not is_paramiko:
     530        from xpra.platform.paths import get_sshpass_command
     531        if password:
     532            sshpass_command = get_sshpass_command()
     533            if sshpass_command:
     534                proxyline.insert(0, sshpass_command)
     535                # is -e forces proxy password to match destination password
     536                proxyline.insert(1, "-e")
     537        args += ["-o", "ProxyCommand " + " ".join(proxyline)]
     538    return args
     539
     540
     541def parse_proxy_attributes(display_name):
     542    import re
     543    # Notes:
     544    # (1) this regex permits a "?" in the password or username (because not just splitting at "?").
     545    #     It doesn't look for the next  "?" until after the "@", where a "?" really indicates
     546    #     another field.
     547    # (2) all characters including "@"s go to "userpass" until the *last* "@" after which it all goes
     548    #     to "hostport"
     549    reout = re.search("\\?proxy=(?P<p>((?P<userpass>.+)@)?(?P<hostport>[^?]+))", display_name)
     550    if not reout:
     551        return display_name, {}
     552    try:
     553        desc_tmp = dict()
     554        # This one should *always* return a host, and should end with an optional numeric port
     555        hostport = reout.group("hostport")
     556        hostport_match = re.match("(?P<host>[^:]+)($|:(?P<port>\d+)$)", hostport)
     557        if not hostport_match:
     558            raise RuntimeError("bad format for 'hostport': '%s'" % hostport)
     559        host = hostport_match.group("host")
     560        if not host:
     561            raise RuntimeError("bad format: missing host in '%s'" % hostport)
     562        desc_tmp["proxy_host"] = host
     563        if hostport_match.group("port"):
     564            desc_tmp["proxy_port"] = hostport_match.group("ssh_port")
     565        userpass = reout.group("userpass")
     566        if userpass:
     567            # The username ends at the first colon. This decision was not unique: I could have
     568            # allowed one colon in username if there were two in the string.
     569            userpass_match = re.match("(?P<username>[^:]+)(:(?P<password>.+))?", userpass)
     570            if not userpass_match:
     571                raise RuntimeError("bad format for 'userpass': '%s'" % userpass)
     572            # If there is a "userpass" part, then it *must* have a username
     573            username = userpass_match.group("username")
     574            if not username:
     575                raise RuntimeError("bad format: missing username in '%s'" % userpass)
     576            desc_tmp["proxy_username"] = username
     577            password = userpass_match.group("password")
     578            if password:
     579                desc_tmp["proxy_password"] = password
     580    except RuntimeError:
     581        from xpra.log import Logger
     582        sshlog = Logger("ssh")
     583        sshlog.error("bad proxy argument: " + reout.group(0))
     584        return display_name, {}
     585    else:
     586        # rip out the part we've processed
     587        display_name = display_name[:reout.start()] + display_name[reout.end():]
     588        return display_name, desc_tmp
     589
    514590def parse_display_name(error_cb, opts, display_name, session_name_lookup=False):
    515591    desc = {"display_name" : display_name}
     592    display_name, proxy_attrs = parse_proxy_attributes(display_name)
     593    desc.update(proxy_attrs)
     594
    516595    #split the display name on ":" or "/"
    517596    scpos = display_name.find(":")
    518597    slpos = display_name.find("/")
     
    676755            host = parts[0]
    677756        #ie: ssh=["/usr/bin/ssh", "-v"]
    678757        ssh = parse_ssh_string(opts.ssh)
    679         full_ssh = ssh
     758        full_ssh = ssh[:]
    680759
    681760        #maybe restrict to win32 only?
    682761        ssh_cmd = ssh[0].lower()
     
    698777        if ssh_port and ssh_port!=22:
    699778            desc["ssh-port"] = ssh_port
    700779        full_ssh += add_ssh_args(username, password, host, ssh_port, is_putty, is_paramiko)
     780        if "proxy_host" in desc:
     781            proxy_username = desc.get("proxy_username", "")
     782            proxy_password = desc.get("proxy_password", "")
     783            proxy_host = desc["proxy_host"]
     784            proxy_port = desc.get("proxy_port", 22)
     785            proxy_key = desc.get("proxy_key", "")
     786            full_ssh += add_ssh_proxy_args(proxy_username, proxy_password, proxy_host, proxy_port,
     787                                           proxy_key, ssh, is_putty, is_paramiko)
    701788        desc.update({
    702789                     "host"     : host,
    703790                     "full_ssh" : full_ssh