diff --git a/ansible_mitogen/connection.py b/ansible_mitogen/connection.py index c8b1870c..2b5c4356 100644 --- a/ansible_mitogen/connection.py +++ b/ansible_mitogen/connection.py @@ -173,6 +173,20 @@ def _connect_sudo(spec): } +def _connect_doas(spec): + return { + 'method': 'doas', + 'enable_lru': True, + 'kwargs': { + 'username': spec['become_user'], + 'password': wrap_or_none(mitogen.core.Secret, spec['become_pass']), + 'python_path': spec['python_path'], + 'doas_path': spec['become_exe'], + 'connect_timeout': spec['timeout'], + } + } + + def _connect_mitogen_su(spec): # su as a first-class proxied connection, not a become method. return { @@ -202,6 +216,20 @@ def _connect_mitogen_sudo(spec): } +def _connect_mitogen_doas(spec): + # doas as a first-class proxied connection, not a become method. + return { + 'method': 'doas', + 'kwargs': { + 'username': spec['remote_user'], + 'password': wrap_or_none(mitogen.core.Secret, spec['password']), + 'python_path': spec['python_path'], + 'doas_path': spec['become_exe'], + 'connect_timeout': spec['timeout'], + } + } + + CONNECTION_METHOD = { 'docker': _connect_docker, 'jail': _connect_jail, @@ -213,8 +241,10 @@ CONNECTION_METHOD = { 'ssh': _connect_ssh, 'su': _connect_su, 'sudo': _connect_sudo, + 'doas': _connect_doas, 'mitogen_su': _connect_mitogen_su, 'mitogen_sudo': _connect_mitogen_sudo, + 'mitogen_doas': _connect_mitogen_doas, } @@ -314,8 +344,8 @@ class Connection(ansible.plugins.connection.ConnectionBase): #: target user account. fork_context = None - #: Only sudo and su are supported for now. - become_methods = ['sudo', 'su'] + #: Only sudo, su, and doas are supported for now. + become_methods = ['sudo', 'su', 'doas'] #: Set to 'ansible_python_interpreter' by on_action_run(). python_path = None diff --git a/ansible_mitogen/plugins/connection/mitogen_doas.py b/ansible_mitogen/plugins/connection/mitogen_doas.py new file mode 100644 index 00000000..7d60b482 --- /dev/null +++ b/ansible_mitogen/plugins/connection/mitogen_doas.py @@ -0,0 +1,43 @@ +# 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 os.path +import sys + +try: + import ansible_mitogen.connection +except ImportError: + base_dir = os.path.dirname(__file__) + sys.path.insert(0, os.path.abspath(os.path.join(base_dir, '../../..'))) + del base_dir + +import ansible_mitogen.connection + + +class Connection(ansible_mitogen.connection.Connection): + transport = 'mitogen_doas' diff --git a/mitogen/core.py b/mitogen/core.py index 53f32bb0..6f536ec8 100644 --- a/mitogen/core.py +++ b/mitogen/core.py @@ -607,6 +607,7 @@ class Importer(object): self._present = {'mitogen': [ 'compat', 'debug', + 'doas', 'docker', 'fakessh', 'fork', diff --git a/mitogen/doas.py b/mitogen/doas.py new file mode 100644 index 00000000..1d9d04eb --- /dev/null +++ b/mitogen/doas.py @@ -0,0 +1,118 @@ +# 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 +from mitogen.core import b + + +LOG = logging.getLogger(__name__) + + +class PasswordError(mitogen.core.StreamError): + pass + + +class Stream(mitogen.parent.Stream): + create_child = staticmethod(mitogen.parent.hybrid_tty_create_child) + child_is_immediate_subprocess = False + + #: Once connected, points to the corresponding TtyLogStream, allowing it to + #: be disconnected at the same time this stream is being torn down. + tty_stream = None + + username = 'root' + password = None + doas_path = 'doas' + password_prompt = b('Password:') + incorrect_prompts = ( + b('doas: authentication failed'), + ) + + def construct(self, username=None, password=None, doas_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 doas_path is not None: + self.doas_path = doas_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 = u'doas.' + mitogen.core.to_text(self.username) + + def on_disconnect(self, broker): + self.tty_stream.on_disconnect(broker) + super(Stream, self).on_disconnect(broker) + + def get_boot_command(self): + bits = [self.doas_path, '-u', self.username, '--'] + bits = bits + super(Stream, self).get_boot_command() + LOG.debug('doas command line: %r', bits) + return bits + + password_incorrect_msg = 'doas password is incorrect' + password_required_msg = 'doas password is required' + + def _connect_bootstrap(self, extra_fd): + self.tty_stream = mitogen.parent.TtyLogStream(extra_fd, self) + + password_sent = False + it = mitogen.parent.iter_read( + fds=[self.receive_side.fd, extra_fd], + deadline=self.connect_deadline, + ) + + for buf in it: + LOG.debug('%r: received %r', self, buf) + if buf.endswith(self.EC0_MARKER): + 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.tty_stream.transmit_side.write( + mitogen.core.to_text(self.password + '\n').encode('utf-8') + ) + password_sent = True + raise mitogen.core.StreamError('bootstrap failed') diff --git a/mitogen/parent.py b/mitogen/parent.py index 36d107f5..26796113 100644 --- a/mitogen/parent.py +++ b/mitogen/parent.py @@ -1240,6 +1240,9 @@ class Router(mitogen.core.Router): self._context_by_id[context.context_id] = context return context + def doas(self, **kwargs): + return self.connect(u'doas', **kwargs) + def docker(self, **kwargs): return self.connect(u'docker', **kwargs)