summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--README.rst2
-rw-r--r--paramiko/_winapi.py6
-rw-r--r--paramiko/auth_handler.py13
-rw-r--r--paramiko/channel.py5
-rw-r--r--paramiko/config.py4
-rw-r--r--paramiko/sftp_client.py36
-rw-r--r--paramiko/ssh_exception.py10
-rw-r--r--setup.py4
-rw-r--r--sites/www/changelog.rst32
-rwxr-xr-xtest.py4
-rw-r--r--tests/test_client.py39
-rwxr-xr-xtests/test_sftp.py4
-rw-r--r--tests/test_ssh_exception.py31
-rw-r--r--tests/test_transport.py18
-rw-r--r--tests/test_util.py9
15 files changed, 166 insertions, 51 deletions
diff --git a/README.rst b/README.rst
index 0dcf30e5..8e8c6186 100644
--- a/README.rst
+++ b/README.rst
@@ -11,7 +11,7 @@ Paramiko
:Paramiko: Python SSH module
:Copyright: Copyright (c) 2003-2009 Robey Pointer <robeypointer@gmail.com>
-:Copyright: Copyright (c) 2013-2015 Jeff Forcier <jeff@bitprophet.org>
+:Copyright: Copyright (c) 2013-2016 Jeff Forcier <jeff@bitprophet.org>
:License: `LGPL <https://www.gnu.org/copyleft/lesser.html>`_
:Homepage: http://www.paramiko.org/
:API docs: http://docs.paramiko.org
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/auth_handler.py b/paramiko/auth_handler.py
index ef4a8c7e..38b23729 100644
--- a/paramiko/auth_handler.py
+++ b/paramiko/auth_handler.py
@@ -356,7 +356,7 @@ class AuthHandler (object):
m.add_string(p[0])
m.add_boolean(p[1])
self.transport._send_message(m)
-
+
def _parse_userauth_request(self, m):
if not self.transport.server_mode:
# er, uh... what?
@@ -495,8 +495,9 @@ class AuthHandler (object):
m.add_string(token)
self.transport._send_message(m)
else:
- raise SSHException("Client asked to handle paket %s"
- %MSG_NAMES[ptype])
+ result = AUTH_FAILED
+ self._send_auth_result(username, method, result)
+ return
# check MIC
ptype, m = self.transport.packetizer.read_message()
if ptype == MSG_USERAUTH_GSSAPI_MIC:
@@ -568,7 +569,7 @@ class AuthHandler (object):
lang = m.get_string()
self.transport._log(INFO, 'Auth banner: %s' % banner)
# who cares.
-
+
def _parse_userauth_info_request(self, m):
if self.auth_method != 'keyboard-interactive':
raise SSHException('Illegal info request from server')
@@ -580,14 +581,14 @@ class AuthHandler (object):
for i in range(prompts):
prompt_list.append((m.get_text(), m.get_boolean()))
response_list = self.interactive_handler(title, instructions, prompt_list)
-
+
m = Message()
m.add_byte(cMSG_USERAUTH_INFO_RESPONSE)
m.add_int(len(response_list))
for r in response_list:
m.add_string(r)
self.transport._send_message(m)
-
+
def _parse_userauth_info_response(self, m):
if not self.transport.server_mode:
raise SSHException('Illegal info response from server')
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/config.py b/paramiko/config.py
index 0b1345fd..e18fa4bf 100644
--- a/paramiko/config.py
+++ b/paramiko/config.py
@@ -57,7 +57,9 @@ class SSHConfig (object):
"""
host = {"host": ['*'], "config": {}}
for line in file_obj:
- line = line.rstrip('\r\n').lstrip()
+ # Strip any leading or trailing whitespace from the line.
+ # See https://github.com/paramiko/paramiko/issues/499 for more info.
+ line = line.strip()
if not line or line.startswith('#'):
continue
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, ))
diff --git a/setup.py b/setup.py
index 629c28fd..f9c4b4fe 100644
--- a/setup.py
+++ b/setup.py
@@ -41,8 +41,8 @@ try:
from setuptools import setup
kw = {
'install_requires': [
- 'pycrypto >= 2.1, != 2.4',
- 'ecdsa >= 0.11',
+ 'pycrypto>=2.1,!=2.4',
+ 'ecdsa>=0.11',
],
}
except ImportError:
diff --git a/sites/www/changelog.rst b/sites/www/changelog.rst
index 85fbe73e..7f3667e3 100644
--- a/sites/www/changelog.rst
+++ b/sites/www/changelog.rst
@@ -2,6 +2,33 @@
Changelog
=========
+* :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
+ 3.5). Props to Ed Kellett for assistance during some of the troubleshooting.
+* :support:`697` Remove whitespace in our ``setup.py``'s ``install_requires``
+ as it triggers occasional bugs in some versions of ``setuptools``. Thanks to
+ Justin Lecher for catch & original patch.
+* :bug:`499` Strip trailing/leading whitespace from lines when parsing SSH
+ config files - this brings things in line with OpenSSH behavior. Thanks to
+ Alfredo Esteban for the original report and Nick Pillitteri for the patch.
+* :bug:`652` Fix behavior of ``gssapi-with-mic`` auth requests so they fail
+ gracefully (allowing followup via other auth methods) instead of raising an
+ exception. Patch courtesy of ``@jamercee``.
* :feature:`588` Add missing file-like object methods for
`~paramiko.file.BufferedFile` and `~paramiko.sftp_file.SFTPFile`. Thanks to
Adam Meily for the patch.
@@ -142,8 +169,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.
diff --git a/test.py b/test.py
index 37fc5a6f..a1f13d85 100755
--- a/test.py
+++ b/test.py
@@ -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_client.py b/tests/test_client.py
index f71efd5a..d39febac 100644
--- a/tests/test_client.py
+++ b/tests/test_client.py
@@ -87,6 +87,12 @@ class SSHClientTest (unittest.TestCase):
self.sockl.bind(('localhost', 0))
self.sockl.listen(1)
self.addr, self.port = self.sockl.getsockname()
+ self.connect_kwargs = dict(
+ hostname=self.addr,
+ port=self.port,
+ username='slowdive',
+ look_for_keys=False,
+ )
self.event = threading.Event()
def tearDown(self):
@@ -124,7 +130,7 @@ class SSHClientTest (unittest.TestCase):
self.tc.get_host_keys().add('[%s]:%d' % (self.addr, self.port), 'ssh-rsa', public_host_key)
# Actual connection
- self.tc.connect(self.addr, self.port, username='slowdive', **kwargs)
+ self.tc.connect(**dict(self.connect_kwargs, **kwargs))
# Authentication successful?
self.event.wait(1.0)
@@ -229,7 +235,7 @@ class SSHClientTest (unittest.TestCase):
self.tc = paramiko.SSHClient()
self.tc.set_missing_host_key_policy(paramiko.AutoAddPolicy())
self.assertEqual(0, len(self.tc.get_host_keys()))
- self.tc.connect(self.addr, self.port, username='slowdive', password='pygmalion')
+ self.tc.connect(password='pygmalion', **self.connect_kwargs)
self.event.wait(1.0)
self.assertTrue(self.event.is_set())
@@ -284,7 +290,7 @@ class SSHClientTest (unittest.TestCase):
self.tc = paramiko.SSHClient()
self.tc.set_missing_host_key_policy(paramiko.AutoAddPolicy())
self.assertEqual(0, len(self.tc.get_host_keys()))
- self.tc.connect(self.addr, self.port, username='slowdive', password='pygmalion')
+ self.tc.connect(**dict(self.connect_kwargs, password='pygmalion'))
self.event.wait(1.0)
self.assertTrue(self.event.is_set())
@@ -319,7 +325,7 @@ class SSHClientTest (unittest.TestCase):
self.tc = tc
self.tc.set_missing_host_key_policy(paramiko.AutoAddPolicy())
self.assertEquals(0, len(self.tc.get_host_keys()))
- self.tc.connect(self.addr, self.port, username='slowdive', password='pygmalion')
+ self.tc.connect(**dict(self.connect_kwargs, password='pygmalion'))
self.event.wait(1.0)
self.assertTrue(self.event.is_set())
@@ -341,12 +347,29 @@ class SSHClientTest (unittest.TestCase):
self.tc = paramiko.SSHClient()
self.tc.get_host_keys().add('[%s]:%d' % (self.addr, self.port), 'ssh-rsa', public_host_key)
# Connect with a half second banner timeout.
+ kwargs = dict(self.connect_kwargs, banner_timeout=0.5)
self.assertRaises(
paramiko.SSHException,
self.tc.connect,
- self.addr,
- self.port,
- username='slowdive',
+ **kwargs
+ )
+
+ def test_8_auth_trickledown(self):
+ """
+ Failed key auth doesn't prevent subsequent pw auth from succeeding
+ """
+ # NOTE: re #387, re #394
+ # If pkey module used within Client._auth isn't correctly handling auth
+ # errors (e.g. if it allows things like ValueError to bubble up as per
+ # midway thru #394) client.connect() will fail (at key load step)
+ # instead of succeeding (at password step)
+ kwargs = dict(
+ # Password-protected key whose passphrase is not 'pygmalion' (it's
+ # 'television' as per tests/test_pkey.py). NOTE: must use
+ # key_filename, loading the actual key here with PKey will except
+ # immediately; we're testing the try/except crap within Client.
+ key_filename=[test_path('test_rsa_password.key')],
+ # Actual password for default 'slowdive' user
password='pygmalion',
- banner_timeout=0.5
)
+ self._test_connection(**kwargs)
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)
diff --git a/tests/test_transport.py b/tests/test_transport.py
index a93d8b63..5069e5b0 100644
--- a/tests/test_transport.py
+++ b/tests/test_transport.py
@@ -32,7 +32,7 @@ from hashlib import sha1
import unittest
from paramiko import Transport, SecurityOptions, ServerInterface, RSAKey, DSSKey, \
- SSHException, ChannelException
+ SSHException, ChannelException, Packetizer
from paramiko import AUTH_FAILED, AUTH_SUCCESSFUL
from paramiko import OPEN_SUCCEEDED, OPEN_FAILED_ADMINISTRATIVELY_PROHIBITED
from paramiko.common import MSG_KEXINIT, cMSG_CHANNEL_WINDOW_ADJUST, \
@@ -800,6 +800,22 @@ class TransportTest(unittest.TestCase):
"""
verify that we can get a hanshake timeout.
"""
+ # Tweak client Transport instance's Packetizer instance so
+ # its read_message() sleeps a bit. This helps prevent race conditions
+ # where the client Transport's timeout timer thread doesn't even have
+ # time to get scheduled before the main client thread finishes
+ # handshaking with the server.
+ # (Doing this on the server's transport *sounds* more 'correct' but
+ # actually doesn't work nearly as well for whatever reason.)
+ class SlowPacketizer(Packetizer):
+ def read_message(self):
+ time.sleep(1)
+ return super(SlowPacketizer, self).read_message()
+ # NOTE: prettttty sure since the replaced .packetizer Packetizer is now
+ # no longer doing anything with its copy of the socket...everything'll
+ # be fine. Even tho it's a bit squicky.
+ self.tc.packetizer = SlowPacketizer(self.tc.sock)
+ # Continue with regular test red tape.
host_key = RSAKey.from_private_key_file(test_path('test_rsa.key'))
public_host_key = RSAKey(data=host_key.asbytes())
self.ts.add_server_key(host_key)
diff --git a/tests/test_util.py b/tests/test_util.py
index bfdc525e..a6a2c30b 100644
--- a/tests/test_util.py
+++ b/tests/test_util.py
@@ -30,6 +30,7 @@ import paramiko.util
from paramiko.util import lookup_ssh_host_config as host_config, safe_string
from paramiko.py3compat import StringIO, byte_ord, b
+# Note some lines in this configuration have trailing spaces on purpose
test_config_file = """\
Host *
User robey
@@ -110,7 +111,7 @@ class UtilTest(unittest.TestCase):
self.assertEqual(config._config,
[{'host': ['*'], 'config': {}}, {'host': ['*'], 'config': {'identityfile': ['~/.ssh/id_rsa'], 'user': 'robey'}},
{'host': ['*.example.com'], 'config': {'user': 'bjork', 'port': '3333'}},
- {'host': ['*'], 'config': {'crazy': 'something dumb '}},
+ {'host': ['*'], 'config': {'crazy': 'something dumb'}},
{'host': ['spoo.example.com'], 'config': {'crazy': 'something else'}}])
def test_3_host_config(self):
@@ -119,14 +120,14 @@ class UtilTest(unittest.TestCase):
config = paramiko.util.parse_ssh_config(f)
for host, values in {
- 'irc.danger.com': {'crazy': 'something dumb ',
+ 'irc.danger.com': {'crazy': 'something dumb',
'hostname': 'irc.danger.com',
'user': 'robey'},
- 'irc.example.com': {'crazy': 'something dumb ',
+ 'irc.example.com': {'crazy': 'something dumb',
'hostname': 'irc.example.com',
'user': 'robey',
'port': '3333'},
- 'spoo.example.com': {'crazy': 'something dumb ',
+ 'spoo.example.com': {'crazy': 'something dumb',
'hostname': 'spoo.example.com',
'user': 'robey',
'port': '3333'}