Exemple #1
0
def openssh_connect(user,
                    host,
                    port=22,
                    config=None,
                    command=None,
                    password=None,
                    env=None,
                    socket=None,
                    sshfp=False,
                    randomart=False,
                    identities=None,
                    additional_args=None,
                    debug=False):
    """
    Starts an interactive SSH session to the given host as the given user on the
    given port.

    If *command* isn't given, the equivalent of "which ssh" will be used to
    determine the full path to the ssh executable.  Otherwise *command* will be
    used.

    If a password is given, that will be passed to SSH when prompted.

    If *env* (dict) is given, that will be used for the shell env when opening
    the SSH connection.

    If *socket* (a file path) is given, this will be passed to the SSH command
    as -S<socket>.  If the socket does not exist, ssh's Master mode switch will
    be set (-M) automatically.  This allows sessions to be duplicated
    automatically.

    If *sshfp* resolves to True, SSHFP (DNS-based host verification) support
    will be enabled.

    If *randomart* resolves to True, the VisualHostKey (randomart hash) option
    will be enabled to display randomart when the connection is made.

    If *identities* given (may be a list or just a single string), it/those will
    be passed to the ssh command to use when connecting (e.g. -i/identity/path).

    If *additional_args* is given this value (or values if it is a list) will be
    added to the arguments passed to the ssh command.

    If *debug* is ``True`` then '-vvv' will be passed to the ssh command.
    """
    try:
        int(port)
    except ValueError:
        print(_("The port must be an integer < 65535"))
        sys.exit(1)
    import tempfile, io
    # NOTE: Figure out if we really want to use the env forwarding feature
    if not env:  # Unless we enable SendEnv in ssh these will do nothing
        env = {
            'TERM': 'xterm',
            'LANG': 'en_US.UTF-8',
        }
    try:
        # Get the default rows/cols right from the start
        env['LINES'] = os.environ['LINES']
        env['COLUMNS'] = os.environ['COLUMNS']
        # Also pass on some useful (but harmless) Gate One-specific vars:
        env['GO_TERM'] = os.environ['GO_TERM']
        env['GO_LOCATION'] = os.environ['GO_LOCATION']
        env['GO_SESSION'] = os.environ['GO_SESSION']
    except KeyError:
        pass  # These variables aren't set
    # Get the user's ssh directory
    if 'GO_USER' in os.environ:  # Try to use Gate One's provided user first
        go_user = os.environ['GO_USER']
    else:  # Fall back to the executing user (for testing outside of Gate One)
        go_user = os.environ['USER']
    if 'GO_USER_DIR' in os.environ:
        users_dir = os.path.join(os.environ['GO_USER_DIR'], go_user)
        if isinstance(users_dir, bytes):
            users_dir = users_dir.decode('utf-8')
        users_ssh_dir = os.path.join(users_dir, '.ssh')
    else:  # Fall back to using the default OpenSSH location for ssh stuff
        if POSIX:
            users_dir = os.environ['HOME']
            if isinstance(users_dir, bytes):
                users_dir = users_dir.decode('utf-8')
            users_ssh_dir = os.path.join(users_dir, '.ssh')
        else:
            # Assume Windows.  TODO: Double-check this is the right default path
            users_ssh_dir = os.path.join(os.environ['USERPROFILE'], '.ssh')
    if not os.path.exists(users_ssh_dir):
        mkdir_p(users_ssh_dir)
    if config:
        ssh_config_path = config
    else:
        ssh_config_path = os.path.join(users_ssh_dir, 'config')
    if not os.path.exists(ssh_config_path):
        # Create it (an empty one so ssh doesn't error out)
        with open(ssh_config_path, 'w') as f:
            f.write('\n')
    args = [
        "-x",  # No X11 forwarding, thanks :)
        "-F'%s'" % ssh_config_path,  # It's OK if it doesn't exist
        # This is so people won't have to worry about user management when
        # running one-Gate One-per-server...
        "-oNoHostAuthenticationForLocalhost=yes",
        # This ensure's that the executing user's identity won't be used:
        "-oIdentitiesOnly=yes",
        # This ensures the other end can tell we're a Gate One terminal and
        # possibly use the session ID with plugins (could be interesting).
        "-oSendEnv='GO_TERM GO_LOCATION GO_SESSION'",
        "-p",
        str(port),
        "-l",
        user,
    ]
    if debug:
        args.append('-vvv')
    # If we're given specific identities use them exclusively
    if identities:
        if isinstance(identities, (unicode, str)):
            # Only one identity present, turn it into a list
            if os.path.sep not in identities:
                # Turn the short identity name into an absolute path
                identities = os.path.join(users_ssh_dir, identities)
            identities = [identities]  # Make it a list
    else:
        # No identities given.  Get them from the user's dir (if any)
        identities = get_identities(users_ssh_dir, only_defaults=True)
    # Now make sure we use them in the connection...
    if identities:
        print(
            _("The following SSH identities are being used for this "
              "connection:"))
        for identity in identities:
            if os.path.sep not in identity:
                # Turn the short identity name into an absolute path
                identity = os.path.join(users_ssh_dir, identity)
            args.insert(3, "-i%s" % identity)
            print(_("\t\x1b[1m%s\x1b[0m" % os.path.split(identity)[1]))
        args.insert(
            3,  # Make sure we're using publickey auth first
            "-oPreferredAuthentications='publickey,keyboard-interactive,password'"
        )
    else:
        args.insert(
            3,  # Don't use publickey
            "-oPreferredAuthentications='keyboard-interactive,password'")
    if sshfp:
        args.insert(3, "-oVerifyHostKeyDNS=yes")
    if randomart:
        args.insert(3, "-oVisualHostKey=yes")
    if not command:
        if 'PATH' in env:
            command = which("ssh", path=env['PATH'])
        else:
            env['PATH'] = os.environ['PATH']
            command = which("ssh")
    if '[' in host:  # IPv6 address
        # Have to remove the brackets which is silly.  See bug:
        #   https://bugzilla.mindrot.org/show_bug.cgi?id=1602
        host = host.strip('[]')
    if socket:
        # Only set Master mode if we don't have a socket for this session.
        # This allows us to duplicate a session without having to code
        # anything special to pre-recognize this condition in gateone.py or
        # gateone.js.  It makes everything automagical :)
        socket_path = socket.replace(r'%r', user)  # Replace just like ssh does
        socket_path = socket_path.replace(r'%h', host)
        socket_path = socket_path.replace(r'%p', str(port))
        # The %SHORT_SOCKET% replacement is special: It replaces the equivalent
        # of ssh's %r@%h:%p with a shortened hash of the same value.  For
        # example: user@somehost:22 would become 'ud6U2Q'.  This is to avoid the
        # potential of a really long FQDN (%h) resulting in a "ControlPath too
        # long" error with the ssh command.
        user_at_host_port = "%s@%s:%s" % (user, host, port)
        hashed = short_hash(user_at_host_port)
        socket_path = socket_path.replace(r'%SHORT_SOCKET%', hashed)
        if not os.path.exists(socket_path):
            args.insert(0, "-M")
        else:
            print("\x1b]0;%s@%s (child)\007" % (user, host))
            print(
                _("\x1b]_;notice|Existing ssh session detected for ssh://%s@%s:%s;"
                  " utilizing existing tunnel.\007" % (user, host, port)))
        socket = socket.replace(r'%SHORT_SOCKET%', hashed)
        socket_arg = "-S'%s'" % socket
        # Also make sure the base directory exists
        basedir = os.path.split(socket)[0]
        mkdir_p(basedir)
        os.chmod(basedir, 0o700)  # 0700 for good security practices
        args.insert(1, socket_arg)  # After -M so it is easier to see in ps
    if additional_args:
        if isinstance(additional_args, (list, tuple)):
            args.extend(additional_args)
        else:
            args.extend(additional_args.split())
    args.insert(0, command)  # Command has to go first
    args.append(host)  # Host should be last
    if password:
        # Create a temporary script to use with SSH_ASKPASS
        temp = tempfile.NamedTemporaryFile(delete=False)
        os.chmod(temp.name, 0o700)
        temp.write(
            ('#!/bin/sh\necho "{0}"\n'.format(password)).encode('utf-8'))
        temp.close()
        env['SSH_ASKPASS'] = temp.name
        env['DISPLAY'] = ':9999'  # TODO: Get this using the user's actual X11
        # This removes the temporary file in a timely manner
        from subprocess import Popen
        Popen("sleep 15 && /bin/rm -f %s" % temp.name, shell=True)
        # 15 seconds should be enough even for slow connections/servers
        # It's a tradeoff:  Lower number, more secure.  Higher number, less
        # likely to fail
    script_path = None
    if 'GO_TERM' in os.environ.keys():
        term = os.environ['GO_TERM']
        location = os.environ['GO_LOCATION']
        if socket:
            # Emit our special optional escape sequence to tell ssh.py the path
            # to the SSH socket
            print("\x1b]_;ssh|set;ssh_socket;{0}\007".format(socket))
        if 'GO_SESSION_DIR' in os.environ.keys():
            # Save a file indicating our session is attached to GO_TERM
            ssh_session = 'ssh:%s:%s:%s@%s:%s' % (location, term, user, host,
                                                  port)
            script_path = os.path.join(os.environ['GO_SESSION_DIR'],
                                       os.environ['GO_SESSION'], ssh_session)
    if not script_path:
        # Just use a generic temp file
        temp = tempfile.NamedTemporaryFile(prefix="ssh_connect", delete=False)
        script_path = "%s" % temp.name
        temp.close()  # Will be written to below
    if password:
        # SSH_ASKPASS needs some special handling
        # Make sure setsid gets set in our shell script
        args.insert(0, 'exec setsid')
    # Create our little shell script to wrap the SSH command
    cmd = ""
    for arg in args:
        if isinstance(arg, bytes):
            cmd += arg.decode('utf-8') + ' '
        else:
            cmd += arg + ' '
    script = wrapper_script.format(socket=socket, cmd=cmd, temp=script_path)
    # This whole if/else block is here to enable users running Python 2.7 to
    # remove the 'from __future__ import unicode_literals' line to work around
    # this bug: http://bugs.python.org/issue9161
    if isinstance(script, bytes):
        with io.open(script_path, 'wb') as f:
            f.write(script)  # Save it to disk
    else:
        with io.open(script_path, 'w', encoding='utf-8') as f:
            f.write(script)  # Save it to disk
    # NOTE: We wrap in a shell script so we can execute it and immediately quit.
    # By doing this instead of keeping ssh_connect.py running we can save a lot
    # of memory (depending on how many terminals are open).
    os.chmod(script_path, 0o700)  # 0700 for good security practices
    # Execute then immediately quit so we don't use up any more memory than we
    # need.
    # setup default execvpe args
    args = ['-c', script_path, '&&', 'rm', '-f', script_path]
    # if we detect /bin/sh linked to busybox then make sure we insert the 'sh'
    # at the beginning of the args list
    if os.path.islink('/bin/sh'):
        args.insert(0, 'sh')
    os.execvpe('/bin/sh', args, env)
    os._exit(0)
Exemple #2
0
def openssh_connect(
    user,
    host,
    port=22,
    config=None,
    command=None,
    password=None,
    env=None,
    socket=None,
    sshfp=False,
    randomart=False,
    identities=None,
    additional_args=None,
):
    """
    Starts an interactive SSH session to the given host as the given user on the
    given port.
    If *command* isn't given, the equivalent of "which ssh" will be used to
    determine the full path to the ssh executable.  Otherwise *command* will be
    used.
    If a password is given, that will be passed to SSH when prompted.
    If *env* (dict) is given, that will be used for the shell env when opening
    the SSH connection.
    If *socket* (a file path) is given, this will be passed to the SSH command
    as -S<socket>.  If the socket does not exist, ssh's Master mode switch will
    be set (-M) automatically.  This allows sessions to be duplicated
    automatically.
    If *sshfp* resolves to True, SSHFP (DNS-based host verification) support
    will be enabled.
    If *randomart* resolves to True, the VisualHostKey (randomart hash) option
    will be enabled to display randomart when the connection is made.
    If *identities* given (may be a list or just a single string), it/those will
    be passed to the ssh command to use when connecting (e.g. -i/identity/path).
    If *additional_args* is given this value (or values if it is a list) will be
    added to the arguments passed to the ssh command.
    """
    try:
        int(port)
    except ValueError:
        print(_("The port must be an integer < 65535"))
        sys.exit(1)
    signal.signal(signal.SIGCHLD, signal.SIG_IGN)  # No zombies
    # NOTE: Figure out if we really want to use the env forwarding feature
    if not env:  # Unless we enable SendEnv in ssh these will do nothing
        env = {"TERM": "xterm", "LANG": "en_US.UTF-8"}
    # Get the default rows/cols right from the start
    try:
        env["LINES"] = os.environ["LINES"]
        env["COLUMNS"] = os.environ["COLUMNS"]
    except KeyError:
        pass  # These variables aren't set
    # Get the user's ssh directory
    if "GO_USER" in os.environ:  # Try to use Gate One's provided user first
        go_user = os.environ["GO_USER"]
    else:  # Fall back to the executing user (for testing outside of Gate One)
        go_user = os.environ["USER"]
    if "GO_USER_DIR" in os.environ:
        users_dir = os.path.join(os.environ["GO_USER_DIR"], go_user)
        users_ssh_dir = os.path.join(users_dir, ".ssh")
    else:  # Fall back to using the default OpenSSH location for ssh stuff
        if POSIX:
            users_ssh_dir = os.path.join(os.environ["HOME"], ".ssh")
        else:
            # Assume Windows.  TODO: Double-check this is the right default path
            users_ssh_dir = os.path.join(os.environ["USERPROFILE"], ".ssh")
    if not os.path.exists(users_ssh_dir):
        mkdir_p(users_ssh_dir)
    ssh_config_path = os.path.join(users_ssh_dir, "config")
    if not os.path.exists(ssh_config_path):
        # Create it (an empty one so ssh doesn't error out)
        with open(ssh_config_path, "w") as f:
            f.write("\n")
    args = [
        "-x",  # No X11 forwarding, thanks :)
        "-F'%s'" % ssh_config_path,  # It's OK if it doesn't exist
        # This is so people won't have to worry about user management when
        # running one-Gate One-per-server...
        "-oNoHostAuthenticationForLocalhost=yes",
        # This ensure's that the executing user's identity won't be used:
        "-oIdentityFile='/dev/null'",
        # This ensures the other end can tell we're a Gate One terminal
        "-oSendEnv=GO_TERM",
        "-p",
        str(port),
        "-l",
        user,
    ]
    # If we're given specific identities use them exclusively
    if identities:
        if isinstance(identities, (unicode, str)):
            # Only one identity present, turn it into a list
            if os.path.sep not in identities:
                # Turn the short identity name into an absolute path
                identities = os.path.join(users_ssh_dir, identities)
            identities = [identities]  # Make it a list
    else:
        # No identities given.  Get them from the user's dir (if any)
        identities = get_identities(users_ssh_dir, only_defaults=True)
    # Now make sure we use them in the connection...
    if identities:
        print(_("The following SSH identities are being used for this " "connection:"))
        for identity in identities:
            if os.path.sep not in identity:
                # Turn the short identity name into an absolute path
                identity = os.path.join(users_ssh_dir, identity)
            args.insert(3, "-i%s" % identity)
            print(_("\t\x1b[1m%s\x1b[0m" % os.path.split(identity)[1]))
        args.insert(
            3,  # Make sure we're using publickey auth first
            "-oPreferredAuthentications='publickey,keyboard-interactive,password'",
        )
    else:
        args.insert(3, "-oPreferredAuthentications='keyboard-interactive,password'")  # Don't use publickey
    if sshfp:
        args.insert(3, "-oVerifyHostKeyDNS=yes")
    if randomart:
        args.insert(3, "-oVisualHostKey=yes")
    if not command:
        if "PATH" in env:
            command = which("ssh", path=env["PATH"])
        else:
            env["PATH"] = os.environ["PATH"]
            command = which("ssh")
    if "[" in host:  # IPv6 address
        # Have to remove the brackets which is silly.  See bug:
        #   https://bugzilla.mindrot.org/show_bug.cgi?id=1602
        host = host.strip("[]")
    if socket:
        # Only set Master mode if we don't have a socket for this session.
        # This allows us to duplicate a session without having to code
        # anything special to pre-recognize this condition in gateone.py or
        # gateone.js.  It makes everything automagical :)
        socket_path = socket.replace(r"%r", user)  # Replace just like ssh does
        socket_path = socket_path.replace(r"%h", host)
        socket_path = socket_path.replace(r"%p", str(port))
        # The %SHORT_SOCKET% replacement is special: It replaces the equivalent
        # of ssh's %r@%h:%p with a shortened hash of the same value.  For
        # example: user@somehost:22 would become 'ud6U2Q'.  This is to avoid the
        # potential of a really long FQDN (%h) resulting in a "ControlPath too
        # long" error with the ssh command.
        user_at_host_port = "%s@%s:%s" % (user, host, port)
        hashed = short_hash(user_at_host_port)
        socket_path = socket_path.replace(r"%SHORT_SOCKET%", hashed)
        if not os.path.exists(socket_path):
            args.insert(0, "-M")
        else:
            print("\x1b]0;%s@%s (child)\007" % (user, host))
            print(
                _(
                    "\x1b]_;notice|Existing ssh session detected for ssh://%s@%s:%s;"
                    " utilizing existing tunnel.\007" % (user, host, port)
                )
            )
        socket = socket.replace(r"%SHORT_SOCKET%", hashed)
        socket_arg = "-S'%s'" % socket
        # Also make sure the base directory exists
        basedir = os.path.split(socket)[0]
        mkdir_p(basedir)
        os.chmod(basedir, 0o700)  # 0700 for good security practices
        args.insert(1, socket_arg)  # After -M so it is easier to see in ps
    if additional_args:
        if isinstance(additional_args, list):
            args.extend(additional_args)
        elif isinstance(additional_args, basestring):
            args.extend(additional_args.split())
    args.insert(0, command)  # Command has to go first
    args.append(host)  # Host should be last
    if password:
        # Create a temporary script to use with SSH_ASKPASS
        temp = tempfile.NamedTemporaryFile(delete=False)
        os.chmod(temp.name, 0o700)
        temp.write('#!/bin/sh\necho "%s"\n' % password)
        temp.close()
        env["SSH_ASKPASS"] = temp.name
        env["DISPLAY"] = ":9999"
        # This removes the temporary file in a timely manner
        Popen("sleep 15 && /bin/rm -f %s" % temp.name, shell=True)
        # 15 seconds should be enough even for slow connections/servers
        # It's a tradeoff:  Lower number, more secure.  Higher number, less
        # likely to fail
    script_path = None
    if "GO_TERM" in os.environ.keys():
        if "GO_SESSION_DIR" in os.environ.keys():
            # Save a file indicating our session is attached to GO_TERM
            term = os.environ["GO_TERM"]
            ssh_session = "ssh:%s:%s@%s:%s" % (term, user, host, port)
            script_path = os.path.join(os.environ["GO_SESSION_DIR"], ssh_session)
    if not script_path:
        # Just use a generic temp file
        temp = tempfile.NamedTemporaryFile(prefix="ssh_connect", delete=False)
        script_path = "%s" % temp.name
        temp.close()  # Will be written to below
    # Create our little shell script to wrap the SSH command
    script = wrapper_script.format(socket=socket, cmd=" ".join(args), temp=script_path)
    with open(script_path, "w") as f:
        f.write(script)  # Save it to disk
    # NOTE: We wrap in a shell script so we can execute it and immediately quit.
    # By doing this instead of keeping ssh_connect.py running we can save a lot
    # of memory (depending on how many terminals are open).
    os.chmod(script_path, 0o700)  # 0700 for good security practices
    if password:
        # SSH_ASKPASS needs some special handling
        os.setsid()  # This is the key
    # Execute then immediately quit so we don't use up any more memory than we
    # need.
    os.execvpe(script_path, [], env)
    os._exit(0)
Exemple #3
0
def openssh_connect(
        user,
        host,
        port=22,
        config=None,
        command=None,
        password=None,
        env=None,
        socket=None,
        sshfp=False,
        randomart=False,
        identities=None,
        additional_args=None,
        debug=False):
    """
    Starts an interactive SSH session to the given host as the given user on the
    given port.

    If *command* isn't given, the equivalent of "which ssh" will be used to
    determine the full path to the ssh executable.  Otherwise *command* will be
    used.

    If a password is given, that will be passed to SSH when prompted.

    If *env* (dict) is given, that will be used for the shell env when opening
    the SSH connection.

    If *socket* (a file path) is given, this will be passed to the SSH command
    as -S<socket>.  If the socket does not exist, ssh's Master mode switch will
    be set (-M) automatically.  This allows sessions to be duplicated
    automatically.

    If *sshfp* resolves to True, SSHFP (DNS-based host verification) support
    will be enabled.

    If *randomart* resolves to True, the VisualHostKey (randomart hash) option
    will be enabled to display randomart when the connection is made.

    If *identities* given (may be a list or just a single string), it/those will
    be passed to the ssh command to use when connecting (e.g. -i/identity/path).

    If *additional_args* is given this value (or values if it is a list) will be
    added to the arguments passed to the ssh command.

    If *debug* is ``True`` then '-vvv' will be passed to the ssh command.
    """
    try:
        int(port)
    except ValueError:
        print(_("The port must be an integer < 65535"))
        sys.exit(1)
    import tempfile, io
    # NOTE: Figure out if we really want to use the env forwarding feature
    if not env: # Unless we enable SendEnv in ssh these will do nothing
        env = {
            'TERM': 'xterm',
            'LANG': 'en_US.UTF-8',
        }
    try:
        # Get the default rows/cols right from the start
        env['LINES'] = os.environ['LINES']
        env['COLUMNS'] = os.environ['COLUMNS']
        # Also pass on some useful (but harmless) Gate One-specific vars:
        env['GO_TERM'] = os.environ['GO_TERM']
        env['GO_LOCATION'] = os.environ['GO_LOCATION']
        env['GO_SESSION'] = os.environ['GO_SESSION']
    except KeyError:
        pass # These variables aren't set
    # Get the user's ssh directory
    if 'GO_USER' in os.environ: # Try to use Gate One's provided user first
        go_user = os.environ['GO_USER']
    else: # Fall back to the executing user (for testing outside of Gate One)
        go_user = os.environ['USER']
    if 'GO_USER_DIR' in os.environ:
        users_dir = os.path.join(os.environ['GO_USER_DIR'], go_user)
        if isinstance(users_dir, bytes):
            users_dir = users_dir.decode('utf-8')
        users_ssh_dir = os.path.join(users_dir, '.ssh')
    else: # Fall back to using the default OpenSSH location for ssh stuff
        if POSIX:
            users_dir = os.environ['HOME']
            if isinstance(users_dir, bytes):
                users_dir = users_dir.decode('utf-8')
            users_ssh_dir = os.path.join(users_dir, '.ssh')
        else:
            # Assume Windows.  TODO: Double-check this is the right default path
            users_ssh_dir = os.path.join(os.environ['USERPROFILE'], '.ssh')
    if not os.path.exists(users_ssh_dir):
        mkdir_p(users_ssh_dir)
    ssh_config_path = os.path.join(users_ssh_dir, 'config')
    if not os.path.exists(ssh_config_path):
        # Create it (an empty one so ssh doesn't error out)
        with open(ssh_config_path, 'w') as f:
            f.write('\n')
    args = [
        "-x", # No X11 forwarding, thanks :)
        "-F'%s'" % ssh_config_path, # It's OK if it doesn't exist
        # This is so people won't have to worry about user management when
        # running one-Gate One-per-server...
        "-oNoHostAuthenticationForLocalhost=yes",
        # This ensure's that the executing user's identity won't be used:
        "-oIdentityFile='/dev/null'",
        # This ensures the other end can tell we're a Gate One terminal and
        # possibly use the session ID with plugins (could be interesting).
        "-oSendEnv='GO_TERM GO_LOCATION GO_SESSION'",
        "-p", str(port),
        "-l", user,
    ]
    if debug:
        args.append('-vvv')
    # If we're given specific identities use them exclusively
    if identities:
        if isinstance(identities, (unicode, str)):
            # Only one identity present, turn it into a list
            if os.path.sep not in identities:
                # Turn the short identity name into an absolute path
                identities = os.path.join(users_ssh_dir, identities)
            identities = [identities] # Make it a list
    else:
        # No identities given.  Get them from the user's dir (if any)
        identities = get_identities(users_ssh_dir, only_defaults=True)
    # Now make sure we use them in the connection...
    if identities:
        print(_(
            "The following SSH identities are being used for this "
            "connection:"))
        for identity in identities:
            if os.path.sep not in identity:
                # Turn the short identity name into an absolute path
                identity = os.path.join(users_ssh_dir, identity)
            args.insert(3, "-i%s" % identity)
            print(_("\t\x1b[1m%s\x1b[0m" % os.path.split(identity)[1]))
        args.insert(3, # Make sure we're using publickey auth first
        "-oPreferredAuthentications='publickey,keyboard-interactive,password'"
        )
    else:
        args.insert(
            3, # Don't use publickey
            "-oPreferredAuthentications='keyboard-interactive,password'"
        )
    if sshfp:
        args.insert(3, "-oVerifyHostKeyDNS=yes")
    if randomart:
        args.insert(3, "-oVisualHostKey=yes")
    if not command:
        if 'PATH' in env:
            command = which("ssh", path=env['PATH'])
        else:
            env['PATH'] = os.environ['PATH']
            command = which("ssh")
    if '[' in host: # IPv6 address
        # Have to remove the brackets which is silly.  See bug:
        #   https://bugzilla.mindrot.org/show_bug.cgi?id=1602
        host = host.strip('[]')
    if socket:
        # Only set Master mode if we don't have a socket for this session.
        # This allows us to duplicate a session without having to code
        # anything special to pre-recognize this condition in gateone.py or
        # gateone.js.  It makes everything automagical :)
        socket_path = socket.replace(r'%r', user) # Replace just like ssh does
        socket_path = socket_path.replace(r'%h', host)
        socket_path = socket_path.replace(r'%p', str(port))
        # The %SHORT_SOCKET% replacement is special: It replaces the equivalent
        # of ssh's %r@%h:%p with a shortened hash of the same value.  For
        # example: user@somehost:22 would become 'ud6U2Q'.  This is to avoid the
        # potential of a really long FQDN (%h) resulting in a "ControlPath too
        # long" error with the ssh command.
        user_at_host_port = "%s@%s:%s" % (user, host, port)
        hashed = short_hash(user_at_host_port)
        socket_path = socket_path.replace(r'%SHORT_SOCKET%', hashed)
        if not os.path.exists(socket_path):
            args.insert(0, "-M")
        else:
            print("\x1b]0;%s@%s (child)\007" % (user, host))
            print(_(
                "\x1b]_;notice|Existing ssh session detected for ssh://%s@%s:%s;"
                " utilizing existing tunnel.\007" % (user, host, port)
            ))
        socket = socket.replace(r'%SHORT_SOCKET%', hashed)
        socket_arg = "-S'%s'" % socket
        # Also make sure the base directory exists
        basedir = os.path.split(socket)[0]
        mkdir_p(basedir)
        os.chmod(basedir, 0o700) # 0700 for good security practices
        args.insert(1, socket_arg) # After -M so it is easier to see in ps
    if additional_args:
        if isinstance(additional_args, (list, tuple)):
            args.extend(additional_args)
        else:
            args.extend(additional_args.split())
    args.insert(0, command) # Command has to go first
    args.append(host) # Host should be last
    if password:
        # Create a temporary script to use with SSH_ASKPASS
        temp = tempfile.NamedTemporaryFile(delete=False)
        os.chmod(temp.name, 0o700)
        temp.write(('#!/bin/sh\necho "{0}"\n'.format(password)).encode('utf-8'))
        temp.close()
        env['SSH_ASKPASS'] = temp.name
        env['DISPLAY'] = ':9999' # TODO: Get this using the user's actual X11
        # This removes the temporary file in a timely manner
        from subprocess import Popen
        Popen("sleep 15 && /bin/rm -f %s" % temp.name, shell=True)
        # 15 seconds should be enough even for slow connections/servers
        # It's a tradeoff:  Lower number, more secure.  Higher number, less
        # likely to fail
    script_path = None
    if 'GO_TERM' in os.environ.keys():
        term = os.environ['GO_TERM']
        location = os.environ['GO_LOCATION']
        if socket:
            # Emit our special optional escape sequence to tell ssh.py the path
            # to the SSH socket
            print("\x1b]_;ssh|set;ssh_socket;{0}\007".format(socket))
        if 'GO_SESSION_DIR' in os.environ.keys():
            # Save a file indicating our session is attached to GO_TERM
            ssh_session = 'ssh:%s:%s:%s@%s:%s' % (
                location, term, user, host, port)
            script_path = os.path.join(
                os.environ['GO_SESSION_DIR'],
                os.environ['GO_SESSION'], ssh_session)
    if not script_path:
        # Just use a generic temp file
        temp = tempfile.NamedTemporaryFile(prefix="ssh_connect", delete=False)
        script_path = "%s" % temp.name
        temp.close() # Will be written to below
    if password:
        # SSH_ASKPASS needs some special handling
        # Make sure setsid gets set in our shell script
        args.insert(0, 'exec setsid')
    # Create our little shell script to wrap the SSH command
    cmd = ""
    for arg in args:
        if isinstance(arg, bytes):
            cmd += arg.decode('utf-8') + ' '
        else:
            cmd += arg + ' '
    script = wrapper_script.format(
        socket=socket,
        cmd=cmd,
        temp=script_path)
    # This whole if/else block is here to enable users running Python 2.7 to
    # remove the 'from __future__ import unicode_literals' line to work around
    # this bug: http://bugs.python.org/issue9161
    if isinstance(script, bytes):
        with io.open(script_path, 'wb') as f:
            f.write(script) # Save it to disk
    else:
        with io.open(script_path, 'w', encoding='utf-8') as f:
            f.write(script) # Save it to disk
    # NOTE: We wrap in a shell script so we can execute it and immediately quit.
    # By doing this instead of keeping ssh_connect.py running we can save a lot
    # of memory (depending on how many terminals are open).
    os.chmod(script_path, 0o700) # 0700 for good security practices
    # Execute then immediately quit so we don't use up any more memory than we
    # need.
    os.execvpe('/bin/sh', [
        '-c', script_path, '&&', 'rm', '-f', script_path], env)
    os._exit(0)
Exemple #4
0
 if not command:
     if 'PATH' in env:
         command = which("ssh", path=env['PATH'])
     else:
         env['PATH'] = os.environ['PATH']
         command = which("ssh")
 if '[' in host: # IPv6 address
     # Have to remove the brackets which is silly.  See bug:
     #   https://bugzilla.mindrot.org/show_bug.cgi?id=1602
     host = host.strip('[]')
 if socket:
     # Only set Master mode if we don't have a socket for this session.
     # This allows us to duplicate a session without having to code
     # anything special to pre-recognize this condition in gateone.py or
     # gateone.js.  It makes everything automagical :)
     socket_path = socket.replace(r'%r', user) # Replace just like ssh does
     socket_path = socket_path.replace(r'%h', host)
     socket_path = socket_path.replace(r'%p', str(port))
     # The %SHORT_SOCKET% replacement is special: It replaces the equivalent
     # of ssh's %r@%h:%p with a shortened hash of the same value.  For
     # example: user@somehost:22 would become 'ud6U2Q'.  This is to avoid the
     # potential of a really long FQDN (%h) resulting in a "ControlPath too
     # long" error with the ssh command.
     user_at_host_port = "%s@%s:%s" % (user, host, port)
     hashed = short_hash(user_at_host_port)
     socket_path = socket_path.replace(r'%SHORT_SOCKET%', hashed)
     if not os.path.exists(socket_path):
         args.insert(0, "-M")
     else:
         print("\x1b]0;%s@%s (child)\007" % (user, host))
         print(_(