From 27b800bf3f1c0ee7063221538d2913cec7c048c1 Mon Sep 17 00:00:00 2001 From: Torsten Landschoff Date: Fri, 12 Aug 2011 11:25:36 +0200 Subject: Issue #22: Try IPv4 as well as IPv6 when port is not open on IPv6. With this change, paramiko tries the next address family when the connection gets refused or the target port is unreachable. --- paramiko/client.py | 53 +++++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 39 insertions(+), 14 deletions(-) diff --git a/paramiko/client.py b/paramiko/client.py index c5a2d1ac..da08ad0a 100644 --- a/paramiko/client.py +++ b/paramiko/client.py @@ -25,6 +25,7 @@ import getpass import os import socket import warnings +from errno import ECONNREFUSED, EHOSTUNREACH from paramiko.agent import Agent from paramiko.common import * @@ -231,6 +232,29 @@ class SSHClient (object): """ self._policy = policy + def _families_and_addresses(self, hostname, port): + """ + Yield pairs of address families and addresses to try for connecting. + + @param hostname: the server to connect to + @type hostname: str + @param port: the server port to connect to + @type port: int + @rtype: generator + """ + guess = True + addrinfos = socket.getaddrinfo(hostname, port, socket.AF_UNSPEC, socket.SOCK_STREAM) + for (family, socktype, proto, canonname, sockaddr) in addrinfos: + if socktype == socket.SOCK_STREAM: + yield family, sockaddr + guess = False + + # some OS like AIX don't indicate SOCK_STREAM support, so just guess. :( + # We only do this if we did not get a single result marked as socktype == SOCK_STREAM. + if guess: + for family, _, _, _, sockaddr in addrinfos: + yield family, sockaddr + def connect(self, hostname, port=SSH_PORT, username=None, password=None, pkey=None, key_filename=None, timeout=None, allow_agent=True, look_for_keys=True, compress=False, sock=None): @@ -288,21 +312,22 @@ class SSHClient (object): @raise socket.error: if a socket error occurred while connecting """ if not sock: - for (family, socktype, proto, canonname, sockaddr) in socket.getaddrinfo(hostname, port, socket.AF_UNSPEC, socket.SOCK_STREAM): - if socktype == socket.SOCK_STREAM: - af = family - addr = sockaddr - break - else: - # some OS like AIX don't indicate SOCK_STREAM support, so just guess. :( - af, _, _, _, addr = socket.getaddrinfo(hostname, port, socket.AF_UNSPEC, socket.SOCK_STREAM) - sock = socket.socket(af, socket.SOCK_STREAM) - if timeout is not None: + for af, addr in self._families_and_addresses(hostname, port): try: - sock.settimeout(timeout) - except: - pass - retry_on_signal(lambda: sock.connect(addr)) + sock = socket.socket(af, socket.SOCK_STREAM) + if timeout is not None: + try: + sock.settimeout(timeout) + except: + pass + retry_on_signal(lambda: sock.connect(addr)) + # Break out of the loop on success + break + except socket.error, e: + # If the port is not open on IPv6 for example, we may still try IPv4. + # Likewise if the host is not reachable using that address family. + if e.errno not in (ECONNREFUSED, EHOSTUNREACH): + raise t = self._transport = Transport(sock) t.use_compression(compress=compress) -- cgit v1.2.3 From b4e9dc3ab96e699a38a71aa6bb46337a596312d5 Mon Sep 17 00:00:00 2001 From: John Morrissey Date: Fri, 12 Dec 2014 13:23:25 -0500 Subject: read in >1 byte chunks, and set a useful timeout on select() this way, we're not rolling this loop over nearly as much, while still preserving the overall timeout semantics --- paramiko/proxy.py | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/paramiko/proxy.py b/paramiko/proxy.py index 8959b244..25666be5 100644 --- a/paramiko/proxy.py +++ b/paramiko/proxy.py @@ -24,6 +24,7 @@ import signal from subprocess import Popen, PIPE from select import select import socket +import time from paramiko.ssh_exception import ProxyCommandFailure @@ -76,20 +77,24 @@ class ProxyCommand(object): :return: the length of the read content, as an `int` """ try: - start = datetime.now() + start = time.time() while len(self.buffer) < size: + select_timeout = None if self.timeout is not None: - elapsed = (datetime.now() - start).microseconds - timeout = self.timeout * 1000 * 1000 # to microseconds - if elapsed >= timeout: + elapsed = (time.time() - start) + if elapsed >= self.timeout: raise socket.timeout() - r, w, x = select([self.process.stdout], [], [], 0.0) + select_timeout = self.timeout - elapsed + + r, w, x = select( + [self.process.stdout], [], [], select_timeout) if r and r[0] == self.process.stdout: - b = os.read(self.process.stdout.fileno(), 1) + b = os.read( + self.process.stdout.fileno(), size - len(self.buffer)) # Store in class-level buffer for persistence across # timeouts; this makes us act more like a real socket # (where timeouts don't actually drop data.) - self.buffer.append(b) + self.buffer.extend(b) result = ''.join(self.buffer) self.buffer = [] return result -- cgit v1.2.3 From 741b4fbb9322a45282e86958c92b1b6706c07f8c Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Thu, 18 Dec 2014 13:53:24 -0800 Subject: Changelog re #413, closes #454 --- sites/www/changelog.rst | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/sites/www/changelog.rst b/sites/www/changelog.rst index 99c28fbd..66bbb806 100644 --- a/sites/www/changelog.rst +++ b/sites/www/changelog.rst @@ -2,6 +2,11 @@ Changelog ========= +* :bug:`413` (also :issue:`414`, :issue:`420`, :issue:`454`) Be significantly + smarter about polling & timing behavior when running proxy commands, to avoid + unnecessary (often 100%!) CPU usage. Major thanks to Jason Dunsmore for + report & initial patchset and to Chris Adams & John Morrissey for followup + improvements. * :bug:`428` Fix an issue in `~paramiko.file.BufferedFile` (primarily used in the SFTP modules) concerning incorrect behavior by `~paramiko.file.BufferedFile.readlines` on files whose size exceeds the -- cgit v1.2.3 From ec5a86619c5527f119a687ec00e8811657dd1f51 Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Thu, 18 Dec 2014 13:57:36 -0800 Subject: Fix busted changelog indent --- sites/www/changelog.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sites/www/changelog.rst b/sites/www/changelog.rst index 66bbb806..8a705382 100644 --- a/sites/www/changelog.rst +++ b/sites/www/changelog.rst @@ -24,7 +24,7 @@ Changelog packets encountered type errors. This is now fixed. Thanks to ``@mjmaenpaa`` for the catch. * :bug:`320` Update our win_pageant module to be Python 3 compatible. Thanks to -``@sherbang`` and ``@adamkerz`` for the patches. + ``@sherbang`` and ``@adamkerz`` for the patches. * :support:`378 backported` Minor code cleanup in the SSH config module courtesy of Olle Lundberg. * :support:`249` Consolidate version information into one spot. Thanks to Gabi -- cgit v1.2.3 From 3905dfb0a8f719b435125d404ca4403f9849e17c Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Thu, 18 Dec 2014 14:00:30 -0800 Subject: Mark some backported support items as such --- sites/www/changelog.rst | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/sites/www/changelog.rst b/sites/www/changelog.rst index 8a705382..9cc3a3eb 100644 --- a/sites/www/changelog.rst +++ b/sites/www/changelog.rst @@ -11,7 +11,8 @@ Changelog the SFTP modules) concerning incorrect behavior by `~paramiko.file.BufferedFile.readlines` on files whose size exceeds the buffer size. Thanks to ``@achapp`` for catch & patch. -* :support:`422` Clean up some unused imports. Courtesy of Olle Lundberg. +* :support:`422 backported` Clean up some unused imports. Courtesy of Olle + Lundberg. * :bug:`266` Change numbering of `~paramiko.transport.Transport` channels to start at 0 instead of 1 for better compatibility with OpenSSH & certain server implementations which break on 1-indexed channels. Thanks to @@ -27,8 +28,8 @@ Changelog ``@sherbang`` and ``@adamkerz`` for the patches. * :support:`378 backported` Minor code cleanup in the SSH config module courtesy of Olle Lundberg. -* :support:`249` Consolidate version information into one spot. Thanks to Gabi - Davar for the reminder. +* :support:`249 backported` Consolidate version information into one spot. + Thanks to Gabi Davar for the reminder. * :release:`1.13.2 <2014-08-25>` * :bug:`376` Be less aggressive about expanding variables in ``ssh_config`` files, which results in a speedup of SSH config parsing. Credit to Olle -- cgit v1.2.3 From 5601bf0928e2e738917320d83f8302703a62091b Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Thu, 18 Dec 2014 14:02:28 -0800 Subject: Mark more backported support issues as such --- sites/www/changelog.rst | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/sites/www/changelog.rst b/sites/www/changelog.rst index f6f2bb28..e5adbd22 100644 --- a/sites/www/changelog.rst +++ b/sites/www/changelog.rst @@ -18,15 +18,15 @@ Changelog none`` as the lack of a proxy command, instead of as a literal command string of ``"none"``. Thanks to Richard Spiers for the catch & Sean Johnson for the fix. -* :support:`431` Replace handrolled ``ssh_config`` parsing code with use of the - ``shlex`` module. Thanks to Yan Kalchevskiy. +* :support:`431 backported` Replace handrolled ``ssh_config`` parsing code with + use of the ``shlex`` module. Thanks to Yan Kalchevskiy. * :support:`422 backported` Clean up some unused imports. Courtesy of Olle Lundberg. -* :support:`421` Modernize threading calls to user newer API. Thanks to Olle - Lundberg. -* :support:`419` Modernize a bunch of the codebase internals to leverage - decorators. Props to ``@beckjake`` for realizing we're no longer on Python - 2.2 :D +* :support:`421 backported` Modernize threading calls to user newer API. Thanks + to Olle Lundberg. +* :support:`419 backported` Modernize a bunch of the codebase internals to + leverage decorators. Props to ``@beckjake`` for realizing we're no longer on + Python 2.2 :D * :bug:`266` Change numbering of `~paramiko.transport.Transport` channels to start at 0 instead of 1 for better compatibility with OpenSSH & certain server implementations which break on 1-indexed channels. Thanks to -- cgit v1.2.3 From 9451f2aada77850c4ba5719e8f732989c9b4f663 Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Fri, 19 Dec 2014 14:54:15 -0800 Subject: Cut 1.13.3 --- sites/www/changelog.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/sites/www/changelog.rst b/sites/www/changelog.rst index 9cc3a3eb..9ce2eded 100644 --- a/sites/www/changelog.rst +++ b/sites/www/changelog.rst @@ -2,6 +2,7 @@ Changelog ========= +* :release:`1.13.3 <2014-12-19>` * :bug:`413` (also :issue:`414`, :issue:`420`, :issue:`454`) Be significantly smarter about polling & timing behavior when running proxy commands, to avoid unnecessary (often 100%!) CPU usage. Major thanks to Jason Dunsmore for -- cgit v1.2.3 From ccdfd02c047d5588b6bebdc501a766271a009493 Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Fri, 19 Dec 2014 14:55:15 -0800 Subject: Cut 1.14.2 --- sites/www/changelog.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/sites/www/changelog.rst b/sites/www/changelog.rst index 8ad82a71..695149de 100644 --- a/sites/www/changelog.rst +++ b/sites/www/changelog.rst @@ -2,6 +2,7 @@ Changelog ========= +* :release:`1.14.2 <2014-12-19>` * :release:`1.13.3 <2014-12-19>` * :bug:`413` (also :issue:`414`, :issue:`420`, :issue:`454`) Be significantly smarter about polling & timing behavior when running proxy commands, to avoid -- cgit v1.2.3 From 424ba615c2a94d3b059e7f24db1a1093a92d8d22 Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Fri, 19 Dec 2014 14:55:48 -0800 Subject: Cut 1.15.2 --- paramiko/_version.py | 2 +- sites/www/changelog.rst | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/paramiko/_version.py b/paramiko/_version.py index d9f78740..3bf9dac7 100644 --- a/paramiko/_version.py +++ b/paramiko/_version.py @@ -1,2 +1,2 @@ -__version_info__ = (1, 15, 1) +__version_info__ = (1, 15, 2) __version__ = '.'.join(map(str, __version_info__)) diff --git a/sites/www/changelog.rst b/sites/www/changelog.rst index f5348e5b..bb93f885 100644 --- a/sites/www/changelog.rst +++ b/sites/www/changelog.rst @@ -2,6 +2,7 @@ Changelog ========= +* :release:`1.15.2 <2014-12-19>` * :release:`1.14.2 <2014-12-19>` * :release:`1.13.3 <2014-12-19>` * :bug:`413` (also :issue:`414`, :issue:`420`, :issue:`454`) Be significantly -- cgit v1.2.3 From 67ae41a03ef2c94443e809b65f5eb2c4e7cf8937 Mon Sep 17 00:00:00 2001 From: ThiefMaster Date: Thu, 1 Jan 2015 13:50:52 +0100 Subject: Fix typo --- paramiko/_winapi.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/paramiko/_winapi.py b/paramiko/_winapi.py index f48e1890..d6aabf76 100644 --- a/paramiko/_winapi.py +++ b/paramiko/_winapi.py @@ -106,7 +106,7 @@ MapViewOfFile.restype = ctypes.wintypes.HANDLE class MemoryMap(object): """ - A memory map object which can have security attributes overrideden. + A memory map object which can have security attributes overridden. """ def __init__(self, name, length, security_attributes=None): self.name = name -- cgit v1.2.3 From fac347718a69179ba9404d5f5e825798f4ee9379 Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Fri, 2 Jan 2015 14:24:31 -0800 Subject: Happy new year --- README | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README b/README index 61c5c852..a645b140 100644 --- a/README +++ b/README @@ -5,7 +5,7 @@ paramiko :Paramiko: Python SSH module :Copyright: Copyright (c) 2003-2009 Robey Pointer -:Copyright: Copyright (c) 2013-2014 Jeff Forcier +:Copyright: Copyright (c) 2013-2015 Jeff Forcier :License: LGPL :Homepage: https://github.com/paramiko/paramiko/ :API docs: http://docs.paramiko.org -- cgit v1.2.3 From 3700d6e151d7891a12b958cf8f2729576205d927 Mon Sep 17 00:00:00 2001 From: Ken Jordan Date: Thu, 22 Jan 2015 19:39:22 -0700 Subject: Updated agent.py to print a more appropriate exception when unable to connect to the SSH agent --- paramiko/agent.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/paramiko/agent.py b/paramiko/agent.py index a75ac59e..185666e7 100644 --- a/paramiko/agent.py +++ b/paramiko/agent.py @@ -32,7 +32,7 @@ from select import select from paramiko.common import asbytes, io_sleep from paramiko.py3compat import byte_chr -from paramiko.ssh_exception import SSHException +from paramiko.ssh_exception import SSHException, AuthenticationException from paramiko.message import Message from paramiko.pkey import PKey from paramiko.util import retry_on_signal @@ -109,9 +109,15 @@ class AgentProxyThread(threading.Thread): def run(self): try: (r, addr) = self.get_connection() + # Found that r should be either a socket from the socket library or None self.__inr = r - self.__addr = addr + self.__addr = addr # This should be an IP address as a string? or None self._agent.connect() + print("Conn Value: ", self._agent._conn) + print("Has fileno method: ", hasattr(self._agent._conn, 'fileno')) + print("Verify isinstance of int: ", isinstance(self._agent, int)) + if self._agent._conn is None or not (hasattr(self._agent._conn, 'fileno') or isinstance(self._agent, int)): + raise AuthenticationException("Unable to connect to SSH agent") self._communicate() except: #XXX Not sure what to do here ... raise or pass ? -- cgit v1.2.3 From b26dbc3f719e9844721d38d6264c5cb5e0ed4c2f Mon Sep 17 00:00:00 2001 From: Ken Jordan Date: Thu, 22 Jan 2015 19:41:15 -0700 Subject: Removed debug print statements --- paramiko/agent.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/paramiko/agent.py b/paramiko/agent.py index 185666e7..01bd0252 100644 --- a/paramiko/agent.py +++ b/paramiko/agent.py @@ -113,9 +113,6 @@ class AgentProxyThread(threading.Thread): self.__inr = r self.__addr = addr # This should be an IP address as a string? or None self._agent.connect() - print("Conn Value: ", self._agent._conn) - print("Has fileno method: ", hasattr(self._agent._conn, 'fileno')) - print("Verify isinstance of int: ", isinstance(self._agent, int)) if self._agent._conn is None or not (hasattr(self._agent._conn, 'fileno') or isinstance(self._agent, int)): raise AuthenticationException("Unable to connect to SSH agent") self._communicate() -- cgit v1.2.3 From 394e269a081e6cf07d38fa5a375f0a03374c0d2b Mon Sep 17 00:00:00 2001 From: jordo1ken Date: Fri, 30 Jan 2015 17:57:40 -0700 Subject: Update agent.py Updated logic for error checking. --- paramiko/agent.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/paramiko/agent.py b/paramiko/agent.py index 01bd0252..f928881e 100644 --- a/paramiko/agent.py +++ b/paramiko/agent.py @@ -113,7 +113,7 @@ class AgentProxyThread(threading.Thread): self.__inr = r self.__addr = addr # This should be an IP address as a string? or None self._agent.connect() - if self._agent._conn is None or not (hasattr(self._agent._conn, 'fileno') or isinstance(self._agent, int)): + if not isinstance(self._agent, int) and (self._agent._conn is None or not hasattr(self._agent._conn, 'fileno')): raise AuthenticationException("Unable to connect to SSH agent") self._communicate() except: -- cgit v1.2.3 From c5d0d6a2919ca2158b3f6271f7449faeeb3c865f Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Wed, 4 Feb 2015 16:00:50 -0800 Subject: Changelog fixes #402, closes #479 --- sites/www/changelog.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/sites/www/changelog.rst b/sites/www/changelog.rst index bb93f885..6520dde4 100644 --- a/sites/www/changelog.rst +++ b/sites/www/changelog.rst @@ -2,6 +2,10 @@ Changelog ========= +* :bug:`402` Check to see if an SSH agent is actually present before trying to + forward it to the remote end. This replaces what was usually a useless + ``TypeError`` with a human-readable ``AuthenticationError``. Credit to Ken + Jordan for the fix and Yvan Marques for original report. * :release:`1.15.2 <2014-12-19>` * :release:`1.14.2 <2014-12-19>` * :release:`1.13.3 <2014-12-19>` -- cgit v1.2.3 From d97c938db32c44a8253f6f872cc3b354492a21cc Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Fri, 6 Feb 2015 15:33:28 -0800 Subject: Fix docstring for Sphinx --- paramiko/client.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/paramiko/client.py b/paramiko/client.py index 312f71a9..dbe7fcba 100644 --- a/paramiko/client.py +++ b/paramiko/client.py @@ -177,11 +177,9 @@ class SSHClient (ClosingContextManager): """ Yield pairs of address families and addresses to try for connecting. - @param hostname: the server to connect to - @type hostname: str - @param port: the server port to connect to - @type port: int - @rtype: generator + :param str hostname: the server to connect to + :param int port: the server port to connect to + :returns: Yields an iterable of ``(family, address)`` tuples """ guess = True addrinfos = socket.getaddrinfo(hostname, port, socket.AF_UNSPEC, socket.SOCK_STREAM) -- cgit v1.2.3 From b42b5338de868c3a5343dd03a8ee3f8a968d2f65 Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Fri, 6 Feb 2015 15:33:32 -0800 Subject: Comment --- paramiko/client.py | 1 + 1 file changed, 1 insertion(+) diff --git a/paramiko/client.py b/paramiko/client.py index dbe7fcba..b907d3b0 100644 --- a/paramiko/client.py +++ b/paramiko/client.py @@ -256,6 +256,7 @@ class SSHClient (ClosingContextManager): ``gss_deleg_creds`` and ``gss_host`` arguments. """ if not sock: + # Try multiple possible address families (e.g. IPv4 vs IPv6) for af, addr in self._families_and_addresses(hostname, port): try: sock = socket.socket(af, socket.SOCK_STREAM) -- cgit v1.2.3 From c2e346372c06c5eb4982055c689e1eb89955bddf Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Fri, 6 Feb 2015 16:20:51 -0800 Subject: Bump dev version. (might end up 2.0 tho) --- paramiko/_version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/paramiko/_version.py b/paramiko/_version.py index 3bf9dac7..e82b8667 100644 --- a/paramiko/_version.py +++ b/paramiko/_version.py @@ -1,2 +1,2 @@ -__version_info__ = (1, 15, 2) +__version_info__ = (1, 16, 0) __version__ = '.'.join(map(str, __version_info__)) -- cgit v1.2.3 From f99e1d8775be20c471c39f8d7a91126b8419381d Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Fri, 6 Feb 2015 19:33:06 -0800 Subject: Raise usefully ambiguous error when every connect attempt fails. Re #22 --- paramiko/client.py | 24 ++++++++++++++++++++---- paramiko/ssh_exception.py | 30 ++++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+), 4 deletions(-) diff --git a/paramiko/client.py b/paramiko/client.py index b907d3b0..57e9919d 100644 --- a/paramiko/client.py +++ b/paramiko/client.py @@ -36,7 +36,9 @@ from paramiko.hostkeys import HostKeys from paramiko.py3compat import string_types from paramiko.resource import ResourceManager from paramiko.rsakey import RSAKey -from paramiko.ssh_exception import SSHException, BadHostKeyException +from paramiko.ssh_exception import ( + SSHException, BadHostKeyException, ConnectionError +) from paramiko.transport import Transport from paramiko.util import retry_on_signal, ClosingContextManager @@ -256,8 +258,10 @@ class SSHClient (ClosingContextManager): ``gss_deleg_creds`` and ``gss_host`` arguments. """ if not sock: + errors = {} # Try multiple possible address families (e.g. IPv4 vs IPv6) - for af, addr in self._families_and_addresses(hostname, port): + to_try = list(self._families_and_addresses(hostname, port)) + for af, addr in to_try: try: sock = socket.socket(af, socket.SOCK_STREAM) if timeout is not None: @@ -269,10 +273,22 @@ class SSHClient (ClosingContextManager): # Break out of the loop on success break except socket.error, e: - # If the port is not open on IPv6 for example, we may still try IPv4. - # Likewise if the host is not reachable using that address family. + # Raise anything that isn't a straight up connection error + # (such as a resolution error) if e.errno not in (ECONNREFUSED, EHOSTUNREACH): raise + # Capture anything else so we know how the run looks once + # iteration is complete. Retain info about which attempt + # this was. + errors[addr] = e + + # Make sure we explode usefully if no address family attempts + # succeeded. We've no way of knowing which error is the "right" + # one, so we construct a hybrid exception containing all the real + # ones, of a subclass that client code should still be watching for + # (socket.error) + if len(errors) == len(to_try): + raise ConnectionError(errors) t = self._transport = Transport(sock, gss_kex=gss_kex, gss_deleg_creds=gss_deleg_creds) t.use_compression(compress=compress) diff --git a/paramiko/ssh_exception.py b/paramiko/ssh_exception.py index b99e42b3..7e6f2568 100644 --- a/paramiko/ssh_exception.py +++ b/paramiko/ssh_exception.py @@ -16,6 +16,8 @@ # along with Paramiko; if not, write to the Free Software Foundation, Inc., # 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA. +import socket + class SSHException (Exception): """ @@ -129,3 +131,31 @@ class ProxyCommandFailure (SSHException): self.error = error # for unpickling self.args = (command, error, ) + + +class ConnectionError(socket.error): + """ + High-level socket error wrapping 1+ actual socket.error objects. + + To see the wrapped exception objects, access the ``errors`` attribute. + ``errors`` is a dict whose keys are address tuples (e.g. ``('127.0.0.1', + 22)``) and whose values are the exception encountered trying to connect to + that address. + + 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). + """ + def __init__(self, errors): + """ + :param dict errors: + The errors dict to store, as described by class docstring. + """ + addrs = errors.keys() + body = ', '.join([x[0] for x in addrs[:-1]]) + tail = addrs[-1][0] + msg = "Unable to connect to port {0} at {1} or {2}" + super(ConnectionError, self).__init__( + msg.format(addrs[0][1], body, tail) + ) + self.errors = errors -- cgit v1.2.3 From 4ca8d68c0443c4e5e17ae4fcee39dd6f2507c7cd Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Fri, 27 Feb 2015 13:19:35 -0800 Subject: Changelog closes #22 --- sites/www/changelog.rst | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/sites/www/changelog.rst b/sites/www/changelog.rst index 6520dde4..0e8f92c4 100644 --- a/sites/www/changelog.rst +++ b/sites/www/changelog.rst @@ -2,6 +2,11 @@ Changelog ========= +* :bug:`22 major` Try harder to connect to multiple network families (e.g. IPv4 + vs IPv6) in case of connection issues; this helps with problems such as hosts + which resolve both IPv4 and IPv6 addresses but are only listening on IPv4. + Thanks to Dries Desmet for original report and Torsten Landschoff for the + foundational patchset. * :bug:`402` Check to see if an SSH agent is actually present before trying to forward it to the remote end. This replaces what was usually a useless ``TypeError`` with a human-readable ``AuthenticationError``. Credit to Ken -- cgit v1.2.3 From 27e6177f4b7bae87c9fc7aa3556d20100ff9a319 Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Fri, 27 Feb 2015 13:29:32 -0800 Subject: Fix a Python 3 incompat bit from recent merge --- paramiko/client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/paramiko/client.py b/paramiko/client.py index 57e9919d..2e1a4dc4 100644 --- a/paramiko/client.py +++ b/paramiko/client.py @@ -272,7 +272,7 @@ class SSHClient (ClosingContextManager): retry_on_signal(lambda: sock.connect(addr)) # Break out of the loop on success break - except socket.error, e: + except socket.error as e: # Raise anything that isn't a straight up connection error # (such as a resolution error) if e.errno not in (ECONNREFUSED, EHOSTUNREACH): -- cgit v1.2.3 From 3ceab6a5f65db5be219b3dba17677e3101a0489f Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Fri, 27 Feb 2015 14:52:26 -0800 Subject: Some 80-col fixes --- paramiko/client.py | 35 +++++++++++++++++++++++++++-------- 1 file changed, 27 insertions(+), 8 deletions(-) diff --git a/paramiko/client.py b/paramiko/client.py index 2e1a4dc4..6c48b269 100644 --- a/paramiko/client.py +++ b/paramiko/client.py @@ -196,10 +196,25 @@ class SSHClient (ClosingContextManager): for family, _, _, _, sockaddr in addrinfos: yield family, sockaddr - def connect(self, hostname, port=SSH_PORT, username=None, password=None, pkey=None, - key_filename=None, timeout=None, allow_agent=True, look_for_keys=True, - compress=False, sock=None, gss_auth=False, gss_kex=False, - gss_deleg_creds=True, gss_host=None, banner_timeout=None): + def connect( + self, + hostname, + port=SSH_PORT, + username=None, + password=None, + pkey=None, + key_filename=None, + timeout=None, + allow_agent=True, + look_for_keys=True, + compress=False, + sock=None, + gss_auth=False, + gss_kex=False, + gss_deleg_creds=True, + gss_host=None, + banner_timeout=None + ): """ Connect to an SSH server and authenticate to it. The server's host key is checked against the system host keys (see `load_system_host_keys`) @@ -230,8 +245,10 @@ class SSHClient (ClosingContextManager): :param str key_filename: the filename, or list of filenames, of optional private key(s) to try for authentication - :param float timeout: an optional timeout (in seconds) for the TCP connect - :param bool allow_agent: set to False to disable connecting to the SSH agent + :param float timeout: + an optional timeout (in seconds) for the TCP connect + :param bool allow_agent: + set to False to disable connecting to the SSH agent :param bool look_for_keys: set to False to disable searching for discoverable private key files in ``~/.ssh/`` @@ -240,9 +257,11 @@ class SSHClient (ClosingContextManager): 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_kex: Perform GSS-API Key Exchange and user 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 - :param str gss_host: The targets name in the kerberos database. default: hostname + :param str gss_host: + The targets name in the kerberos database. default: hostname :param float banner_timeout: an optional timeout (in seconds) to wait for the SSH banner to be presented. -- cgit v1.2.3 From ca0fd1024ecf61b1758bdd38350fbd4c4ccaaefb Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Sat, 28 Feb 2015 19:54:52 -0800 Subject: Replace/add RFC links using ``:rfc:``, /ht @sigmavirus24 --- paramiko/channel.py | 2 +- paramiko/kex_gss.py | 27 +++++++++++++++------------ paramiko/ssh_gss.py | 2 +- sites/www/index.rst | 8 ++------ 4 files changed, 19 insertions(+), 20 deletions(-) diff --git a/paramiko/channel.py b/paramiko/channel.py index 8a97c974..7e39a15b 100644 --- a/paramiko/channel.py +++ b/paramiko/channel.py @@ -337,7 +337,7 @@ class Channel (ClosingContextManager): further x11 requests can be made from the server to the client, when an x11 application is run in a shell session. - From RFC4254:: + From :rfc:`4254`:: It is RECOMMENDED that the 'x11 authentication cookie' that is sent be a fake, random cookie, and that the cookie be checked and diff --git a/paramiko/kex_gss.py b/paramiko/kex_gss.py index 4e8380ef..d026807c 100644 --- a/paramiko/kex_gss.py +++ b/paramiko/kex_gss.py @@ -21,14 +21,15 @@ """ -This module provides GSS-API / SSPI Key Exchange as defined in RFC 4462. +This module provides GSS-API / SSPI Key Exchange as defined in :rfc:`4462`. .. note:: Credential delegation is not supported in server mode. .. note:: - `RFC 4462 Section 2.2 `_ says we are - not required to implement GSS-API error messages. Thus, in many methods - within this module, if an error occurs an exception will be thrown and the + `RFC 4462 Section 2.2 + `_ says we are not + required to implement GSS-API error messages. Thus, in many methods within + this module, if an error occurs an exception will be thrown and the connection will be terminated. .. seealso:: :doc:`/api/ssh_gss` @@ -55,8 +56,8 @@ c_MSG_KEXGSS_GROUPREQ, c_MSG_KEXGSS_GROUP = [byte_chr(c) for c in range(40, 42)] class KexGSSGroup1(object): """ - GSS-API / SSPI Authenticated Diffie-Hellman Key Exchange - as defined in `RFC 4462 Section 2 `_ + GSS-API / SSPI Authenticated Diffie-Hellman Key Exchange as defined in `RFC + 4462 Section 2 `_ """ # draft-ietf-secsh-transport-09.txt, page 17 P = 0xFFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD129024E088A67CC74020BBEA63B139B22514A08798E3404DDEF9519B3CD3A431B302B0A6DF25F14374FE1356D6D51C245E485B576625E7EC6F44C42E9A637ED6B0BFF5CB6F406B7EDEE386BFB5A899FA5AE9F24117C4B1FE649286651ECE65381FFFFFFFFFFFFFFFF @@ -278,8 +279,9 @@ class KexGSSGroup1(object): class KexGSSGroup14(KexGSSGroup1): """ - GSS-API / SSPI Authenticated Diffie-Hellman Group14 Key Exchange - as defined in `RFC 4462 Section 2 `_ + GSS-API / SSPI Authenticated Diffie-Hellman Group14 Key Exchange as defined + in `RFC 4462 Section 2 + `_ """ P = 0xFFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD129024E088A67CC74020BBEA63B139B22514A08798E3404DDEF9519B3CD3A431B302B0A6DF25F14374FE1356D6D51C245E485B576625E7EC6F44C42E9A637ED6B0BFF5CB6F406B7EDEE386BFB5A899FA5AE9F24117C4B1FE649286651ECE45B3DC2007CB8A163BF0598DA48361C55D39A69163FA8FD24CF5F83655D23DCA3AD961C62F356208552BB9ED529077096966D670C354E4ABC9804F1746C08CA18217C32905E462E36CE3BE39E772C180E86039B2783A2EC07A28FB5C55DF06F4C52C9DE2BCBF6955817183995497CEA956AE515D2261898FA051015728E5A8AACAA68FFFFFFFFFFFFFFFF G = 2 @@ -288,8 +290,8 @@ class KexGSSGroup14(KexGSSGroup1): class KexGSSGex(object): """ - GSS-API / SSPI Authenticated Diffie-Hellman Group Exchange - as defined in `RFC 4462 Section 2 `_ + GSS-API / SSPI Authenticated Diffie-Hellman Group Exchange as defined in + `RFC 4462 Section 2 `_ """ NAME = "gss-gex-sha1-toWM5Slw5Ew8Mqkay+al2g==" min_bits = 1024 @@ -590,8 +592,9 @@ class KexGSSGex(object): class NullHostKey(object): """ - This class represents the Null Host Key for GSS-API Key Exchange - as defined in `RFC 4462 Section 5 `_ + This class represents the Null Host Key for GSS-API Key Exchange as defined + in `RFC 4462 Section 5 + `_ """ def __init__(self): self.key = "" diff --git a/paramiko/ssh_gss.py b/paramiko/ssh_gss.py index ebf2cc80..aa28e2ec 100644 --- a/paramiko/ssh_gss.py +++ b/paramiko/ssh_gss.py @@ -20,7 +20,7 @@ """ -This module provides GSS-API / SSPI authentication as defined in RFC 4462. +This module provides GSS-API / SSPI authentication as defined in :rfc:`4462`. .. note:: Credential delegation is not supported in server mode. diff --git a/sites/www/index.rst b/sites/www/index.rst index 1b609709..8e7562af 100644 --- a/sites/www/index.rst +++ b/sites/www/index.rst @@ -26,11 +26,7 @@ Please see the sidebar to the left to begin. .. rubric:: Footnotes .. [#] - SSH is defined in RFCs - `4251 `_, - `4252 `_, - `4253 `_, and - `4254 `_; - the primary working implementation of the protocol is the `OpenSSH project + SSH is defined in :rfc:`4251`, :rfc:`4252`, :rfc:`4253` and :rfc:`4254`. The + primary working implementation of the protocol is the `OpenSSH project `_. Paramiko implements a large portion of the SSH feature set, but there are occasional gaps. -- cgit v1.2.3 From b0a5ca8e3747a34082895bbd170a617f76ebe7e5 Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Thu, 5 Mar 2015 09:54:41 -0800 Subject: Rename new exception class to be less generic Re #22 --- paramiko/client.py | 4 ++-- paramiko/ssh_exception.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/paramiko/client.py b/paramiko/client.py index 6c48b269..e10e9da7 100644 --- a/paramiko/client.py +++ b/paramiko/client.py @@ -37,7 +37,7 @@ from paramiko.py3compat import string_types from paramiko.resource import ResourceManager from paramiko.rsakey import RSAKey from paramiko.ssh_exception import ( - SSHException, BadHostKeyException, ConnectionError + SSHException, BadHostKeyException, NoValidConnectionsError ) from paramiko.transport import Transport from paramiko.util import retry_on_signal, ClosingContextManager @@ -307,7 +307,7 @@ class SSHClient (ClosingContextManager): # ones, of a subclass that client code should still be watching for # (socket.error) if len(errors) == len(to_try): - raise ConnectionError(errors) + raise NoValidConnectionsError(errors) t = self._transport = Transport(sock, gss_kex=gss_kex, gss_deleg_creds=gss_deleg_creds) t.use_compression(compress=compress) diff --git a/paramiko/ssh_exception.py b/paramiko/ssh_exception.py index 7e6f2568..169dad81 100644 --- a/paramiko/ssh_exception.py +++ b/paramiko/ssh_exception.py @@ -133,7 +133,7 @@ class ProxyCommandFailure (SSHException): self.args = (command, error, ) -class ConnectionError(socket.error): +class NoValidConnectionsError(socket.error): """ High-level socket error wrapping 1+ actual socket.error objects. @@ -155,7 +155,7 @@ class ConnectionError(socket.error): body = ', '.join([x[0] for x in addrs[:-1]]) tail = addrs[-1][0] msg = "Unable to connect to port {0} at {1} or {2}" - super(ConnectionError, self).__init__( + super(NoValidConnectionsError, self).__init__( msg.format(addrs[0][1], body, tail) ) self.errors = errors -- cgit v1.2.3 From 136e0deef9c949a1b223244b696e6d870cc12e34 Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Thu, 5 Mar 2015 09:55:29 -0800 Subject: Add null errno to socket.error subclass. Makes downstream code less likely to break when they expect errno+msg style socket error objects. Re #22 --- paramiko/ssh_exception.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/paramiko/ssh_exception.py b/paramiko/ssh_exception.py index 169dad81..1fbebde8 100644 --- a/paramiko/ssh_exception.py +++ b/paramiko/ssh_exception.py @@ -135,7 +135,14 @@ class ProxyCommandFailure (SSHException): class NoValidConnectionsError(socket.error): """ - High-level socket error wrapping 1+ actual socket.error objects. + Multiple connection attempts were made and no families succeeded. + + This exception class wraps multiple "real" underlying connection errors, + all of which represent failed connection attempts. Because these errors are + not guaranteed to all be of the same error type (i.e. different errno, + class, message, etc) we expose a single unified error message and a + ``None`` errno so that instances of this class match most normal handling + of `socket.error` objects. To see the wrapped exception objects, access the ``errors`` attribute. ``errors`` is a dict whose keys are address tuples (e.g. ``('127.0.0.1', @@ -156,6 +163,7 @@ class NoValidConnectionsError(socket.error): tail = addrs[-1][0] msg = "Unable to connect to port {0} at {1} or {2}" super(NoValidConnectionsError, self).__init__( + None, # stand-in for errno msg.format(addrs[0][1], body, tail) ) self.errors = errors -- cgit v1.2.3 From cfeca480db0116c5c8d95ba1aaa9e5e5e02951ed Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Thu, 5 Mar 2015 09:55:36 -0800 Subject: Error message langauge tweak --- paramiko/ssh_exception.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/paramiko/ssh_exception.py b/paramiko/ssh_exception.py index 1fbebde8..2e09c6d6 100644 --- a/paramiko/ssh_exception.py +++ b/paramiko/ssh_exception.py @@ -161,7 +161,7 @@ class NoValidConnectionsError(socket.error): addrs = errors.keys() body = ', '.join([x[0] for x in addrs[:-1]]) tail = addrs[-1][0] - msg = "Unable to connect to port {0} at {1} or {2}" + msg = "Unable to connect to port {0} on {1} or {2}" super(NoValidConnectionsError, self).__init__( None, # stand-in for errno msg.format(addrs[0][1], body, tail) -- cgit v1.2.3