From 3322eaef455863998598e0b9d4ecf41e36fd7a08 Mon Sep 17 00:00:00 2001 From: David Wilson Date: Wed, 2 May 2018 03:30:31 +0100 Subject: [PATCH] Basic "su" method. --- docs/api.rst | 30 ++++++++++++ mitogen/core.py | 1 + mitogen/parent.py | 7 ++- mitogen/su.py | 113 ++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 149 insertions(+), 2 deletions(-) create mode 100644 mitogen/su.py diff --git a/docs/api.rst b/docs/api.rst index 923c368d..7d928b42 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -780,6 +780,36 @@ Router Class will be searched if given as a filename. Defaults to ``machinectl``. + .. method:: su (username=None, password=None, su_path=None, password_prompt=None, incorrect_prompts=None, \**kwargs) + + Construct a context on the local machine over a ``su`` invocation. The + ``su`` process is started in a newly allocated pseudo-terminal, and + supports typing interactive passwords. + + Accepts all parameters accepted by :py:meth:`local`, in addition to: + + :param str username: + Username to pass to ``su``, defaults to ``root``. + :param str password: + The account password to use if requested. + :param str su_path: + Filename or complete path to the ``su`` binary. ``PATH`` will be + searched if given as a filename. Defaults to ``su``. + :param str password_prompt: + The string to wait to that signals ``su`` is requesting a password. + Defaults to ``Password:``. + :param str password_prompt: + The string that signal a request for the password. Defaults to + ``Password:``. + :param str incorrect_prompts: + Strings that signal the password is incorrect. Defaults to `("su: + sorry", "su: authentication failure")`. + + :raises mitogen.su.PasswordError: + A password was requested but none was provided, the supplied + password was incorrect, or (on BSD) the target account did not + exist. + .. method:: sudo (username=None, sudo_path=None, password=None, \**kwargs) Construct a context on the local machine over a ``sudo`` invocation. diff --git a/mitogen/core.py b/mitogen/core.py index d5044307..bd4493e9 100644 --- a/mitogen/core.py +++ b/mitogen/core.py @@ -498,6 +498,7 @@ class Importer(object): 'service', 'setns', 'ssh', + 'su', 'sudo', 'utils', ]} diff --git a/mitogen/parent.py b/mitogen/parent.py index 2203ef9c..76ca50bd 100644 --- a/mitogen/parent.py +++ b/mitogen/parent.py @@ -1031,12 +1031,15 @@ class Router(mitogen.core.Router): def setns(self, **kwargs): return self.connect('setns', **kwargs) - def ssh(self, **kwargs): - return self.connect('ssh', **kwargs) + def su(self, **kwargs): + return self.connect('su', **kwargs) def sudo(self, **kwargs): return self.connect('sudo', **kwargs) + def ssh(self, **kwargs): + return self.connect('ssh', **kwargs) + class ProcessMonitor(object): def __init__(self): diff --git a/mitogen/su.py b/mitogen/su.py new file mode 100644 index 00000000..bfaada11 --- /dev/null +++ b/mitogen/su.py @@ -0,0 +1,113 @@ +# Copyright 2017, David Wilson +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# +# 2. Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# +# 3. Neither the name of the copyright holder nor the names of its contributors +# may be used to endorse or promote products derived from this software without +# specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. + +import logging +import os + +import mitogen.core +import mitogen.parent + + +LOG = logging.getLogger(__name__) + + +class PasswordError(mitogen.core.StreamError): + pass + + +class Stream(mitogen.parent.Stream): + # TODO: BSD su cannot handle stdin being a socketpair, but it does let the + # child inherit fds from the parent. So we can still pass a socketpair in + # for hybrid_tty_create_child(), there just needs to be either a shell + # snippet or bootstrap support for fixing things up afterwards. + create_child = staticmethod(mitogen.parent.tty_create_child) + + #: Once connected, points to the corresponding TtyLogStream, allowing it to + #: be disconnected at the same time this stream is being torn down. + + username = 'root' + password = None + su_path = 'su' + password_prompt = 'password:' + incorrect_prompts = ( + 'su: sorry', # BSD + 'su: authentication failure', # Linux + ) + + def construct(self, username=None, password=None, su_path=None, + password_prompt=None, incorrect_prompts=None, **kwargs): + super(Stream, self).construct(**kwargs) + if username is not None: + self.username = username + if password is not None: + self.password = password + if su_path is not None: + self.su_path = su_path + if password_prompt is not None: + self.password_prompt = password_prompt.lower() + if incorrect_prompts is not None: + self.incorrect_prompts = map(str.lower, incorrect_prompts) + + def connect(self): + super(Stream, self).connect() + self.name = 'su.' + self.username + + def on_disconnect(self, broker): + super(Stream, self).on_disconnect(broker) + + def get_boot_command(self): + argv = mitogen.parent.Argv(super(Stream, self).get_boot_command()) + return [self.su_path, self.username, '-c', str(argv)] + + password_incorrect_msg = 'su password is incorrect' + password_required_msg = 'su password is required' + + def _connect_bootstrap(self, extra_fd): + password_sent = False + it = mitogen.parent.iter_read( + fds=[self.receive_side.fd], + deadline=self.connect_deadline, + ) + + for buf in it: + LOG.debug('%r: received %r', self, buf) + if buf.endswith('EC0\n'): + self._ec0_received() + return + if any(s in buf.lower() for s in self.incorrect_prompts): + if password_sent: + raise PasswordError(self.password_incorrect_msg) + elif self.password_prompt in buf.lower(): + if self.password is None: + raise PasswordError(self.password_required_msg) + if password_sent: + raise PasswordError(self.password_incorrect_msg) + LOG.debug('sending password') + self.transmit_side.write(self.password + '\n') + password_sent = True + raise mitogen.core.StreamError('bootstrap failed')