def _connect(self): import paramiko # Imported here due to relatively slow import self.__client = paramiko.SSHClient() self.__client.set_missing_host_key_policy(paramiko.client.AutoAddPolicy()) self.__client.load_system_host_keys() try: self.__client.connect(self.host, username=self.username) self.__client_sftp = paramiko.SFTPClient.from_transport(self.__client.get_transport()) except paramiko.SSHException as e: if len(e.args) == 1 and e.args[0] == 'No authentication methods available': raise DuctAuthenticationError(e.args[0]) raise e
def _connect(self): """ The workflow to handle passwords and host keys used by this method is inspired by the `pxssh` module of `pexpect` (https://github.com/pexpect/pexpect). We have adjusted this workflow to our purposes. """ import pexpect # Create socket directory if it doesn't exist. socket_dir = os.path.dirname(self._socket_path) if not os.path.exists(socket_dir): os.makedirs(socket_dir) # Create persistent master connection and exit. cmd = ''.join([ "ssh {login} -MT ", "-S {socket} ", "-o ControlPersist=yes ", "-o StrictHostKeyChecking=no ", "-o UserKnownHostsFile=/dev/null " if not self.check_known_hosts else "", "-o NoHostAuthenticationForLocalhost=yes ", "-o ServerAliveInterval=60 ", "-o ServerAliveCountMax=2 ", "'exit'", ]).format(login=self._login_info, socket=self._socket_path) expected = [ "WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED!", # 0 "(?i)are you sure you want to continue connecting", # 1 "(?i)(?:(?:password)|(?:passphrase for key)):", # 2 "(?i)permission denied", # 3 "(?i)terminal type", # 4 pexpect.TIMEOUT, # 5 "(?i)connection closed by remote host", # 6 "(?i)could not resolve hostname", # 7 pexpect.EOF # 8 ] try: expect = pexpect.spawn(cmd) i = expect.expect(expected, timeout=10) # First phase if i == 0: # If host identification changed, arrest any further attempts to connect error_message = ( 'Host identification for {} has changed! This is most likely ' 'due to the the server being redeployed or reconfigured but ' 'may also be due to a man-in-the-middle attack. If you trust ' 'your network connection, you should be safe to update the ' 'host keys for this host. To do this manually, please remove ' 'the line corresponding to this host in ~/.ssh/known_hosts; ' 'or call the `update_host_keys` method of this client.'.format(self._host) ) if self.interactive: logger.error(error_message) auto_fix = input('Would you like this client to do this for you? (y/n)') if auto_fix == 'y': self.update_host_keys() return self.connect() else: raise RuntimeError("Host keys not updated. Please update keys manually.") else: raise RuntimeError(error_message) if i == 1: # Request to authorize host certificate (i.e. host not in the 'known_hosts' file) expect.sendline("yes") i = self.expect(expected) if i == 2: # Request for password/passphrase expect.sendline(self.password or getpass.getpass('Password: '******'ascii') i = self.expect(expected) # Second phase if i == 1: # Another request to authorize host certificate (i.e. host not in the 'known_hosts' file) raise RuntimeError('Received a second request to authorize host key. This should not have happened!') elif i in (2, 3): # Second request for password/passphrase or rejection of credentials. For now, give up. raise DuctAuthenticationError('Invalid username and/or password, or private key is not unlocked.') elif i == 4: # Another request for terminal type. raise RuntimeError('Received a second request for terminal type. This should not have happened!') elif i == 5: # Timeout # In our instance, this means that we have not handled some or another aspect of the login procedure. # Since we are expecting an EOF when we have successfully logged in, hanging means that the SSH login # procedure is waiting for more information. Since we have no more to give, this means our login # was unsuccessful. raise RuntimeError('SSH client seems to be awaiting more information, but we have no more to give. The ' 'messages received so far are:\n{}'.format(expect.before)) elif i == 6: # Connection closed by remote host raise RuntimeError("Remote closed SSH connection") elif i == 7: raise RuntimeError("Cannot connect to {} on your current network connection".format(self.host)) finally: expect.close() # We should be logged in at this point, but let us make doubly sure assert self.is_connected(), 'Unexpected failure to establish a connection with the remote host with command: \n ' \ '{}\n\n Please report this!'.format(cmd)