2

I am trying to write a python script that will automatically log in to a remote host via ssh and update a users password. Since ssh demands that it take its input from a terminal, I am using os.forkpty(), running ssh in the child process and using the parent process to send command input through the pseudo terminal. Here is what I have so far:

import os, sys, time, getpass, select, termios

# Time in seconds between commands sent to tty
SLEEP_TIME = 1
NETWORK_TIMEOUT = 15

#------------------------------Get Passwords------------------------------------

# get username
login = getpass.getuser()

# get current password
current_pass = getpass.getpass("Enter current password: ")

# get new password, retry if same as current password
new_pass = current_pass
first_try = True

while new_pass == current_pass:
    if first_try:
        first_try = False
    else:
        # New password equal to old password
        print("New password must differ from current password.")

    # Get new password
    new_pass = getpass.getpass("Enter new password: ")
    new_pass_confirm = getpass.getpass("Confirm new password: ")
    while new_pass != new_pass_confirm:
        # Passwords do not match
        print("Passwords do not match")
        new_pass = getpass.getpass("Enter new password: ")
        new_pass_confirm = getpass.getpass("Confirm new password: ")

#------------------------------End Get Passwords--------------------------------

ssh = "/usr/bin/ssh" # ssh bin location
args = ["ssh", login + "@localhost", "-o StrictHostKeyChecking=no"]

#fork
pid, master = os.forkpty()
if pid == 0:
    # Turn off echo so master does not need to read back its own input
    attrs = termios.tcgetattr(sys.stdin.fileno())
    attrs[3] = attrs[3] & ~termios.ECHO
    termios.tcsetattr(sys.stdin.fileno(), termios.TCSADRAIN, attrs)
    os.execv(ssh, args)
else:
    select.select([master], [], [])
    os.write(master, current_pass + "\n")
    select.select([master], [], [])
    os.write(master, "passwd\n")
    select.select([master], [], [])
    os.write(master, current_pass + "\n")
    select.select([master], [], [])
    os.write(master, new_pass + "\n")
    select.select([master], [], [])
    os.write(master, new_pass + "\n")
    select.select([master], [], [])
    os.write(master, "id\n")
    select.select([master], [], [])
    sys.stdout.write(os.read(master, 2048))
    os.wait()

The script prompts the user for his/her current and new passwords, then forks and sends appropriate responses to ssh login prompt and then passwd prompts.

The problem I am having is that the select syscalls are not behaving as I would expect. They don't appear to be blocking at all. I'm thinking that I am misunderstanding something about the way select works with the master end of a pty.

If I replace them all with time.sleep(1), the script works fine, but I don't want to have to rely on that solution because I can't always guarantee the network will respond in a short time, and I don't want to make it something rediculous that will take forever (I intend to use this to programatically log into several servers to update passwords)

Is there a way to reliably poll the master side of a pty to wait for the slave's output?

Note: I realize there are better solutions to this problem with things like sshpass and chpasswd, but I am in an environment where this cannot be run as root and very few utilities are available. Thankfully python is.

Deddryk
  • 123
  • 4

2 Answers2

1

select doesn't read any data; it simply blocks until data is available to be read.

Since you don't read any data after the first select, there will still be data left in the buffer for you to read, so any subsequent select will not block.

You need to read the data in the buffer before calling select again. Doing this without blocking means that you will likely have to set the file to non-blocking mode (I don't know how to do that in Python).


A better way of providing the password over SSH would be to use the --stdin flag if your passwd supports it, and to run the command directly over SSH instead of through the created shell.

handle = subprocess.Popen(["ssh", login + "@localhost", "-o StrictHostKeyChecking=no", "passwd --stdin"])
handle.communicate("\n".join([oldpass, newpass, newpass, ""]))
Community
  • 1
  • 1
Colonel Thirty Two
  • 23,953
  • 8
  • 45
  • 85
  • This is part of my issue. Reading the current data out of the file solves some of the problem. Now the issue is figuring out when ssh is DONE writing. Eg. after the first select, I'll get half of the login message read. Unfortunately my passwd does not support the --stdin flag. That method also does not address the need to enter the initial password to ssh. – Deddryk Nov 14 '15 at 19:32
0

Have a look in man ssh at the -f option. It may be what you need when launching ssh. It will block ssh until the password is typed and then fork by itself. You could probably use this feature to achieve what you want (but you may have to slightly change your current code because it will perform by itself what you currently try to embed in your script).

This option is generally used at the command line for starting a remote graphical program: once the password is typed, you can safely close the terminal and keep interacting with the remote process with its graphical interface. But I think using this feature here would lead to a much cleaner way than playing with low-level blocking features and similar things.

Thomas Baruchel
  • 7,236
  • 2
  • 27
  • 46