diff options
-rw-r--r-- | .travis.yml | 3 | ||||
-rw-r--r-- | paramiko/_winapi.py | 6 | ||||
-rw-r--r-- | paramiko/channel.py | 5 | ||||
-rw-r--r-- | paramiko/client.py | 21 | ||||
-rw-r--r-- | paramiko/primes.py | 4 | ||||
-rw-r--r-- | paramiko/sftp_client.py | 36 | ||||
-rw-r--r-- | paramiko/ssh_exception.py | 10 | ||||
-rw-r--r-- | setup.py | 24 | ||||
-rw-r--r-- | sites/www/changelog.rst | 25 | ||||
-rwxr-xr-x | test.py | 4 | ||||
-rwxr-xr-x | tests/test_sftp.py | 4 | ||||
-rw-r--r-- | tests/test_ssh_exception.py | 31 |
12 files changed, 122 insertions, 51 deletions
diff --git a/.travis.yml b/.travis.yml index 55cba46d..3b7b2b42 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,5 +1,8 @@ language: python sudo: false +cache: + directories: + - $HOME/.cache/pip python: - "2.6" - "2.7" diff --git a/paramiko/_winapi.py b/paramiko/_winapi.py index 9a8bdedd..77e0129c 100644 --- a/paramiko/_winapi.py +++ b/paramiko/_winapi.py @@ -1,6 +1,6 @@ """ Windows API functions implemented as ctypes functions and classes as found -in jaraco.windows (3.3). +in jaraco.windows (3.4.1). 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. @@ -158,7 +158,7 @@ class MemoryMap(object): if self.pos + n >= self.length: # A little safety. raise ValueError("Refusing to write %d bytes" % n) dest = self.view + self.pos - length = ctypes.wintypes.SIZE(n) + length = ctypes.c_size_t(n) ctypes.windll.kernel32.RtlMoveMemory(dest, msg, length) self.pos += n @@ -168,7 +168,7 @@ class MemoryMap(object): """ out = ctypes.create_string_buffer(n) source = self.view + self.pos - length = ctypes.wintypes.SIZE(n) + length = ctypes.c_size_t(n) ctypes.windll.kernel32.RtlMoveMemory(out, source, length) self.pos += n return out.raw diff --git a/paramiko/channel.py b/paramiko/channel.py index 4ce4f286..3c43eb10 100644 --- a/paramiko/channel.py +++ b/paramiko/channel.py @@ -88,15 +88,20 @@ class Channel (ClosingContextManager): :param int chanid: the ID of this channel, as passed by an existing `.Transport`. """ + #: Channel ID self.chanid = chanid + #: Remote channel ID self.remote_chanid = 0 + #: `.Transport` managing this channel self.transport = None + #: Whether the connection is presently active self.active = False self.eof_received = 0 self.eof_sent = 0 self.in_buffer = BufferedPipe() self.in_stderr_buffer = BufferedPipe() self.timeout = None + #: Whether the connection has been closed self.closed = False self.ultra_debug = False self.lock = threading.Lock() diff --git a/paramiko/client.py b/paramiko/client.py index 8d899a15..e3d3780e 100644 --- a/paramiko/client.py +++ b/paramiko/client.py @@ -164,10 +164,23 @@ class SSHClient (ClosingContextManager): def set_missing_host_key_policy(self, policy): """ - Set the policy to use when connecting to a server that doesn't have a - host key in either the system or local `.HostKeys` objects. The - default policy is to reject all unknown servers (using `.RejectPolicy`). - You may substitute `.AutoAddPolicy` or write your own policy class. + Set policy to use when connecting to servers without a known host key. + + Specifically: + + * A **policy** is an instance of a "policy class", namely some subclass + of `.MissingHostKeyPolicy` such as `.RejectPolicy` (the default), + `.AutoAddPolicy`, `.WarningPolicy`, or a user-created subclass. + + .. note:: + This method takes class **instances**, not **classes** themselves. + Thus it must be called as e.g. + ``.set_missing_host_key_policy(WarningPolicy())`` and *not* + ``.set_missing_host_key_policy(WarningPolicy)``. + + * A host key is **known** when it appears in the client object's cached + host keys structures (those manipulated by `load_system_host_keys` + and/or `load_host_keys`). :param .MissingHostKeyPolicy policy: the policy to use when receiving a host key from a diff --git a/paramiko/primes.py b/paramiko/primes.py index 7415c182..d0e17575 100644 --- a/paramiko/primes.py +++ b/paramiko/primes.py @@ -113,12 +113,12 @@ class ModulusPack (object): good = -1 # find nearest bitsize >= preferred for b in bitsizes: - if (b >= prefer) and (b < max) and (b < good or good == -1): + if (b >= prefer) and (b <= max) and (b < good or good == -1): good = b # if that failed, find greatest bitsize >= min if good == -1: for b in bitsizes: - if (b >= min) and (b < max) and (b > good): + if (b >= min) and (b <= max) and (b > good): good = b if good == -1: # their entire (min, max) range has no intersection with our range. diff --git a/paramiko/sftp_client.py b/paramiko/sftp_client.py index 57225558..daaae3ef 100644 --- a/paramiko/sftp_client.py +++ b/paramiko/sftp_client.py @@ -592,6 +592,18 @@ class SFTPClient(BaseSFTP, ClosingContextManager): # TODO: make class initialize with self._cwd set to self.normalize('.') return self._cwd and u(self._cwd) + def _transfer_with_callback(self, reader, writer, file_size, callback): + size = 0 + while True: + data = reader.read(32768) + writer.write(data) + size += len(data) + if len(data) == 0: + break + if callback is not None: + callback(size, file_size) + return size + def putfo(self, fl, remotepath, file_size=0, callback=None, confirm=True): """ Copy the contents of an open file object (``fl``) to the SFTP server as @@ -621,15 +633,9 @@ class SFTPClient(BaseSFTP, ClosingContextManager): """ with self.file(remotepath, 'wb') as fr: fr.set_pipelined(True) - size = 0 - while True: - data = fl.read(32768) - fr.write(data) - size += len(data) - if callback is not None: - callback(size, file_size) - if len(data) == 0: - break + size = self._transfer_with_callback( + reader=fl, writer=fr, file_size=file_size, callback=callback + ) if confirm: s = self.stat(remotepath) if s.st_size != size: @@ -689,16 +695,10 @@ class SFTPClient(BaseSFTP, ClosingContextManager): file_size = self.stat(remotepath).st_size with self.open(remotepath, 'rb') as fr: fr.prefetch(file_size) + return self._transfer_with_callback( + reader=fr, writer=fl, file_size=file_size, callback=callback + ) - size = 0 - while True: - data = fr.read(32768) - fl.write(data) - size += len(data) - if callback is not None: - callback(size, file_size) - if len(data) == 0: - break return size def get(self, remotepath, localpath, callback=None): diff --git a/paramiko/ssh_exception.py b/paramiko/ssh_exception.py index 016a411e..ed36a952 100644 --- a/paramiko/ssh_exception.py +++ b/paramiko/ssh_exception.py @@ -164,12 +164,18 @@ class NoValidConnectionsError(socket.error): :param dict errors: The errors dict to store, as described by class docstring. """ - addrs = errors.keys() + addrs = sorted(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}" + if body: + msg = "Unable to connect to port {0} on {1} or {2}" + else: + msg = "Unable to connect to port {0} on {2}" super(NoValidConnectionsError, self).__init__( None, # stand-in for errno msg.format(addrs[0][1], body, tail) ) self.errors = errors + + def __reduce__(self): + return (self.__class__, (self.errors, )) @@ -31,23 +31,10 @@ To install the `in-development version `pip install paramiko==dev`. ''' -# if someday we want to *require* setuptools, uncomment this: -# (it will cause setuptools to be automatically downloaded) -#import ez_setup -#ez_setup.use_setuptools() - import sys -try: - from setuptools import setup - kw = { - 'install_requires': [ - 'cryptography>=0.8', - 'pyasn1>=0.1.7', - ], - } -except ImportError: - from distutils.core import setup - kw = {} + +from setuptools import setup + if sys.platform == 'darwin': import setup_helper @@ -89,5 +76,8 @@ setup( 'Programming Language :: Python :: 3.4', 'Programming Language :: Python :: 3.5', ], - **kw + install_requires=[ + 'cryptography>=0.8', + 'pyasn1>=0.1.7', + ], ) diff --git a/sites/www/changelog.rst b/sites/www/changelog.rst index eaf3cd57..488360d7 100644 --- a/sites/www/changelog.rst +++ b/sites/www/changelog.rst @@ -2,6 +2,26 @@ Changelog ========= +* :support:`729 backported` Clean up ``setup.py`` to always use ``setuptools``, + not doing so was a historical artifact from bygone days. Thanks to Alex + Gaynor. +* :bug:`649 major` Update the module in charge of handling SSH moduli so it's + consistent with OpenSSH behavior re: prime number selection. Thanks to Damien + Tournoud for catch & patch. +* :bug:`617` (aka `fabric/fabric#1429 + <https://github.com/fabric/fabric/issues/1429>`_; via :issue:`679`; related: + :issue:`678`, :issue:`685`, :issue:`615` & :issue:`616`) Fix up + `~paramiko.ssh_exception.NoValidConnectionsError` so it pickles correctly, + and fix a related Python 3 compatibility issue. Thanks to Rebecca Schlussel + for the report & Marius Gedminas for the patch. +* :bug:`613` (via :issue:`619`) Update to ``jaraco.windows`` 3.4.1 to fix some + errors related to ``ctypes`` on Windows platforms. Credit to Jason R. Coombs. +* :support:`621 backported` Annotate some public attributes on + `~paramiko.channel.Channel` such as ``.closed``. Thanks to Sergey Vasilyev + for the report. +* :bug:`632` Fix logic bug in the SFTP client's callback-calling functionality; + previously there was a chance the given callback would fire twice at the end + of a transfer. Thanks to ``@ab9-er`` for catch & original patch. * :support:`612` Identify & work around a race condition in the test for handshake timeouts, which was causing frequent test failures for a subset of contributors as well as Travis-CI (usually, but not always, limited to Python @@ -155,8 +175,9 @@ Changelog well) would hang due to incorrect values passed into the new window size arguments for `.Transport` (thanks to a botched merge). This has been corrected. Thanks to Dylan Thacker-Smith for the report & patch. -* :feature:`167` Add `.SSHConfig.get_hostnames` for easier introspection of a - loaded SSH config file or object. Courtesy of Søren Løvborg. +* :feature:`167` Add `~paramiko.config.SSHConfig.get_hostnames` for easier + introspection of a loaded SSH config file or object. Courtesy of Søren + Løvborg. * :release:`1.15.0 <2014-09-18>` * :support:`393` Replace internal use of PyCrypto's ``SHA.new`` with the stdlib's ``hashlib.sha1``. Thanks to Alex Gaynor. @@ -43,8 +43,9 @@ from tests.test_kex import KexTest from tests.test_packetizer import PacketizerTest from tests.test_auth import AuthTest from tests.test_transport import TransportTest +from tests.test_ssh_exception import NoValidConnectionsErrorTest from tests.test_client import SSHClientTest -from test_client import SSHClientTest +from test_client import SSHClientTest # XXX why shadow the above import? from test_gssapi import GSSAPITest from test_ssh_gss import GSSAuthTest from test_kex_gss import GSSKexTest @@ -156,6 +157,7 @@ def main(): if options.use_transport: suite.addTest(unittest.makeSuite(AuthTest)) suite.addTest(unittest.makeSuite(TransportTest)) + suite.addTest(unittest.makeSuite(NoValidConnectionsErrorTest)) suite.addTest(unittest.makeSuite(SSHClientTest)) if options.use_sftp: suite.addTest(unittest.makeSuite(SFTPTest)) diff --git a/tests/test_sftp.py b/tests/test_sftp.py index 53b73ee0..e4c2c3a3 100755 --- a/tests/test_sftp.py +++ b/tests/test_sftp.py @@ -611,7 +611,7 @@ class SFTPTest (unittest.TestCase): with sftp.open(FOLDER + '/bunny.txt', 'rb') as f: self.assertEqual(text, f.read(128)) - self.assertEqual((41, 41), saved_progress[-1]) + self.assertEqual([(41, 41)], saved_progress) os.unlink(localname) fd, localname = mkstemp() @@ -621,7 +621,7 @@ class SFTPTest (unittest.TestCase): with open(localname, 'rb') as f: self.assertEqual(text, f.read(128)) - self.assertEqual((41, 41), saved_progress[-1]) + self.assertEqual([(41, 41)], saved_progress) os.unlink(localname) sftp.unlink(FOLDER + '/bunny.txt') diff --git a/tests/test_ssh_exception.py b/tests/test_ssh_exception.py new file mode 100644 index 00000000..18f2a97d --- /dev/null +++ b/tests/test_ssh_exception.py @@ -0,0 +1,31 @@ +import pickle +import unittest + +from paramiko.ssh_exception import NoValidConnectionsError + + +class NoValidConnectionsErrorTest (unittest.TestCase): + + def test_pickling(self): + # Regression test for https://github.com/paramiko/paramiko/issues/617 + exc = NoValidConnectionsError({('127.0.0.1', '22'): Exception()}) + new_exc = pickle.loads(pickle.dumps(exc)) + self.assertEqual(type(exc), type(new_exc)) + self.assertEqual(str(exc), str(new_exc)) + self.assertEqual(exc.args, new_exc.args) + + def test_error_message_for_single_host(self): + exc = NoValidConnectionsError({('127.0.0.1', '22'): Exception()}) + assert "Unable to connect to port 22 on 127.0.0.1" in str(exc) + + def test_error_message_for_two_hosts(self): + exc = NoValidConnectionsError({('127.0.0.1', '22'): Exception(), + ('::1', '22'): Exception()}) + assert "Unable to connect to port 22 on 127.0.0.1 or ::1" in str(exc) + + def test_error_message_for_multiple_hosts(self): + exc = NoValidConnectionsError({('127.0.0.1', '22'): Exception(), + ('::1', '22'): Exception(), + ('10.0.0.42', '22'): Exception()}) + exp = "Unable to connect to port 22 on 10.0.0.42, 127.0.0.1 or ::1" + assert exp in str(exc) |