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.
pull/35/head
David Wilson 7 years ago
parent db793003e0
commit a882497b62

@ -12,11 +12,13 @@ import itertools
import logging import logging
import os import os
import pkgutil import pkgutil
import pty
import re import re
import select import select
import signal import signal
import socket import socket
import sys import sys
import termios
import textwrap import textwrap
import time import time
import types import types
@ -73,6 +75,78 @@ def create_child(*args):
return pid, os.dup(parentfp.fileno()) 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): def write_all(fd, s):
written = 0 written = 0
while written < len(s): while written < len(s):
@ -105,8 +179,8 @@ def iter_read(fd, deadline):
if not s: if not s:
raise mitogen.core.StreamError( raise mitogen.core.StreamError(
'EOF on stream; last 100 bytes received: %r' % 'EOF on stream; last 300 bytes received: %r' %
(''.join(bits)[-100:],) (''.join(bits)[-300:],)
) )
bits.append(s) bits.append(s)

@ -3,36 +3,58 @@ Functionality to allow establishing new slave contexts over an SSH connection.
""" """
import commands import commands
import logging
import time
import mitogen.master 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): 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. #: The path to the SSH binary.
ssh_path = 'ssh' ssh_path = 'ssh'
identity_file = None
password = None
port = None port = None
def construct(self, hostname, username=None, ssh_path=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) super(Stream, self).construct(**kwargs)
self.hostname = hostname self.hostname = hostname
self.username = username self.username = username
self.port = port self.port = port
self.check_host_keys = check_host_keys self.check_host_keys = check_host_keys
self.password = password
self.identity_file = identity_file
if ssh_path: if ssh_path:
self.ssh_path = ssh_path self.ssh_path = ssh_path
def get_boot_command(self): def get_boot_command(self):
bits = [self.ssh_path] bits = [self.ssh_path]
bits += ['-o', 'BatchMode yes'] #bits += ['-o', 'BatchMode yes']
if self.username: if self.username:
bits += ['-l', self.username] bits += ['-l', self.username]
if self.port is not None: if self.port is not None:
bits += ['-p', str(self.port)] 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: if not self.check_host_keys:
bits += [ bits += [
'-o', 'StrictHostKeyChecking no', '-o', 'StrictHostKeyChecking no',
@ -47,3 +69,27 @@ class Stream(mitogen.master.Stream):
self.name = 'ssh.' + self.hostname self.name = 'ssh.' + self.hostname
if self.port: if self.port:
self.name += ':%s' % (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')

@ -1,8 +1,6 @@
import logging import logging
import os import os
import pty
import termios
import time import time
import mitogen.core import mitogen.core
@ -17,80 +15,8 @@ class PasswordError(mitogen.core.Error):
pass 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): class Stream(mitogen.master.Stream):
create_child = staticmethod(tty_create_child) create_child = staticmethod(mitogen.master.tty_create_child)
sudo_path = 'sudo' sudo_path = 'sudo'
password = None password = None

Loading…
Cancel
Save