Source code for powermolelib.transferagent

#!/usr/bin/env python
# -*- coding: utf-8 -*-
# File: transferagent.py
#
# Copyright 2021 Vincent Schouten
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
#  of this software and associated documentation files (the "Software"), to
#  deal in the Software without restriction, including without limitation the
#  rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
#  sell copies of the Software, and to permit persons to whom the Software is
#  furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
#  all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
#  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
#  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
#  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
#  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
#  FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
#  DEALINGS IN THE SOFTWARE.
#

"""
Main code for transferagent.

.. _Google Python Style Guide:
   http://google.github.io/styleguide/pyguide.html

NOTE: The TransferAgent class is responsible to purge the stream (ie. index in stream is at COMMAND_PROMPT)

"""

import inspect
import os.path
import pexpect
from .logging import LoggerMixin

__author__ = '''Vincent Schouten <powermole@protonmail.com>'''
__docformat__ = '''google'''
__date__ = '''10-05-2019'''
__copyright__ = '''Copyright 2021, Vincent Schouten'''
__credits__ = ["Vincent Schouten"]
__license__ = '''MIT'''
__maintainer__ = '''Vincent Schouten'''
__email__ = '''<powermole@protonmail.com>'''
__status__ = '''Development'''  # "Prototype", "Development", "Production".

# Constant for Pexpect. This prompt is default for Fedora and CentOS.
COMMAND_PROMPT = '[#$] '


[docs]class TransferAgent(LoggerMixin): """Establishes a connection to the target destination host via one or more intermediaries.""" def __init__(self, path_ssh_cfg, all_host_addr, deploy_path='/tmp'): """Initializes the TransferAgent object. Args: path_ssh_cfg (str): Path to the SSH config file that is generated by write_ssh_config_file(). all_host_addr (list): IP addresses of all hosts (eg. gateway/intermediary and destination hosts). deploy_path (str): Path where the agent.py module (Agent) will reside. """ super().__init__() self.path_ssh_cfg = path_ssh_cfg self.all_host_addr = all_host_addr self.last_host_addr = all_host_addr[-1] self.deploy_path = deploy_path self.child = None self.authenticated_hosts = [] def __str__(self): return 'TransferAgent' @property def _path_to_agent_module(self): """Determines the path to the Agent-module.""" running_script = inspect.getframeinfo(inspect.currentframe()).filename running_script_dir = os.path.dirname(os.path.abspath(running_script)) path_file = os.path.join(running_script_dir, 'payload', 'agent.py') self._logger.debug('agent.py resides in: %s', running_script_dir) return path_file def _generate_ssh_runtime_param(self): """Composes a SSH runtime param command.""" if len(self.all_host_addr) == 2: # only 1 gateway + destination host # the result will be something in this format: # scp -F {} -o 'ProxyJump 10.10.1.72' /home/vincent/Pictures/andy_apollo_imdb.jpg 10.10.2.92:/tmp order_of_hosts = f'{self.all_host_addr[0]}' else: # more than 1 gateway + destination host # the result will be something in this format: # scp -F {} -o 'ProxyJump 10.10.1.72,10.10.2.92' /home/vincent/Pictures/andy_apollo_imdb.jpg 10.10.3.82:/tmp order_of_hosts = '' for i, host in enumerate(self.all_host_addr): if i == 0: order_of_hosts += f'{host}' elif i < len(self.all_host_addr) - 1: order_of_hosts += f',{host}' else: order_of_hosts += f' {host}' # determine if this branch is still necessary. runtime_param = f"scp -v -F {self.path_ssh_cfg} -o 'ProxyJump {order_of_hosts}' " \ f"{self._path_to_agent_module} " runtime_param += f'{self.last_host_addr}:{self.deploy_path}' self._logger.debug(runtime_param) return runtime_param
[docs] def start(self): """Transfers the Agent module. It determines along the way if the authentication process is successful. """ result = False try: self.child = pexpect.spawn(self._generate_ssh_runtime_param(), env={"TERM": "dumb"}, timeout=10) # self.process.setecho(False) # doesn't seem to have effect # self.process.waitnoecho() # doesn't seem to have effect self._logger.debug('going through the stream to match patterns: %s', self.all_host_addr) for hostname in self.all_host_addr: # according to the documentation, "If you wish to read up to the end of the child's output - # without generating an EOF exception then use the expect(pexpect.EOF) method." # but apparently this doesn't work in a shell within a shell (SSH spawns a new shell) index = self.child.expect( [f'Authenticated to {hostname}', 'Last failed login:', 'Last login:', 'socket error', 'not accessible', 'fingerprint', 'open failed: connect failed:', pexpect.TIMEOUT]) result = False # reset var as this var could be set True in a previous iteration, we want fresh start if index == 0: self._logger.info('authenticated to %s', hostname) # logger level is "info" to inform user self.authenticated_hosts.append(hostname) result = True elif index == 1: self._logger.debug('there were failed login attempts') result = True elif index == 2: self._logger.debug('there were no failed login attempts') result = True elif index == 3: self._logger.error('socket error. probable cause: SCP service on proxy or target machine disabled') break elif index == 4: self._logger.error('the identity file is not accessible') break elif index == 5: self._logger.warning('warning: hostname automatically added to list of known hosts') self.child.sendline('yes') # security issue elif index == 6: self._logger.error('SCP could not connect to %s', hostname) break elif index == 7: self._logger.error('TIMEOUT exception was thrown. SCP could probably not connect to %s', hostname) break else: self._logger.error('unknown state reached') self.child.expect(pexpect.EOF) # the buffer has to be 'read' continuously, otherwise Pexpect deteriorates except pexpect.exceptions.ExceptionPexpect: self._logger.error('EOF is read; SCP has exited abnormally.') self.child.terminate() if not result: self._logger.error('debug information: %s', str(self.child)) self.child.terminate() return result