From dc446f904282702a9e1e842e8f794cce489fff61 Mon Sep 17 00:00:00 2001 From: David Wilson Date: Sun, 17 Sep 2017 18:09:26 +0530 Subject: [PATCH] ssh: Learn to type passwords and supply pubkeys. Now ssh requires a tty allocation. This presents a scalability problem, a future version could selectively allocate a tty only if typing passwords is desired. Sudo's tty handling is now moved into mitogen.master. --- mitogen/master.py | 78 +++++++++++++++++++++++++++++++++++++++++++++-- mitogen/ssh.py | 52 +++++++++++++++++++++++++++++-- mitogen/sudo.py | 76 +-------------------------------------------- 3 files changed, 126 insertions(+), 80 deletions(-) diff --git a/mitogen/master.py b/mitogen/master.py index f9e3341d..eb419fba 100644 --- a/mitogen/master.py +++ b/mitogen/master.py @@ -12,11 +12,13 @@ import itertools import logging import os import pkgutil +import pty import re import select import signal import socket import sys +import termios import textwrap import time import types @@ -73,6 +75,78 @@ def create_child(*args): return pid, os.dup(parentfp.fileno()) +def flags(names): + """Return the result of ORing a set of (space separated) :py:mod:`termios` + module constants together.""" + return sum(getattr(termios, name) for name in names.split()) + + +def cfmakeraw((iflag, oflag, cflag, lflag, ispeed, ospeed, cc)): + """Given a list returned by :py:func:`termios.tcgetattr`, return a list + that has been modified in the same manner as the `cfmakeraw()` C library + function.""" + iflag &= ~flags('IGNBRK BRKINT PARMRK ISTRIP INLCR IGNCR ICRNL IXON') + oflag &= ~flags('OPOST IXOFF') + lflag &= ~flags('ECHO ECHOE ECHONL ICANON ISIG IEXTEN') + cflag &= ~flags('CSIZE PARENB') + cflag |= flags('CS8') + + iflag = 0 + oflag = 0 + lflag = 0 + return [iflag, oflag, cflag, lflag, ispeed, ospeed, cc] + + +def disable_echo(fd): + old = termios.tcgetattr(fd) + new = cfmakeraw(old) + flags = ( + termios.TCSAFLUSH | + getattr(termios, 'TCSASOFT', 0) + ) + termios.tcsetattr(fd, flags, new) + + +def close_nonstandard_fds(): + for fd in xrange(3, 1024): + try: + os.close(fd) + except OSError: + pass + + +def tty_create_child(*args): + """ + Return a file descriptor connected to the master end of a pseudo-terminal, + whose slave end is connected to stdin/stdout/stderr of a new child process. + The child is created such that the pseudo-terminal becomes its controlling + TTY, ensuring access to /dev/tty returns a new file descriptor open on the + slave end. + + :param args: + execl() arguments. + """ + master_fd, slave_fd = os.openpty() + disable_echo(master_fd) + disable_echo(slave_fd) + + pid = os.fork() + if not pid: + os.dup2(slave_fd, 0) + os.dup2(slave_fd, 1) + os.dup2(slave_fd, 2) + close_nonstandard_fds() + os.setsid() + os.close(os.open(os.ttyname(1), os.O_RDWR)) + os.execvp(args[0], args) + raise SystemExit + + os.close(slave_fd) + LOG.debug('tty_create_child() child %d fd %d, parent %d, args %r', + pid, master_fd, os.getpid(), args) + return pid, master_fd + + def write_all(fd, s): written = 0 while written < len(s): @@ -105,8 +179,8 @@ def iter_read(fd, deadline): if not s: raise mitogen.core.StreamError( - 'EOF on stream; last 100 bytes received: %r' % - (''.join(bits)[-100:],) + 'EOF on stream; last 300 bytes received: %r' % + (''.join(bits)[-300:],) ) bits.append(s) diff --git a/mitogen/ssh.py b/mitogen/ssh.py index 6bb4a73a..c5668ba9 100644 --- a/mitogen/ssh.py +++ b/mitogen/ssh.py @@ -3,36 +3,58 @@ Functionality to allow establishing new slave contexts over an SSH connection. """ import commands +import logging +import time import mitogen.master +LOG = logging.getLogger('mitogen') + +PASSWORD_PROMPT = 'password' +PERMDENIED_PROMPT = 'permission denied' + + +class PasswordError(mitogen.core.Error): + pass + + class Stream(mitogen.master.Stream): - python_path = 'python' + create_child = staticmethod(mitogen.master.tty_create_child) + python_path = 'python2.7' #: The path to the SSH binary. ssh_path = 'ssh' + identity_file = None + password = None port = None def construct(self, hostname, username=None, ssh_path=None, port=None, - check_host_keys=True, **kwargs): + check_host_keys=True, password=None, identity_file=None, + **kwargs): super(Stream, self).construct(**kwargs) self.hostname = hostname self.username = username self.port = port self.check_host_keys = check_host_keys + self.password = password + self.identity_file = identity_file if ssh_path: self.ssh_path = ssh_path def get_boot_command(self): bits = [self.ssh_path] - bits += ['-o', 'BatchMode yes'] + #bits += ['-o', 'BatchMode yes'] if self.username: bits += ['-l', self.username] if self.port is not None: bits += ['-p', str(self.port)] + if self.identity_file or self.password: + bits += ['-o', 'IdentitiesOnly yes'] + if self.identity_file: + bits += ['-i', self.identity_file] if not self.check_host_keys: bits += [ '-o', 'StrictHostKeyChecking no', @@ -47,3 +69,27 @@ class Stream(mitogen.master.Stream): self.name = 'ssh.' + self.hostname if self.port: self.name += ':%s' % (self.port,) + + password_incorrect_msg = 'SSH password is incorrect' + password_required_msg = 'SSH password was requested, but none specified' + + def _connect_bootstrap(self): + password_sent = False + for buf in mitogen.master.iter_read(self.receive_side.fd, + time.time() + 10.0): + LOG.debug('%r: received %r', self, buf) + if buf.endswith('EC0\n'): + return self._ec0_received() + elif PERMDENIED_PROMPT in buf.lower(): + if self.password is not None and password_sent: + raise PasswordError(self.password_incorrect_msg) + else: + raise PasswordError(self.auth_incorrect_msg) + elif PASSWORD_PROMPT in buf.lower(): + if self.password is None: + raise PasswordError(self.password_required_msg) + LOG.debug('sending password') + self.transmit_side.write(self.password + '\n') + password_sent = True + else: + raise mitogen.core.StreamError('bootstrap failed') diff --git a/mitogen/sudo.py b/mitogen/sudo.py index 51c59186..58a30111 100644 --- a/mitogen/sudo.py +++ b/mitogen/sudo.py @@ -1,8 +1,6 @@ import logging import os -import pty -import termios import time import mitogen.core @@ -17,80 +15,8 @@ class PasswordError(mitogen.core.Error): pass -def flags(names): - """Return the result of ORing a set of (space separated) :py:mod:`termios` - module constants together.""" - return sum(getattr(termios, name) for name in names.split()) - - -def cfmakeraw((iflag, oflag, cflag, lflag, ispeed, ospeed, cc)): - """Given a list returned by :py:func:`termios.tcgetattr`, return a list - that has been modified in the same manner as the `cfmakeraw()` C library - function.""" - iflag &= ~flags('IGNBRK BRKINT PARMRK ISTRIP INLCR IGNCR ICRNL IXON') - oflag &= ~flags('OPOST IXOFF') - lflag &= ~flags('ECHO ECHOE ECHONL ICANON ISIG IEXTEN') - cflag &= ~flags('CSIZE PARENB') - cflag |= flags('CS8') - - iflag = 0 - oflag = 0 - lflag = 0 - return [iflag, oflag, cflag, lflag, ispeed, ospeed, cc] - - -def disable_echo(fd): - old = termios.tcgetattr(fd) - new = cfmakeraw(old) - flags = ( - termios.TCSAFLUSH | - getattr(termios, 'TCSASOFT', 0) - ) - termios.tcsetattr(fd, flags, new) - - -def close_nonstandard_fds(): - for fd in xrange(3, 1024): - try: - os.close(fd) - except OSError: - pass - - -def tty_create_child(*args): - """ - Return a file descriptor connected to the master end of a pseudo-terminal, - whose slave end is connected to stdin/stdout/stderr of a new child process. - The child is created such that the pseudo-terminal becomes its controlling - TTY, ensuring access to /dev/tty returns a new file descriptor open on the - slave end. - - :param args: - execl() arguments. - """ - master_fd, slave_fd = os.openpty() - disable_echo(master_fd) - disable_echo(slave_fd) - - pid = os.fork() - if not pid: - os.dup2(slave_fd, 0) - os.dup2(slave_fd, 1) - os.dup2(slave_fd, 2) - close_nonstandard_fds() - os.setsid() - os.close(os.open(os.ttyname(1), os.O_RDWR)) - os.execvp(args[0], args) - raise SystemExit - - os.close(slave_fd) - LOG.debug('tty_create_child() child %d fd %d, parent %d, args %r', - pid, master_fd, os.getpid(), args) - return pid, master_fd - - class Stream(mitogen.master.Stream): - create_child = staticmethod(tty_create_child) + create_child = staticmethod(mitogen.master.tty_create_child) sudo_path = 'sudo' password = None