diff options
author | Adam Meily <meily.adam@gmail.com> | 2015-12-17 20:18:00 -0500 |
---|---|---|
committer | Adam Meily <meily.adam@gmail.com> | 2015-12-17 20:18:00 -0500 |
commit | 9a87a8eb599d2b3914a3f3fcedbea8b25ab33d5a (patch) | |
tree | 9ca66cf1c5f262e55f5c9cf420d316a6f25d0a00 /paramiko | |
parent | 787839d3bc41cdd86905df6883b7de2b1bfb165a (diff) | |
parent | 844dda88ca1827ede2c3f73792facab7d28eef74 (diff) | |
download | paramiko-9a87a8eb599d2b3914a3f3fcedbea8b25ab33d5a.tar.gz |
Merge branch 'master' of github.com:paramiko/paramiko
Conflicts:
paramiko/sftp_file.py
sites/www/changelog.rst
Diffstat (limited to 'paramiko')
-rw-r--r-- | paramiko/_winapi.py | 143 | ||||
-rw-r--r-- | paramiko/agent.py | 20 | ||||
-rw-r--r-- | paramiko/auth_handler.py | 24 | ||||
-rw-r--r-- | paramiko/buffered_pipe.py | 4 | ||||
-rw-r--r-- | paramiko/channel.py | 18 | ||||
-rw-r--r-- | paramiko/client.py | 32 | ||||
-rw-r--r-- | paramiko/config.py | 4 | ||||
-rw-r--r-- | paramiko/ecdsakey.py | 8 | ||||
-rw-r--r-- | paramiko/hostkeys.py | 8 | ||||
-rw-r--r-- | paramiko/kex_gex.py | 13 | ||||
-rw-r--r-- | paramiko/kex_group1.py | 1 | ||||
-rw-r--r-- | paramiko/kex_group14.py | 2 | ||||
-rw-r--r-- | paramiko/kex_gss.py | 5 | ||||
-rw-r--r-- | paramiko/message.py | 45 | ||||
-rw-r--r-- | paramiko/packet.py | 49 | ||||
-rw-r--r-- | paramiko/pkey.py | 24 | ||||
-rw-r--r-- | paramiko/py3compat.py | 5 | ||||
-rw-r--r-- | paramiko/server.py | 2 | ||||
-rw-r--r-- | paramiko/sftp_attr.py | 5 | ||||
-rw-r--r-- | paramiko/sftp_client.py | 8 | ||||
-rw-r--r-- | paramiko/sftp_file.py | 7 | ||||
-rw-r--r-- | paramiko/ssh_exception.py | 8 | ||||
-rw-r--r-- | paramiko/ssh_gss.py | 31 | ||||
-rw-r--r-- | paramiko/transport.py | 241 | ||||
-rw-r--r-- | paramiko/util.py | 2 |
25 files changed, 512 insertions, 197 deletions
diff --git a/paramiko/_winapi.py b/paramiko/_winapi.py index d6aabf76..9a8bdedd 100644 --- a/paramiko/_winapi.py +++ b/paramiko/_winapi.py @@ -1,23 +1,16 @@ """ Windows API functions implemented as ctypes functions and classes as found -in jaraco.windows (2.10). +in jaraco.windows (3.3). If you encounter issues with this module, please consider reporting the issues in jaraco.windows and asking the author to port the fixes back here. """ -import ctypes +import sys import ctypes.wintypes -from paramiko.py3compat import u -try: - import builtins -except ImportError: - import __builtin__ as builtins -try: - USHORT = ctypes.wintypes.USHORT -except AttributeError: - USHORT = ctypes.c_ushort +from paramiko.py3compat import u, builtins + ###################### # jaraco.windows.error @@ -29,11 +22,7 @@ def format_system_message(errno): """ # first some flags used by FormatMessageW ALLOCATE_BUFFER = 0x100 - ARGUMENT_ARRAY = 0x2000 - FROM_HMODULE = 0x800 - FROM_STRING = 0x400 FROM_SYSTEM = 0x1000 - IGNORE_INSERTS = 0x200 # Let FormatMessageW allocate the buffer (we'll free it below) # Also, let it know we want a system error message. @@ -44,7 +33,7 @@ def format_system_message(errno): result_buffer = ctypes.wintypes.LPWSTR() buffer_size = 0 arguments = None - format_bytes = ctypes.windll.kernel32.FormatMessageW( + bytes = ctypes.windll.kernel32.FormatMessageW( flags, source, message_id, @@ -52,11 +41,11 @@ def format_system_message(errno): ctypes.byref(result_buffer), buffer_size, arguments, - ) + ) # note the following will cause an infinite loop if GetLastError # repeatedly returns an error that cannot be formatted, although # this should not happen. - handle_nonzero_success(format_bytes) + handle_nonzero_success(bytes) message = result_buffer.value ctypes.windll.kernel32.LocalFree(result_buffer) return message @@ -69,7 +58,11 @@ class WindowsError(builtins.WindowsError): if value is None: value = ctypes.windll.kernel32.GetLastError() strerror = format_system_message(value) - super(WindowsError, self).__init__(value, strerror) + if sys.version_info > (3,3): + args = 0, strerror, None, value + else: + args = value, strerror + super(WindowsError, self).__init__(*args) @property def message(self): @@ -90,6 +83,27 @@ def handle_nonzero_success(result): raise WindowsError() +########################### +# jaraco.windows.api.memory + +GMEM_MOVEABLE = 0x2 + +GlobalAlloc = ctypes.windll.kernel32.GlobalAlloc +GlobalAlloc.argtypes = ctypes.wintypes.UINT, ctypes.c_ssize_t +GlobalAlloc.restype = ctypes.wintypes.HANDLE + +GlobalLock = ctypes.windll.kernel32.GlobalLock +GlobalLock.argtypes = ctypes.wintypes.HGLOBAL, +GlobalLock.restype = ctypes.wintypes.LPVOID + +GlobalUnlock = ctypes.windll.kernel32.GlobalUnlock +GlobalUnlock.argtypes = ctypes.wintypes.HGLOBAL, +GlobalUnlock.restype = ctypes.wintypes.BOOL + +GlobalSize = ctypes.windll.kernel32.GlobalSize +GlobalSize.argtypes = ctypes.wintypes.HGLOBAL, +GlobalSize.restype = ctypes.c_size_t + CreateFileMapping = ctypes.windll.kernel32.CreateFileMappingW CreateFileMapping.argtypes = [ ctypes.wintypes.HANDLE, @@ -104,6 +118,9 @@ CreateFileMapping.restype = ctypes.wintypes.HANDLE MapViewOfFile = ctypes.windll.kernel32.MapViewOfFile MapViewOfFile.restype = ctypes.wintypes.HANDLE +##################### +# jaraco.windows.mmap + class MemoryMap(object): """ A memory map object which can have security attributes overridden. @@ -136,10 +153,13 @@ class MemoryMap(object): self.pos = pos def write(self, msg): + assert isinstance(msg, bytes) n = len(msg) if self.pos + n >= self.length: # A little safety. raise ValueError("Refusing to write %d bytes" % n) - ctypes.windll.kernel32.RtlMoveMemory(self.view + self.pos, msg, n) + dest = self.view + self.pos + length = ctypes.wintypes.SIZE(n) + ctypes.windll.kernel32.RtlMoveMemory(dest, msg, length) self.pos += n def read(self, n): @@ -147,7 +167,9 @@ class MemoryMap(object): Read n bytes from mapped view. """ out = ctypes.create_string_buffer(n) - ctypes.windll.kernel32.RtlMoveMemory(out, self.view + self.pos, n) + source = self.view + self.pos + length = ctypes.wintypes.SIZE(n) + ctypes.windll.kernel32.RtlMoveMemory(out, source, length) self.pos += n return out.raw @@ -155,8 +177,71 @@ class MemoryMap(object): ctypes.windll.kernel32.UnmapViewOfFile(self.view) ctypes.windll.kernel32.CloseHandle(self.filemap) -######################### -# jaraco.windows.security +############################# +# jaraco.windows.api.security + +# from WinNT.h +READ_CONTROL = 0x00020000 +STANDARD_RIGHTS_REQUIRED = 0x000F0000 +STANDARD_RIGHTS_READ = READ_CONTROL +STANDARD_RIGHTS_WRITE = READ_CONTROL +STANDARD_RIGHTS_EXECUTE = READ_CONTROL +STANDARD_RIGHTS_ALL = 0x001F0000 + +# from NTSecAPI.h +POLICY_VIEW_LOCAL_INFORMATION = 0x00000001 +POLICY_VIEW_AUDIT_INFORMATION = 0x00000002 +POLICY_GET_PRIVATE_INFORMATION = 0x00000004 +POLICY_TRUST_ADMIN = 0x00000008 +POLICY_CREATE_ACCOUNT = 0x00000010 +POLICY_CREATE_SECRET = 0x00000020 +POLICY_CREATE_PRIVILEGE = 0x00000040 +POLICY_SET_DEFAULT_QUOTA_LIMITS = 0x00000080 +POLICY_SET_AUDIT_REQUIREMENTS = 0x00000100 +POLICY_AUDIT_LOG_ADMIN = 0x00000200 +POLICY_SERVER_ADMIN = 0x00000400 +POLICY_LOOKUP_NAMES = 0x00000800 +POLICY_NOTIFICATION = 0x00001000 + +POLICY_ALL_ACCESS = ( + STANDARD_RIGHTS_REQUIRED | + POLICY_VIEW_LOCAL_INFORMATION | + POLICY_VIEW_AUDIT_INFORMATION | + POLICY_GET_PRIVATE_INFORMATION | + POLICY_TRUST_ADMIN | + POLICY_CREATE_ACCOUNT | + POLICY_CREATE_SECRET | + POLICY_CREATE_PRIVILEGE | + POLICY_SET_DEFAULT_QUOTA_LIMITS | + POLICY_SET_AUDIT_REQUIREMENTS | + POLICY_AUDIT_LOG_ADMIN | + POLICY_SERVER_ADMIN | + POLICY_LOOKUP_NAMES) + + +POLICY_READ = ( + STANDARD_RIGHTS_READ | + POLICY_VIEW_AUDIT_INFORMATION | + POLICY_GET_PRIVATE_INFORMATION) + +POLICY_WRITE = ( + STANDARD_RIGHTS_WRITE | + POLICY_TRUST_ADMIN | + POLICY_CREATE_ACCOUNT | + POLICY_CREATE_SECRET | + POLICY_CREATE_PRIVILEGE | + POLICY_SET_DEFAULT_QUOTA_LIMITS | + POLICY_SET_AUDIT_REQUIREMENTS | + POLICY_AUDIT_LOG_ADMIN | + POLICY_SERVER_ADMIN) + +POLICY_EXECUTE = ( + STANDARD_RIGHTS_EXECUTE | + POLICY_VIEW_LOCAL_INFORMATION | + POLICY_LOOKUP_NAMES) + +class TokenAccess: + TOKEN_QUERY = 0x8 class TokenInformationClass: TokenUser = 1 @@ -182,7 +267,7 @@ class SECURITY_DESCRIPTOR(ctypes.Structure): PACL Dacl; } SECURITY_DESCRIPTOR; """ - SECURITY_DESCRIPTOR_CONTROL = USHORT + SECURITY_DESCRIPTOR_CONTROL = ctypes.wintypes.USHORT REVISION = 1 _fields_ = [ @@ -219,8 +304,11 @@ class SECURITY_ATTRIBUTES(ctypes.Structure): @descriptor.setter def descriptor(self, value): - self._descriptor = descriptor - self.lpSecurityDescriptor = ctypes.addressof(descriptor) + self._descriptor = value + self.lpSecurityDescriptor = ctypes.addressof(value) + +######################### +# jaraco.windows.security def GetTokenInformation(token, information_class): """ @@ -236,9 +324,6 @@ def GetTokenInformation(token, information_class): ctypes.byref(data_size))) return ctypes.cast(data, ctypes.POINTER(TOKEN_USER)).contents -class TokenAccess: - TOKEN_QUERY = 0x8 - def OpenProcessToken(proc_handle, access): result = ctypes.wintypes.HANDLE() proc_handle = ctypes.wintypes.HANDLE(proc_handle) diff --git a/paramiko/agent.py b/paramiko/agent.py index f928881e..6a8e7fb4 100644 --- a/paramiko/agent.py +++ b/paramiko/agent.py @@ -290,6 +290,26 @@ class AgentServerProxy(AgentSSH): class AgentRequestHandler(object): + """ + Primary/default implementation of SSH agent forwarding functionality. + + Simply instantiate this class, handing it a live command-executing session + object, and it will handle forwarding any local SSH agent processes it + finds. + + For example:: + + # Connect + client = SSHClient() + client.connect(host, port, username) + # Obtain session + session = client.get_transport().open_session() + # Forward local agent + AgentRequestHandler(session) + # Commands executed after this point will see the forwarded agent on + # the remote end. + session.exec_command("git clone https://my.git.repository/") + """ def __init__(self, chanClient): self._conn = None self.__chanC = chanClient diff --git a/paramiko/auth_handler.py b/paramiko/auth_handler.py index c001aeee..ef4a8c7e 100644 --- a/paramiko/auth_handler.py +++ b/paramiko/auth_handler.py @@ -34,7 +34,7 @@ from paramiko.common import cMSG_SERVICE_REQUEST, cMSG_DISCONNECT, \ cMSG_USERAUTH_GSSAPI_ERRTOK, cMSG_USERAUTH_GSSAPI_MIC,\ MSG_USERAUTH_GSSAPI_RESPONSE, MSG_USERAUTH_GSSAPI_TOKEN, \ MSG_USERAUTH_GSSAPI_EXCHANGE_COMPLETE, MSG_USERAUTH_GSSAPI_ERROR, \ - MSG_USERAUTH_GSSAPI_ERRTOK, MSG_USERAUTH_GSSAPI_MIC + MSG_USERAUTH_GSSAPI_ERRTOK, MSG_USERAUTH_GSSAPI_MIC, MSG_NAMES from paramiko.message import Message from paramiko.py3compat import bytestring @@ -510,15 +510,11 @@ class AuthHandler (object): result = AUTH_FAILED self._send_auth_result(username, method, result) raise - if retval == 0: - # TODO: Implement client credential saving. - # The OpenSSH server is able to create a TGT with the delegated - # client credentials, but this is not supported by GSS-API. - result = AUTH_SUCCESSFUL - self.transport.server_object.check_auth_gssapi_with_mic( - username, result) - else: - result = AUTH_FAILED + # TODO: Implement client credential saving. + # The OpenSSH server is able to create a TGT with the delegated + # client credentials, but this is not supported by GSS-API. + result = AUTH_SUCCESSFUL + self.transport.server_object.check_auth_gssapi_with_mic(username, result) elif method == "gssapi-keyex" and gss_auth: mic_token = m.get_string() sshgss = self.transport.kexgss_ctxt @@ -534,12 +530,8 @@ class AuthHandler (object): result = AUTH_FAILED self._send_auth_result(username, method, result) raise - if retval == 0: - result = AUTH_SUCCESSFUL - self.transport.server_object.check_auth_gssapi_keyex(username, - result) - else: - result = AUTH_FAILED + result = AUTH_SUCCESSFUL + self.transport.server_object.check_auth_gssapi_keyex(username, result) else: result = self.transport.server_object.check_auth_none(username) # okay, send result diff --git a/paramiko/buffered_pipe.py b/paramiko/buffered_pipe.py index ac35b3e1..d5fe164e 100644 --- a/paramiko/buffered_pipe.py +++ b/paramiko/buffered_pipe.py @@ -81,7 +81,7 @@ class BufferedPipe (object): Feed new data into this pipe. This method is assumed to be called from a separate thread, so synchronization is done. - :param data: the data to add, as a `str` + :param data: the data to add, as a `str` or `bytes` """ self._lock.acquire() try: @@ -125,7 +125,7 @@ class BufferedPipe (object): :param int nbytes: maximum number of bytes to read :param float timeout: maximum seconds to wait (or ``None``, the default, to wait forever) - :return: the read data, as a `str` + :return: the read data, as a `bytes` :raises PipeTimeout: if a timeout was specified and no data was ready before that diff --git a/paramiko/channel.py b/paramiko/channel.py index 7e39a15b..4ce4f286 100644 --- a/paramiko/channel.py +++ b/paramiko/channel.py @@ -286,7 +286,8 @@ class Channel (ClosingContextManager): return an exit status in some cases (like bad servers). :return: - ``True`` if `recv_exit_status` will return immediately, else ``False``. + ``True`` if `recv_exit_status` will return immediately, else + ``False``. .. versionadded:: 1.7.3 """ @@ -300,6 +301,17 @@ class Channel (ClosingContextManager): it does, or until the channel is closed. If no exit status is provided by the server, -1 is returned. + .. warning:: + In some situations, receiving remote output larger than the current + `.Transport` or session's ``window_size`` (e.g. that set by the + ``default_window_size`` kwarg for `.Transport.__init__`) will cause + `.recv_exit_status` to hang indefinitely if it is called prior to a + sufficiently large `~Channel..read` (or if there are no threads + calling `~Channel.read` in the background). + + In these cases, ensuring that `.recv_exit_status` is called *after* + `~Channel.read` (or, again, using threads) can avoid the hang. + :return: the exit code (as an `int`) of the process on the server. .. versionadded:: 1.2 @@ -587,7 +599,7 @@ class Channel (ClosingContextManager): is returned, the channel stream has closed. :param int nbytes: maximum number of bytes to read. - :return: received data, as a `str` + :return: received data, as a `bytes` :raises socket.timeout: if no data is ready before the timeout set by `settimeout`. @@ -975,7 +987,7 @@ class Channel (ClosingContextManager): else: ok = server.check_channel_env_request(self, name, value) elif key == 'exec': - cmd = m.get_text() + cmd = m.get_string() if server is None: ok = False else: diff --git a/paramiko/client.py b/paramiko/client.py index e10e9da7..8d899a15 100644 --- a/paramiko/client.py +++ b/paramiko/client.py @@ -256,7 +256,8 @@ class SSHClient (ClosingContextManager): :param socket sock: an open socket or socket-like object (such as a `.Channel`) to use for communication to the target host - :param bool gss_auth: ``True`` if you want to use GSS-API authentication + :param bool gss_auth: + ``True`` if you want to use GSS-API authentication :param bool gss_kex: Perform GSS-API Key Exchange and user authentication :param bool gss_deleg_creds: Delegate GSS-API client credentials or not @@ -397,7 +398,7 @@ class SSHClient (ClosingContextManager): :raises SSHException: if the server fails to execute the command """ - chan = self._transport.open_session() + chan = self._transport.open_session(timeout=timeout) if get_pty: chan.get_pty() chan.settimeout(timeout) @@ -463,7 +464,8 @@ class SSHClient (ClosingContextManager): """ saved_exception = None two_factor = False - allowed_types = [] + allowed_types = set() + two_factor_types = set(['keyboard-interactive','password']) # If GSS-API support and GSS-PI Key Exchange was performed, we attempt # authentication with gssapi-keyex. @@ -489,8 +491,8 @@ class SSHClient (ClosingContextManager): if pkey is not None: try: self._log(DEBUG, 'Trying SSH key %s' % hexlify(pkey.get_fingerprint())) - allowed_types = self._transport.auth_publickey(username, pkey) - two_factor = (allowed_types == ['password']) + allowed_types = set(self._transport.auth_publickey(username, pkey)) + two_factor = (allowed_types & two_factor_types) if not two_factor: return except SSHException as e: @@ -502,8 +504,8 @@ class SSHClient (ClosingContextManager): try: key = pkey_class.from_private_key_file(key_filename, password) self._log(DEBUG, 'Trying key %s from %s' % (hexlify(key.get_fingerprint()), key_filename)) - self._transport.auth_publickey(username, key) - two_factor = (allowed_types == ['password']) + allowed_types = set(self._transport.auth_publickey(username, key)) + two_factor = (allowed_types & two_factor_types) if not two_factor: return break @@ -517,9 +519,9 @@ class SSHClient (ClosingContextManager): for key in self._agent.get_keys(): try: self._log(DEBUG, 'Trying SSH agent key %s' % hexlify(key.get_fingerprint())) - # for 2-factor auth a successfully auth'd key will result in ['password'] - allowed_types = self._transport.auth_publickey(username, key) - two_factor = (allowed_types == ['password']) + # for 2-factor auth a successfully auth'd key password will return an allowed 2fac auth method + allowed_types = set(self._transport.auth_publickey(username, key)) + two_factor = (allowed_types & two_factor_types) if not two_factor: return break @@ -556,8 +558,8 @@ class SSHClient (ClosingContextManager): key = pkey_class.from_private_key_file(filename, password) self._log(DEBUG, 'Trying discovered key %s in %s' % (hexlify(key.get_fingerprint()), filename)) # for 2-factor auth a successfully auth'd key will result in ['password'] - allowed_types = self._transport.auth_publickey(username, key) - two_factor = (allowed_types == ['password']) + allowed_types = set(self._transport.auth_publickey(username, key)) + two_factor = (allowed_types & two_factor_types) if not two_factor: return break @@ -571,7 +573,11 @@ class SSHClient (ClosingContextManager): except SSHException as e: saved_exception = e elif two_factor: - raise SSHException('Two-factor authentication requires a password') + try: + self._transport.auth_interactive_dumb(username) + return + except SSHException as e: + saved_exception = e # if we got an auth-failed exception earlier, re-raise it if saved_exception is not None: diff --git a/paramiko/config.py b/paramiko/config.py index 233a87d9..0b1345fd 100644 --- a/paramiko/config.py +++ b/paramiko/config.py @@ -53,7 +53,7 @@ class SSHConfig (object): """ Read an OpenSSH config from the given file object. - :param file file_obj: a file-like object to read the config file from + :param file_obj: a file-like object to read the config file from """ host = {"host": ['*'], "config": {}} for line in file_obj: @@ -98,7 +98,7 @@ class SSHConfig (object): The host-matching rules of OpenSSH's ``ssh_config`` man page are used: For each parameter, the first obtained value will be used. The - configuration files contain sections separated by ``Host'' + configuration files contain sections separated by ``Host`` specifications, and that section is only applied for hosts that match one of the patterns given in the specification. diff --git a/paramiko/ecdsakey.py b/paramiko/ecdsakey.py index 6b047959..8827a1db 100644 --- a/paramiko/ecdsakey.py +++ b/paramiko/ecdsakey.py @@ -129,13 +129,11 @@ class ECDSAKey (PKey): @staticmethod def generate(curve=curves.NIST256p, progress_func=None): """ - Generate a new private RSA key. This factory function can be used to + Generate a new private ECDSA key. This factory function can be used to generate a new host key or authentication key. - :param function progress_func: - an optional function to call at key points in key generation (used - by ``pyCrypto.PublicKey``). - :returns: A new private key (`.RSAKey`) object + :param function progress_func: Not used for this type of key. + :returns: A new private key (`.ECDSAKey`) object """ signing_key = SigningKey.generate(curve) key = ECDSAKey(vals=(signing_key, signing_key.get_verifying_key())) diff --git a/paramiko/hostkeys.py b/paramiko/hostkeys.py index 84868875..38ac866b 100644 --- a/paramiko/hostkeys.py +++ b/paramiko/hostkeys.py @@ -35,6 +35,7 @@ from paramiko.dsskey import DSSKey from paramiko.rsakey import RSAKey from paramiko.util import get_logger, constant_time_bytes_eq from paramiko.ecdsakey import ECDSAKey +from paramiko.ssh_exception import SSHException class HostKeys (MutableMapping): @@ -92,11 +93,14 @@ class HostKeys (MutableMapping): :raises IOError: if there was an error reading the file """ with open(filename, 'r') as f: - for lineno, line in enumerate(f): + for lineno, line in enumerate(f, 1): line = line.strip() if (len(line) == 0) or (line[0] == '#'): continue - e = HostKeyEntry.from_line(line, lineno) + try: + e = HostKeyEntry.from_line(line, lineno) + except SSHException: + continue if e is not None: _hostnames = e.hostnames for h in _hostnames: diff --git a/paramiko/kex_gex.py b/paramiko/kex_gex.py index cb548f33..c980b690 100644 --- a/paramiko/kex_gex.py +++ b/paramiko/kex_gex.py @@ -23,7 +23,7 @@ client side, and a **lot** more on the server side. """ import os -from hashlib import sha1 +from hashlib import sha1, sha256 from paramiko import util from paramiko.common import DEBUG @@ -44,6 +44,7 @@ class KexGex (object): min_bits = 1024 max_bits = 8192 preferred_bits = 2048 + hash_algo = sha1 def __init__(self, transport): self.transport = transport @@ -87,7 +88,7 @@ class KexGex (object): return self._parse_kexdh_gex_reply(m) elif ptype == _MSG_KEXDH_GEX_REQUEST_OLD: return self._parse_kexdh_gex_request_old(m) - raise SSHException('KexGex asked to handle packet type %d' % ptype) + raise SSHException('KexGex %s asked to handle packet type %d' % self.name, ptype) ### internals... @@ -204,7 +205,7 @@ class KexGex (object): hm.add_mpint(self.e) hm.add_mpint(self.f) hm.add_mpint(K) - H = sha1(hm.asbytes()).digest() + H = self.hash_algo(hm.asbytes()).digest() self.transport._set_K_H(K, H) # sign it sig = self.transport.get_server_key().sign_ssh_data(H) @@ -239,6 +240,10 @@ class KexGex (object): hm.add_mpint(self.e) hm.add_mpint(self.f) hm.add_mpint(K) - self.transport._set_K_H(K, sha1(hm.asbytes()).digest()) + self.transport._set_K_H(K, self.hash_algo(hm.asbytes()).digest()) self.transport._verify_key(host_key, sig) self.transport._activate_outbound() + +class KexGexSHA256(KexGex): + name = 'diffie-hellman-group-exchange-sha256' + hash_algo = sha256 diff --git a/paramiko/kex_group1.py b/paramiko/kex_group1.py index a88f00d2..9eee066c 100644 --- a/paramiko/kex_group1.py +++ b/paramiko/kex_group1.py @@ -45,6 +45,7 @@ class KexGroup1(object): G = 2 name = 'diffie-hellman-group1-sha1' + hash_algo = sha1 def __init__(self, transport): self.transport = transport diff --git a/paramiko/kex_group14.py b/paramiko/kex_group14.py index a914aeaf..9f7dd216 100644 --- a/paramiko/kex_group14.py +++ b/paramiko/kex_group14.py @@ -22,6 +22,7 @@ Standard SSH key exchange ("kex" if you wanna sound cool). Diffie-Hellman of """ from paramiko.kex_group1 import KexGroup1 +from hashlib import sha1 class KexGroup14(KexGroup1): @@ -31,3 +32,4 @@ class KexGroup14(KexGroup1): G = 2 name = 'diffie-hellman-group14-sha1' + hash_algo = sha1 diff --git a/paramiko/kex_gss.py b/paramiko/kex_gss.py index d026807c..69969f8a 100644 --- a/paramiko/kex_gss.py +++ b/paramiko/kex_gss.py @@ -37,6 +37,7 @@ This module provides GSS-API / SSPI Key Exchange as defined in :rfc:`4462`. .. versionadded:: 1.15 """ +import os from hashlib import sha1 from paramiko.common import * @@ -130,7 +131,7 @@ class KexGSSGroup1(object): larger than q (but this is a tiny tiny subset of potential x). """ while 1: - x_bytes = self.transport.rng.read(128) + x_bytes = os.urandom(128) x_bytes = byte_mask(x_bytes[0], 0x7f) + x_bytes[1:] if (x_bytes[:8] != self.b7fffffffffffffff) and \ (x_bytes[:8] != self.b0000000000000000): @@ -366,7 +367,7 @@ class KexGSSGex(object): qhbyte <<= 1 qmask >>= 1 while True: - x_bytes = self.transport.rng.read(byte_count) + x_bytes = os.urandom(byte_count) x_bytes = byte_mask(x_bytes[0], qmask) + x_bytes[1:] x = util.inflate_long(x_bytes, 1) if (x > 1) and (x < q): diff --git a/paramiko/message.py b/paramiko/message.py index b893e76d..bf4c6b95 100644 --- a/paramiko/message.py +++ b/paramiko/message.py @@ -129,7 +129,7 @@ class Message (object): b = self.get_bytes(1) return b != zero_byte - def get_int(self): + def get_adaptive_int(self): """ Fetch an int from the stream. @@ -141,20 +141,7 @@ class Message (object): byte += self.get_bytes(3) return struct.unpack('>I', byte)[0] - def get_size(self): - """ - Fetch an int from the stream. - - @return: a 32-bit unsigned integer. - @rtype: int - """ - byte = self.get_bytes(1) - if byte == max_byte: - return util.inflate_long(self.get_binary()) - byte += self.get_bytes(3) - return struct.unpack('>I', byte)[0] - - def get_size(self): + def get_int(self): """ Fetch an int from the stream. @@ -185,7 +172,7 @@ class Message (object): contain unprintable characters. (It's not unheard of for a string to contain another byte-stream message.) """ - return self.get_bytes(self.get_size()) + return self.get_bytes(self.get_int()) def get_text(self): """ @@ -196,7 +183,7 @@ class Message (object): @return: a string. @rtype: string """ - return u(self.get_bytes(self.get_size())) + return u(self.get_bytes(self.get_int())) #return self.get_bytes(self.get_size()) def get_binary(self): @@ -208,7 +195,7 @@ class Message (object): @return: a string. @rtype: string """ - return self.get_bytes(self.get_size()) + return self.get_bytes(self.get_int()) def get_list(self): """ @@ -248,7 +235,7 @@ class Message (object): self.packet.write(zero_byte) return self - def add_size(self, n): + def add_int(self, n): """ Add an integer to the stream. @@ -257,7 +244,7 @@ class Message (object): self.packet.write(struct.pack('>I', n)) return self - def add_int(self, n): + def add_adaptive_int(self, n): """ Add an integer to the stream. @@ -270,20 +257,6 @@ class Message (object): self.packet.write(struct.pack('>I', n)) return self - def add_int(self, n): - """ - Add an integer to the stream. - - @param n: integer to add - @type n: int - """ - if n >= Message.big_int: - self.packet.write(max_byte) - self.add_string(util.deflate_long(n)) - else: - self.packet.write(struct.pack('>I', n)) - return self - def add_int64(self, n): """ Add a 64-bit int to the stream. @@ -310,7 +283,7 @@ class Message (object): :param str s: string to add """ s = asbytes(s) - self.add_size(len(s)) + self.add_int(len(s)) self.packet.write(s) return self @@ -329,7 +302,7 @@ class Message (object): if type(i) is bool: return self.add_boolean(i) elif isinstance(i, integer_types): - return self.add_int(i) + return self.add_adaptive_int(i) elif type(i) is list: return self.add_list(i) else: diff --git a/paramiko/packet.py b/paramiko/packet.py index f516ff9b..89a514d1 100644 --- a/paramiko/packet.py +++ b/paramiko/packet.py @@ -99,6 +99,10 @@ class Packetizer (object): self.__keepalive_last = time.time() self.__keepalive_callback = None + self.__timer = None + self.__handshake_complete = False + self.__timer_expired = False + def set_log(self, log): """ Set the Python log object to use for logging. @@ -182,6 +186,46 @@ class Packetizer (object): self.__keepalive_callback = callback self.__keepalive_last = time.time() + def read_timer(self): + self.__timer_expired = True + + def start_handshake(self, timeout): + """ + Tells `Packetizer` that the handshake process started. + Starts a book keeping timer that can signal a timeout in the + handshake process. + + :param float timeout: amount of seconds to wait before timing out + """ + if not self.__timer: + self.__timer = threading.Timer(float(timeout), self.read_timer) + self.__timer.start() + + def handshake_timed_out(self): + """ + Checks if the handshake has timed out. + + If `start_handshake` wasn't called before the call to this function, + the return value will always be `False`. If the handshake completed + before a timeout was reached, the return value will be `False` + + :return: handshake time out status, as a `bool` + """ + if not self.__timer: + return False + if self.__handshake_complete: + return False + return self.__timer_expired + + def complete_handshake(self): + """ + Tells `Packetizer` that the handshake has completed. + """ + if self.__timer: + self.__timer.cancel() + self.__timer_expired = False + self.__handshake_complete = True + def read_all(self, n, check_rekey=False): """ Read as close to N bytes as possible, blocking as long as necessary. @@ -200,6 +244,8 @@ class Packetizer (object): n -= len(out) while n > 0: got_timeout = False + if self.handshake_timed_out(): + raise EOFError() try: x = self.__socket.recv(n) if len(x) == 0: @@ -344,7 +390,8 @@ class Packetizer (object): if self.__dump_packets: self._log(DEBUG, util.format_binary(header, 'IN: ')) packet_size = struct.unpack('>I', header[:4])[0] - # leftover contains decrypted bytes from the first block (after the length field) + # leftover contains decrypted bytes from the first block (after the + # length field) leftover = header[4:] if (packet_size - len(leftover)) % self.__block_size_in != 0: raise SSHException('Invalid packet blocking') diff --git a/paramiko/pkey.py b/paramiko/pkey.py index 1b4af010..e95d60ba 100644 --- a/paramiko/pkey.py +++ b/paramiko/pkey.py @@ -171,8 +171,9 @@ class PKey (object): is useless on the abstract PKey class. :param str filename: name of the file to read - :param str password: an optional password to use to decrypt the key file, - if it's encrypted + :param str password: + an optional password to use to decrypt the key file, if it's + encrypted :return: a new `.PKey` based on the given private key :raises IOError: if there was an error reading the file @@ -187,18 +188,18 @@ class PKey (object): def from_private_key(cls, file_obj, password=None): """ Create a key object by reading a private key from a file (or file-like) - object. If the private key is encrypted and ``password`` is not ``None``, - the given password will be used to decrypt the key (otherwise + object. If the private key is encrypted and ``password`` is not + ``None``, the given password will be used to decrypt the key (otherwise `.PasswordRequiredException` is thrown). - :param file file_obj: the file to read from + :param file_obj: the file-like object to read from :param str password: an optional password to use to decrypt the key, if it's encrypted :return: a new `.PKey` based on the given private key :raises IOError: if there was an error reading the key - :raises PasswordRequiredException: if the private key file is encrypted, - and ``password`` is ``None`` + :raises PasswordRequiredException: + if the private key file is encrypted, and ``password`` is ``None`` :raises SSHException: if the key file is invalid """ key = cls(file_obj=file_obj, password=password) @@ -223,7 +224,7 @@ class PKey (object): Write private key contents into a file (or file-like) object. If the password is not ``None``, the key is encrypted before writing. - :param file file_obj: the file object to write into + :param file_obj: the file-like object to write into :param str password: an optional password to use to encrypt the key :raises IOError: if there was an error writing to the file @@ -273,7 +274,7 @@ class PKey (object): start += 1 # find end end = start - while (lines[end].strip() != '-----END ' + tag + ' PRIVATE KEY-----') and (end < len(lines)): + while end < len(lines) and lines[end].strip() != '-----END ' + tag + ' PRIVATE KEY-----': end += 1 # if we trudged to the end of the file, just try to cope. try: @@ -309,8 +310,9 @@ class PKey (object): a trivially-encoded format (base64) which is completely insecure. If a password is given, DES-EDE3-CBC is used. - :param str tag: ``"RSA"`` or ``"DSA"``, the tag used to mark the data block. - :param file filename: name of the file to write. + :param str tag: + ``"RSA"`` or ``"DSA"``, the tag used to mark the data block. + :param filename: name of the file to write. :param str data: data blob that makes up the private key. :param str password: an optional password to use to encrypt the file. diff --git a/paramiko/py3compat.py b/paramiko/py3compat.py index 57c096b2..6fafc31d 100644 --- a/paramiko/py3compat.py +++ b/paramiko/py3compat.py @@ -3,7 +3,7 @@ import base64 __all__ = ['PY2', 'string_types', 'integer_types', 'text_type', 'bytes_types', 'bytes', 'long', 'input', 'decodebytes', 'encodebytes', 'bytestring', 'byte_ord', 'byte_chr', 'byte_mask', - 'b', 'u', 'b2s', 'StringIO', 'BytesIO', 'is_callable', 'MAXSIZE', 'next'] + 'b', 'u', 'b2s', 'StringIO', 'BytesIO', 'is_callable', 'MAXSIZE', 'next', 'builtins'] PY2 = sys.version_info[0] < 3 @@ -18,6 +18,8 @@ if PY2: decodebytes = base64.decodestring encodebytes = base64.encodestring + import __builtin__ as builtins + def bytestring(s): # NOQA if isinstance(s, unicode): @@ -102,6 +104,7 @@ if PY2: else: import collections import struct + import builtins string_types = str text_type = str bytes = bytes diff --git a/paramiko/server.py b/paramiko/server.py index bf5039a2..f79a1748 100644 --- a/paramiko/server.py +++ b/paramiko/server.py @@ -22,7 +22,7 @@ import threading from paramiko import util -from paramiko.common import DEBUG, ERROR, OPEN_FAILED_ADMINISTRATIVELY_PROHIBITED, AUTH_FAILED +from paramiko.common import DEBUG, ERROR, OPEN_FAILED_ADMINISTRATIVELY_PROHIBITED, AUTH_FAILED, AUTH_SUCCESSFUL from paramiko.py3compat import string_types diff --git a/paramiko/sftp_attr.py b/paramiko/sftp_attr.py index cf48f654..0eaca30b 100644 --- a/paramiko/sftp_attr.py +++ b/paramiko/sftp_attr.py @@ -209,12 +209,15 @@ class SFTPAttributes (object): # not all servers support uid/gid uid = self.st_uid gid = self.st_gid + size = self.st_size if uid is None: uid = 0 if gid is None: gid = 0 + if size is None: + size = 0 - return '%s 1 %-8d %-8d %8d %-12s %s' % (ks, uid, gid, self.st_size, datestr, filename) + return '%s 1 %-8d %-8d %8d %-12s %s' % (ks, uid, gid, size, datestr, filename) def asbytes(self): return b(str(self)) diff --git a/paramiko/sftp_client.py b/paramiko/sftp_client.py index 6d48e692..57225558 100644 --- a/paramiko/sftp_client.py +++ b/paramiko/sftp_client.py @@ -600,7 +600,7 @@ class SFTPClient(BaseSFTP, ClosingContextManager): The SFTP operations use pipelining for speed. - :param file fl: opened file or file-like object to copy + :param fl: opened file or file-like object to copy :param str remotepath: the destination path on the SFTP server :param int file_size: optional size parameter passed to callback. If none is specified, @@ -686,9 +686,10 @@ class SFTPClient(BaseSFTP, ClosingContextManager): .. versionadded:: 1.10 """ + file_size = self.stat(remotepath).st_size with self.open(remotepath, 'rb') as fr: - file_size = self.stat(remotepath).st_size - fr.prefetch() + fr.prefetch(file_size) + size = 0 while True: data = fr.read(32768) @@ -716,7 +717,6 @@ class SFTPClient(BaseSFTP, ClosingContextManager): .. versionchanged:: 1.7.4 Added the ``callback`` param """ - file_size = self.stat(remotepath).st_size with open(localpath, 'wb') as fl: size = self.getfo(remotepath, fl, callback) s = os.stat(localpath) diff --git a/paramiko/sftp_file.py b/paramiko/sftp_file.py index 1d070ee0..3b584dbe 100644 --- a/paramiko/sftp_file.py +++ b/paramiko/sftp_file.py @@ -389,7 +389,7 @@ class SFTPFile (BufferedFile): """ self.pipelined = pipelined - def prefetch(self): + def prefetch(self, file_size): """ Pre-fetch the remaining contents of this file in anticipation of future `.read` calls. If reading the entire file, pre-fetching can @@ -403,12 +403,11 @@ class SFTPFile (BufferedFile): .. versionadded:: 1.5.1 """ - size = self.stat().st_size # queue up async reads for the rest of the file chunks = [] n = self._realpos - while n < size: - chunk = min(self.MAX_REQUEST_SIZE, size - n) + while n < file_size: + chunk = min(self.MAX_REQUEST_SIZE, file_size - n) chunks.append((n, chunk)) n += chunk if len(chunks) > 0: diff --git a/paramiko/ssh_exception.py b/paramiko/ssh_exception.py index d053974a..016a411e 100644 --- a/paramiko/ssh_exception.py +++ b/paramiko/ssh_exception.py @@ -107,7 +107,11 @@ class BadHostKeyException (SSHException): .. versionadded:: 1.6 """ def __init__(self, hostname, got_key, expected_key): - SSHException.__init__(self, 'Host key for server %s does not match!' % hostname) + SSHException.__init__(self, + 'Host key for server %s does not match : got %s expected %s' % ( + hostname, + got_key.get_base64(), + expected_key.get_base64())) self.hostname = hostname self.key = got_key self.expected_key = expected_key @@ -152,6 +156,8 @@ class NoValidConnectionsError(socket.error): It is implied/assumed that all the errors given to a single instance of this class are from connecting to the same hostname + port (and thus that the differences are in the resolution of the hostname - e.g. IPv4 vs v6). + + .. versionadded:: 1.16 """ def __init__(self, errors): """ diff --git a/paramiko/ssh_gss.py b/paramiko/ssh_gss.py index aa28e2ec..e9b13a66 100644 --- a/paramiko/ssh_gss.py +++ b/paramiko/ssh_gss.py @@ -360,8 +360,8 @@ class _SSH_GSSAPI(_SSH_GSSAuth): :param str mic_token: The MIC token received from the client :param str session_id: The SSH session ID :param str username: The name of the user who attempts to login - :return: 0 if the MIC check was successful and 1 if it fails - :rtype: int + :return: None if the MIC check was successful + :raises gssapi.GSSException: if the MIC check failed """ self._session_id = session_id self._username = username @@ -371,11 +371,7 @@ class _SSH_GSSAPI(_SSH_GSSAuth): self._username, self._service, self._auth_method) - try: - self._gss_srv_ctxt.verify_mic(mic_field, - mic_token) - except gssapi.BadSignature: - raise Exception("GSS-API MIC check failed.") + self._gss_srv_ctxt.verify_mic(mic_field, mic_token) else: # for key exchange with gssapi-keyex # client mode @@ -534,31 +530,26 @@ class _SSH_SSPI(_SSH_GSSAuth): :param str mic_token: The MIC token received from the client :param str session_id: The SSH session ID :param str username: The name of the user who attempts to login - :return: 0 if the MIC check was successful - :rtype: int + :return: None if the MIC check was successful + :raises sspi.error: if the MIC check failed """ self._session_id = session_id self._username = username - mic_status = 1 if username is not None: # server mode mic_field = self._ssh_build_mic(self._session_id, self._username, self._service, self._auth_method) - mic_status = self._gss_srv_ctxt.verify(mic_field, - mic_token) + # Verifies data and its signature. If verification fails, an + # sspi.error will be raised. + self._gss_srv_ctxt.verify(mic_field, mic_token) else: # for key exchange with gssapi-keyex # client mode - mic_status = self._gss_ctxt.verify(self._session_id, - mic_token) - """ - The SSPI method C{verify} has no return value, so if no SSPI error - is returned, set C{mic_status} to 0. - """ - mic_status = 0 - return mic_status + # Verifies data and its signature. If verification fails, an + # sspi.error will be raised. + self._gss_ctxt.verify(self._session_id, mic_token) @property def credentials_delegated(self): diff --git a/paramiko/transport.py b/paramiko/transport.py index 36da3043..18fb103b 100644 --- a/paramiko/transport.py +++ b/paramiko/transport.py @@ -20,13 +20,14 @@ Core protocol implementation """ +from __future__ import print_function import os import socket import sys import threading import time import weakref -from hashlib import md5, sha1 +from hashlib import md5, sha1, sha256, sha512 import paramiko from paramiko import util @@ -47,7 +48,7 @@ from paramiko.common import xffffffff, cMSG_CHANNEL_OPEN, cMSG_IGNORE, \ MAX_WINDOW_SIZE, DEFAULT_WINDOW_SIZE, DEFAULT_MAX_PACKET_SIZE from paramiko.compress import ZlibCompressor, ZlibDecompressor from paramiko.dsskey import DSSKey -from paramiko.kex_gex import KexGex +from paramiko.kex_gex import KexGex, KexGexSHA256 from paramiko.kex_group1 import KexGroup1 from paramiko.kex_group14 import KexGroup14 from paramiko.kex_gss import KexGSSGex, KexGSSGroup1, KexGSSGroup14, NullHostKey @@ -94,27 +95,109 @@ class Transport (threading.Thread, ClosingContextManager): _PROTO_ID = '2.0' _CLIENT_ID = 'paramiko_%s' % paramiko.__version__ - _preferred_ciphers = ('aes128-ctr', 'aes256-ctr', 'aes128-cbc', 'blowfish-cbc', - 'aes256-cbc', '3des-cbc', 'arcfour128', 'arcfour256') - _preferred_macs = ('hmac-sha1', 'hmac-md5', 'hmac-sha1-96', 'hmac-md5-96') - _preferred_keys = ('ssh-rsa', 'ssh-dss', 'ecdsa-sha2-nistp256') - _preferred_kex = ( 'diffie-hellman-group14-sha1', 'diffie-hellman-group-exchange-sha1' , 'diffie-hellman-group1-sha1') + # These tuples of algorithm identifiers are in preference order; do not + # reorder without reason! + _preferred_ciphers = ( + 'aes128-ctr', + 'aes192-ctr', + 'aes256-ctr', + 'aes128-cbc', + 'blowfish-cbc', + 'aes192-cbc', + 'aes256-cbc', + '3des-cbc', + 'arcfour128', + 'arcfour256', + ) + _preferred_macs = ( + 'hmac-sha2-256', + 'hmac-sha2-512', + 'hmac-md5', + 'hmac-sha1-96', + 'hmac-md5-96', + 'hmac-sha1', + ) + _preferred_keys = ( + 'ssh-rsa', + 'ssh-dss', + 'ecdsa-sha2-nistp256', + ) + _preferred_kex = ( + 'diffie-hellman-group1-sha1', + 'diffie-hellman-group14-sha1', + 'diffie-hellman-group-exchange-sha1', + 'diffie-hellman-group-exchange-sha256', + ) _preferred_compression = ('none',) _cipher_info = { - 'aes128-ctr': {'class': AES, 'mode': AES.MODE_CTR, 'block-size': 16, 'key-size': 16}, - 'aes256-ctr': {'class': AES, 'mode': AES.MODE_CTR, 'block-size': 16, 'key-size': 32}, - 'blowfish-cbc': {'class': Blowfish, 'mode': Blowfish.MODE_CBC, 'block-size': 8, 'key-size': 16}, - 'aes128-cbc': {'class': AES, 'mode': AES.MODE_CBC, 'block-size': 16, 'key-size': 16}, - 'aes256-cbc': {'class': AES, 'mode': AES.MODE_CBC, 'block-size': 16, 'key-size': 32}, - '3des-cbc': {'class': DES3, 'mode': DES3.MODE_CBC, 'block-size': 8, 'key-size': 24}, - 'arcfour128': {'class': ARC4, 'mode': None, 'block-size': 8, 'key-size': 16}, - 'arcfour256': {'class': ARC4, 'mode': None, 'block-size': 8, 'key-size': 32}, + 'aes128-ctr': { + 'class': AES, + 'mode': AES.MODE_CTR, + 'block-size': 16, + 'key-size': 16 + }, + 'aes192-ctr': { + 'class': AES, + 'mode': AES.MODE_CTR, + 'block-size': 16, + 'key-size': 24 + }, + 'aes256-ctr': { + 'class': AES, + 'mode': AES.MODE_CTR, + 'block-size': 16, + 'key-size': 32 + }, + 'blowfish-cbc': { + 'class': Blowfish, + 'mode': Blowfish.MODE_CBC, + 'block-size': 8, + 'key-size': 16 + }, + 'aes128-cbc': { + 'class': AES, + 'mode': AES.MODE_CBC, + 'block-size': 16, + 'key-size': 16 + }, + 'aes192-cbc': { + 'class': AES, + 'mode': AES.MODE_CBC, + 'block-size': 16, + 'key-size': 24 + }, + 'aes256-cbc': { + 'class': AES, + 'mode': AES.MODE_CBC, + 'block-size': 16, + 'key-size': 32 + }, + '3des-cbc': { + 'class': DES3, + 'mode': DES3.MODE_CBC, + 'block-size': 8, + 'key-size': 24 + }, + 'arcfour128': { + 'class': ARC4, + 'mode': None, + 'block-size': 8, + 'key-size': 16 + }, + 'arcfour256': { + 'class': ARC4, + 'mode': None, + 'block-size': 8, + 'key-size': 32 + }, } _mac_info = { 'hmac-sha1': {'class': sha1, 'size': 20}, 'hmac-sha1-96': {'class': sha1, 'size': 12}, + 'hmac-sha2-256': {'class': sha256, 'size': 32}, + 'hmac-sha2-512': {'class': sha512, 'size': 64}, 'hmac-md5': {'class': md5, 'size': 16}, 'hmac-md5-96': {'class': md5, 'size': 12}, } @@ -129,6 +212,7 @@ class Transport (threading.Thread, ClosingContextManager): 'diffie-hellman-group1-sha1': KexGroup1, 'diffie-hellman-group14-sha1': KexGroup14, 'diffie-hellman-group-exchange-sha1': KexGex, + 'diffie-hellman-group-exchange-sha256': KexGexSHA256, 'gss-group1-sha1-toWM5Slw5Ew8Mqkay+al2g==': KexGSSGroup1, 'gss-group14-sha1-toWM5Slw5Ew8Mqkay+al2g==': KexGSSGroup14, 'gss-gex-sha1-toWM5Slw5Ew8Mqkay+al2g==': KexGSSGex @@ -295,6 +379,8 @@ class Transport (threading.Thread, ClosingContextManager): self.global_response = None # response Message from an arbitrary global request self.completion_event = None # user-defined event callbacks self.banner_timeout = 15 # how long (seconds) to wait for the SSH banner + self.handshake_timeout = 15 # how long (seconds) to wait for the handshake to finish after SSH banner sent. + # server mode: self.server_mode = False @@ -587,7 +673,7 @@ class Transport (threading.Thread, ClosingContextManager): """ return self.active - def open_session(self, window_size=None, max_packet_size=None): + def open_session(self, window_size=None, max_packet_size=None, timeout=None): """ Request a new channel to the server, of type ``"session"``. This is just an alias for calling `open_channel` with an argument of @@ -612,7 +698,8 @@ class Transport (threading.Thread, ClosingContextManager): """ return self.open_channel('session', window_size=window_size, - max_packet_size=max_packet_size) + max_packet_size=max_packet_size, + timeout=timeout) def open_x11_channel(self, src_addr=None): """ @@ -659,7 +746,8 @@ class Transport (threading.Thread, ClosingContextManager): dest_addr=None, src_addr=None, window_size=None, - max_packet_size=None): + max_packet_size=None, + timeout=None): """ Request a new channel to the server. `Channels <.Channel>` are socket-like objects used for the actual transfer of data across the @@ -683,17 +771,20 @@ class Transport (threading.Thread, ClosingContextManager): optional window size for this session. :param int max_packet_size: optional max packet size for this session. + :param float timeout: + optional timeout opening a channel, default 3600s (1h) :return: a new `.Channel` on success - :raises SSHException: if the request is rejected or the session ends - prematurely + :raises SSHException: if the request is rejected, the session ends + prematurely or there is a timeout openning a channel .. versionchanged:: 1.15 Added the ``window_size`` and ``max_packet_size`` arguments. """ if not self.active: raise SSHException('SSH session not active') + timeout = 3600 if timeout is None else timeout self.lock.acquire() try: window_size = self._sanitize_window_size(window_size) @@ -722,6 +813,7 @@ class Transport (threading.Thread, ClosingContextManager): finally: self.lock.release() self._send_user_message(m) + start_ts = time.time() while True: event.wait(0.1) if not self.active: @@ -731,6 +823,8 @@ class Transport (threading.Thread, ClosingContextManager): raise e if event.is_set(): break + elif start_ts + timeout < time.time(): + raise SSHException('Timeout openning channel.') chan = self._channels.get(chanid) if chan is not None: return chan @@ -1284,6 +1378,27 @@ class Transport (threading.Thread, ClosingContextManager): self.auth_handler.auth_interactive(username, handler, my_event, submethods) return self.auth_handler.wait_for_response(my_event) + def auth_interactive_dumb(self, username, handler=None, submethods=''): + """ + Autenticate to the server interactively but dumber. + Just print the prompt and / or instructions to stdout and send back + the response. This is good for situations where partial auth is + achieved by key and then the user has to enter a 2fac token. + """ + + if not handler: + def handler(title, instructions, prompt_list): + answers = [] + if title: + print(title.strip()) + if instructions: + print(instructions.strip()) + for prompt,show_input in prompt_list: + print(prompt.strip(),end=' ') + answers.append(raw_input()) + return answers + return self.auth_interactive(username, handler, submethods) + def auth_gssapi_with_mic(self, username, gss_host, gss_deleg_creds): """ Authenticate to the Server using GSS-API / SSPI. @@ -1497,13 +1612,23 @@ class Transport (threading.Thread, ClosingContextManager): m.add_bytes(self.H) m.add_byte(b(id)) m.add_bytes(self.session_id) - out = sofar = sha1(m.asbytes()).digest() + # Fallback to SHA1 for kex engines that fail to specify a hex + # algorithm, or for e.g. transport tests that don't run kexinit. + hash_algo = getattr(self.kex_engine, 'hash_algo', None) + hash_select_msg = "kex engine %s specified hash_algo %r" % (self.kex_engine.__class__.__name__, hash_algo) + if hash_algo is None: + hash_algo = sha1 + hash_select_msg += ", falling back to sha1" + if not hasattr(self, '_logged_hash_selection'): + self._log(DEBUG, hash_select_msg) + setattr(self, '_logged_hash_selection', True) + out = sofar = hash_algo(m.asbytes()).digest() while len(out) < nbytes: m = Message() m.add_mpint(self.K) m.add_bytes(self.H) m.add_bytes(sofar) - digest = sha1(m.asbytes()).digest() + digest = hash_algo(m.asbytes()).digest() out += digest sofar += digest return out[:nbytes] @@ -1581,7 +1706,16 @@ class Transport (threading.Thread, ClosingContextManager): try: try: self.packetizer.write_all(b(self.local_version + '\r\n')) + self._log(DEBUG, 'Local version/idstring: %s' % self.local_version) self._check_banner() + # The above is actually very much part of the handshake, but + # sometimes the banner can be read but the machine is not + # responding, for example when the remote ssh daemon is loaded + # in to memory but we can not read from the disk/spawn a new + # shell. + # Make sure we can specify a timeout for the initial handshake. + # Re-use the banner timeout for now. + self.packetizer.start_handshake(self.handshake_timeout) self._send_kex_init() self._expect_packet(MSG_KEXINIT) @@ -1631,6 +1765,7 @@ class Transport (threading.Thread, ClosingContextManager): msg.add_byte(cMSG_UNIMPLEMENTED) msg.add_int(m.seqno) self._send_message(msg) + self.packetizer.complete_handshake() except SSHException as e: self._log(ERROR, 'Exception: ' + str(e)) self._log(ERROR, util.tb_strings()) @@ -1679,6 +1814,18 @@ class Transport (threading.Thread, ClosingContextManager): if self.sys.modules is not None: raise + + def _log_agreement(self, which, local, remote): + # Log useful, non-duplicative line re: an agreed-upon algorithm. + # Old code implied algorithms could be asymmetrical (different for + # inbound vs outbound) so we preserve that possibility. + msg = "{0} agreed: ".format(which) + if local == remote: + msg += local + else: + msg += "local={0}, remote={1}".format(local, remote) + self._log(DEBUG, msg) + ### protocol stages def _negotiate_keys(self, m): @@ -1716,6 +1863,7 @@ class Transport (threading.Thread, ClosingContextManager): raise SSHException('Indecipherable protocol version "' + buf + '"') # save this server version string for later self.remote_version = buf + self._log(DEBUG, 'Remote version/idstring: %s' % buf) # pull off any attached comment comment = '' i = buf.find(' ') @@ -1744,10 +1892,12 @@ class Transport (threading.Thread, ClosingContextManager): self.clear_to_send_lock.release() self.in_kex = True if self.server_mode: - if (self._modulus_pack is None) and ('diffie-hellman-group-exchange-sha1' in self._preferred_kex): + mp_required_prefix = 'diffie-hellman-group-exchange-sha' + kex_mp = [k for k in self._preferred_kex if k.startswith(mp_required_prefix)] + if (self._modulus_pack is None) and (len(kex_mp) > 0): # can't do group-exchange if we don't have a pack of potential primes - pkex = list(self.get_security_options().kex) - pkex.remove('diffie-hellman-group-exchange-sha1') + pkex = [k for k in self.get_security_options().kex + if not k.startswith(mp_required_prefix)] self.get_security_options().kex = pkex available_server_keys = list(filter(list(self.server_key_dict.keys()).__contains__, self._preferred_keys)) @@ -1799,15 +1949,24 @@ class Transport (threading.Thread, ClosingContextManager): ' server lang:' + str(server_lang_list) + ' kex follows?' + str(kex_follows)) - # as a server, we pick the first item in the client's list that we support. - # as a client, we pick the first item in our list that the server supports. + # as a server, we pick the first item in the client's list that we + # support. + # as a client, we pick the first item in our list that the server + # supports. if self.server_mode: - agreed_kex = list(filter(self._preferred_kex.__contains__, kex_algo_list)) + agreed_kex = list(filter( + self._preferred_kex.__contains__, + kex_algo_list + )) else: - agreed_kex = list(filter(kex_algo_list.__contains__, self._preferred_kex)) + agreed_kex = list(filter( + kex_algo_list.__contains__, + self._preferred_kex + )) if len(agreed_kex) == 0: raise SSHException('Incompatible ssh peer (no acceptable kex algorithm)') self.kex_engine = self._kex_info[agreed_kex[0]](self) + self._log(DEBUG, "Kex agreed: %s" % agreed_kex[0]) if self.server_mode: available_server_keys = list(filter(list(self.server_key_dict.keys()).__contains__, @@ -1835,7 +1994,9 @@ class Transport (threading.Thread, ClosingContextManager): raise SSHException('Incompatible ssh server (no acceptable ciphers)') self.local_cipher = agreed_local_ciphers[0] self.remote_cipher = agreed_remote_ciphers[0] - self._log(DEBUG, 'Ciphers agreed: local=%s, remote=%s' % (self.local_cipher, self.remote_cipher)) + self._log_agreement( + 'Cipher', local=self.local_cipher, remote=self.remote_cipher + ) if self.server_mode: agreed_remote_macs = list(filter(self._preferred_macs.__contains__, client_mac_algo_list)) @@ -1847,6 +2008,9 @@ class Transport (threading.Thread, ClosingContextManager): raise SSHException('Incompatible ssh server (no acceptable macs)') self.local_mac = agreed_local_macs[0] self.remote_mac = agreed_remote_macs[0] + self._log_agreement( + 'MAC', local=self.local_mac, remote=self.remote_mac + ) if self.server_mode: agreed_remote_compression = list(filter(self._preferred_compression.__contains__, client_compress_algo_list)) @@ -1858,10 +2022,11 @@ class Transport (threading.Thread, ClosingContextManager): raise SSHException('Incompatible ssh server (no acceptable compression) %r %r %r' % (agreed_local_compression, agreed_remote_compression, self._preferred_compression)) self.local_compression = agreed_local_compression[0] self.remote_compression = agreed_remote_compression[0] - - self._log(DEBUG, 'using kex %s; server key type %s; cipher: local %s, remote %s; mac: local %s, remote %s; compression: local %s, remote %s' % - (agreed_kex[0], self.host_key_type, self.local_cipher, self.remote_cipher, self.local_mac, - self.remote_mac, self.local_compression, self.remote_compression)) + self._log_agreement( + 'Compression', + local=self.local_compression, + remote=self.remote_compression + ) # save for computing hash later... # now wait! openssh has a bug (and others might too) where there are @@ -1882,8 +2047,8 @@ class Transport (threading.Thread, ClosingContextManager): engine = self._get_cipher(self.remote_cipher, key_in, IV_in) mac_size = self._mac_info[self.remote_mac]['size'] mac_engine = self._mac_info[self.remote_mac]['class'] - # initial mac keys are done in the hash's natural size (not the potentially truncated - # transmission size) + # initial mac keys are done in the hash's natural size (not the + # potentially truncated transmission size) if self.server_mode: mac_key = self._compute_key('E', mac_engine().digest_size) else: @@ -1909,8 +2074,8 @@ class Transport (threading.Thread, ClosingContextManager): engine = self._get_cipher(self.local_cipher, key_out, IV_out) mac_size = self._mac_info[self.local_mac]['size'] mac_engine = self._mac_info[self.local_mac]['class'] - # initial mac keys are done in the hash's natural size (not the potentially truncated - # transmission size) + # initial mac keys are done in the hash's natural size (not the + # potentially truncated transmission size) if self.server_mode: mac_key = self._compute_key('F', mac_engine().digest_size) else: diff --git a/paramiko/util.py b/paramiko/util.py index d9a29d74..855e5757 100644 --- a/paramiko/util.py +++ b/paramiko/util.py @@ -118,7 +118,7 @@ def safe_string(s): def bit_length(n): try: - return n.bitlength() + return n.bit_length() except AttributeError: norm = deflate_long(n, False) hbyte = byte_ord(norm[0]) |