First draft of econtext/sudo.py.

pull/35/head
David Wilson 9 years ago
parent 9733668b50
commit 99a2ccf68c

@ -53,6 +53,7 @@ contribution to a solution, I hope you find it useful.
Future Future
###### ######
* Move connect() logic to dispatcher thread.
* Connect back using TCP and SSL. * Connect back using TCP and SSL.
* Python 3 support. * Python 3 support.
* Windows support via psexec or similar. * Windows support via psexec or similar.

@ -11,9 +11,11 @@ import logging
import os import os
import pkgutil import pkgutil
import re import re
import select
import socket import socket
import sys import sys
import textwrap import textwrap
import time
import types import types
import zlib import zlib
@ -68,16 +70,29 @@ def create_child(*args):
return pid, os.dup(parentfp.fileno()) return pid, os.dup(parentfp.fileno())
def discard_until(fd, s): def read_with_deadline(fd, size, deadline):
timeout = deadline - time.time()
if timeout > 0:
rfds, _, _ = select.select([fd], [], [], timeout)
if rfds:
return os.read(fd, size)
raise econtext.core.TimeoutError('read timed out')
def iter_read(fd, deadline):
buf = '' buf = ''
while not buf.endswith(s): while True:
print 'hi'
s = os.read(fd, 4096) s = os.read(fd, 4096)
print ['got', s]
if not s: if not s:
raise econtext.core.StreamError('Expected %r, received %r', s, buf) raise econtext.core.StreamError('EOF on stream; received %r', buf)
buf += s buf += s
yield buf
def discard_until(fd, s, deadline):
for buf in iter_read(fd, deadline):
if buf.endswith(s):
return
class LogForwarder(object): class LogForwarder(object):
@ -282,14 +297,28 @@ class Stream(econtext.core.Stream):
LOG.debug('%r.connect(): child process stdin/stdout=%r', LOG.debug('%r.connect(): child process stdin/stdout=%r',
self, self.receive_side.fd) self, self.receive_side.fd)
discard_until(self.receive_side.fd, 'EC0\n') self._connect_bootstrap()
def _ec0_received(self):
LOG.debug('%r._ec0_received()', self)
econtext.core.write_all(self.transmit_side.fd, self.get_preamble()) econtext.core.write_all(self.transmit_side.fd, self.get_preamble())
discard_until(self.receive_side.fd, 'EC1\n') discard_until(self.receive_side.fd, 'EC1\n', time.time() + 10.0)
def _connect_bootstrap(self):
discard_until(self.receive_side.fd, 'EC0\n', time.time() + 10.0)
return self._ec0_received()
class Broker(econtext.core.Broker): class Broker(econtext.core.Broker):
shutdown_timeout = 5.0 shutdown_timeout = 5.0
def __enter__(self):
return self
def __exit__(self, e_type, e_val, tb):
self.shutdown()
self.join()
class Context(econtext.core.Context): class Context(econtext.core.Context):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):

@ -0,0 +1,123 @@
import logging
import os
import pty
import termios
import time
import econtext.master
LOG = logging.getLogger(__name__)
PASSWORD_PROMPT = 'password'
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)
#new = old[:]
#new[3] &= ~flags('ECHO')
tcsetattr_flags = termios.TCSAFLUSH
if hasattr(termios, 'TCSASOFT'):
tcsetattr_flags |= termios.TCSASOFT
termios.tcsetattr(fd, tcsetattr_flags, new)
def tty_create_child(*args):
master_fd, slave_fd = os.openpty()
import econtext.core
#econtext.core.set_nonblocking(master_fd)
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)
os.close(slave_fd)
os.close(master_fd)
#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 StateMachine(object):
S_PW_PROMPT, S_SHELL, S_CONNECTED = range(3)
state = S_PW_PROMPT
class Stream(econtext.master.Stream):
create_child = staticmethod(tty_create_child)
sudo_path = 'sudo'
password = None
def get_boot_command(self):
bits = [self.sudo_path, '-S', '-u', self._context.username]
return bits + super(Stream, self).get_boot_command()
def _connect_bootstrap(self):
password_sent = False
it = econtext.master.iter_read(self.receive_side.fd,
time.time() + 10.0)
for buf in it:
if buf.endswith('EC0\n'):
return self._ec0_received()
elif (not password_sent) and 'password' in buf.lower():
if self.password is None:
raise econtext.core.StreamError('password required')
LOG.debug('sending password')
os.write(self.transmit_side.fd, self.password + '\n')
password_sent = True
else:
raise econtext.core.StreamError('bootstrap failed')
def connect(broker, username=None, sudo_path=None, python_path=None, password=None):
"""Get the named sudo context, creating it if it does not exist."""
if username is None:
username = 'root'
context = econtext.master.Context(
broker=broker,
name='sudo:' + username,
username=username)
context.stream = Stream(context)
if sudo_path:
context.stream.sudo_path = sudo_path
if password:
context.stream.password = password
if python_path:
context.stream.python_path = python_path
context.stream.connect()
return broker.register(context)
Loading…
Cancel
Save