summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--paramiko/_version.py2
-rw-r--r--paramiko/client.py105
-rw-r--r--paramiko/sftp_client.py1
-rw-r--r--paramiko/ssh_exception.py38
-rw-r--r--setup_helper.py62
-rw-r--r--sites/shared_conf.py1
-rw-r--r--sites/www/changelog.rst7
7 files changed, 186 insertions, 30 deletions
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__))
diff --git a/paramiko/client.py b/paramiko/client.py
index 9ee30287..5a215a81 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 DEBUG
@@ -35,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, NoValidConnectionsError
+)
from paramiko.transport import Transport
from paramiko.util import retry_on_signal, ClosingContextManager
@@ -172,10 +175,46 @@ class SSHClient (ClosingContextManager):
"""
self._policy = policy
- 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 _families_and_addresses(self, hostname, port):
+ """
+ Yield pairs of address families and addresses to try for connecting.
+
+ :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)
+ 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,
+ 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`)
@@ -206,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/``
@@ -216,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.
@@ -234,21 +277,37 @@ class SSHClient (ClosingContextManager):
``gss_deleg_creds`` and ``gss_host`` arguments.
"""
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:
+ errors = {}
+ # Try multiple possible address families (e.g. IPv4 vs IPv6)
+ to_try = list(self._families_and_addresses(hostname, port))
+ for af, addr in to_try:
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 as e:
+ # 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 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/sftp_client.py b/paramiko/sftp_client.py
index 89840eaa..6d48e692 100644
--- a/paramiko/sftp_client.py
+++ b/paramiko/sftp_client.py
@@ -589,6 +589,7 @@ class SFTPClient(BaseSFTP, ClosingContextManager):
.. versionadded:: 1.4
"""
+ # TODO: make class initialize with self._cwd set to self.normalize('.')
return self._cwd and u(self._cwd)
def putfo(self, fl, remotepath, file_size=0, callback=None, confirm=True):
diff --git a/paramiko/ssh_exception.py b/paramiko/ssh_exception.py
index e120a45e..02f3e52e 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):
"""
@@ -133,3 +135,39 @@ class ProxyCommandFailure (SSHException):
self.error = error
# for unpickling
self.args = (command, error, )
+
+
+class NoValidConnectionsError(socket.error):
+ """
+ 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,
+ `socket.error` subclass, 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',
+ 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} on {1} or {2}"
+ super(NoValidConnectionsError, self).__init__(
+ None, # stand-in for errno
+ msg.format(addrs[0][1], body, tail)
+ )
+ self.errors = errors
diff --git a/setup_helper.py b/setup_helper.py
index ff6b0e16..9e3834b3 100644
--- a/setup_helper.py
+++ b/setup_helper.py
@@ -30,9 +30,42 @@ import distutils.archive_util
from distutils.dir_util import mkpath
from distutils.spawn import spawn
-
-def make_tarball(base_name, base_dir, compress='gzip',
- verbose=False, dry_run=False):
+try:
+ from pwd import getpwnam
+except ImportError:
+ getpwnam = None
+
+try:
+ from grp import getgrnam
+except ImportError:
+ getgrnam = None
+
+def _get_gid(name):
+ """Returns a gid, given a group name."""
+ if getgrnam is None or name is None:
+ return None
+ try:
+ result = getgrnam(name)
+ except KeyError:
+ result = None
+ if result is not None:
+ return result[2]
+ return None
+
+def _get_uid(name):
+ """Returns an uid, given a user name."""
+ if getpwnam is None or name is None:
+ return None
+ try:
+ result = getpwnam(name)
+ except KeyError:
+ result = None
+ if result is not None:
+ return result[2]
+ return None
+
+def make_tarball(base_name, base_dir, compress='gzip', verbose=0, dry_run=0,
+ owner=None, group=None):
"""Create a tar file from all the files under 'base_dir'.
This file may be compressed.
@@ -75,11 +108,30 @@ def make_tarball(base_name, base_dir, compress='gzip',
mkpath(os.path.dirname(archive_name), dry_run=dry_run)
log.info('Creating tar file %s with mode %s' % (archive_name, mode))
+ uid = _get_uid(owner)
+ gid = _get_gid(group)
+
+ def _set_uid_gid(tarinfo):
+ if gid is not None:
+ tarinfo.gid = gid
+ tarinfo.gname = group
+ if uid is not None:
+ tarinfo.uid = uid
+ tarinfo.uname = owner
+ return tarinfo
+
if not dry_run:
tar = tarfile.open(archive_name, mode=mode)
# This recursively adds everything underneath base_dir
- tar.add(base_dir)
- tar.close()
+ try:
+ try:
+ # Support for the `filter' parameter was added in Python 2.7,
+ # earlier versions will raise TypeError.
+ tar.add(base_dir, filter=_set_uid_gid)
+ except TypeError:
+ tar.add(base_dir)
+ finally:
+ tar.close()
if compress and compress not in tarfile_compress_flag:
spawn([compress] + compress_flags[compress] + [archive_name],
diff --git a/sites/shared_conf.py b/sites/shared_conf.py
index 4a6a5c4e..99fab315 100644
--- a/sites/shared_conf.py
+++ b/sites/shared_conf.py
@@ -12,7 +12,6 @@ html_theme_options = {
'description': "A Python implementation of SSHv2.",
'github_user': 'paramiko',
'github_repo': 'paramiko',
- 'gratipay_user': 'bitprophet',
'analytics_id': 'UA-18486793-2',
'travis_button': True,
}
diff --git a/sites/www/changelog.rst b/sites/www/changelog.rst
index 764c8801..bbc89b75 100644
--- a/sites/www/changelog.rst
+++ b/sites/www/changelog.rst
@@ -21,6 +21,13 @@ Changelog
which caused ``OverFlowError`` (and other symptoms) in SFTP functionality.
Thanks to ``@dboreham`` for leading the troubleshooting charge, and to
Scott Maxwell for the final patch.
+* :support:`582` Fix some old ``setup.py`` related helper code which was
+ breaking ``bdist_dumb`` on Mac OS X. Thanks to Peter Odding for the patch.
+* :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