summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--.travis.yml4
-rw-r--r--README.rst4
-rw-r--r--codecov.yml1
-rw-r--r--paramiko/auth_handler.py9
-rw-r--r--paramiko/client.py10
-rw-r--r--paramiko/common.py22
-rw-r--r--paramiko/dsskey.py8
-rw-r--r--paramiko/ecdsakey.py6
-rw-r--r--paramiko/file.py6
-rw-r--r--paramiko/hostkeys.py7
-rw-r--r--paramiko/kex_gss.py2
-rw-r--r--paramiko/pkey.py10
-rw-r--r--paramiko/primes.py1
-rw-r--r--paramiko/py3compat.py11
-rw-r--r--paramiko/resource.py71
-rw-r--r--paramiko/rsakey.py6
-rw-r--r--paramiko/sftp_client.py44
-rw-r--r--paramiko/sftp_server.py6
-rw-r--r--paramiko/sftp_si.py12
-rw-r--r--paramiko/transport.py28
-rw-r--r--paramiko/win_pageant.py2
-rw-r--r--sites/www/changelog.rst57
-rw-r--r--tests/__init__.py36
-rw-r--r--tests/stub_sftp.py14
-rw-r--r--tests/test_auth.py19
-rw-r--r--tests/test_client.py30
-rwxr-xr-xtests/test_file.py57
-rw-r--r--tests/test_pkey.py35
-rwxr-xr-xtests/test_sftp.py63
-rw-r--r--tests/test_transport.py69
30 files changed, 468 insertions, 182 deletions
diff --git a/.travis.yml b/.travis.yml
index c8faf0a2..7ca8db09 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -15,7 +15,7 @@ install:
# Self-install for setup.py-driven deps
- pip install -e .
# Dev (doc/test running) requirements
- - pip install coveralls # For coveralls.io specifically
+ - pip install codecov # For codecov specifically
- pip install -r dev-requirements.txt
script:
# Main tests, w/ coverage!
@@ -33,4 +33,4 @@ notifications:
on_failure: change
email: false
after_success:
- - coveralls
+ - codecov
diff --git a/README.rst b/README.rst
index e267f69a..399dceb9 100644
--- a/README.rst
+++ b/README.rst
@@ -6,8 +6,8 @@ Paramiko
.. image:: https://travis-ci.org/paramiko/paramiko.svg?branch=master
:target: https://travis-ci.org/paramiko/paramiko
-.. image:: https://coveralls.io/repos/paramiko/paramiko/badge.svg?branch=master&service=github
- :target: https://coveralls.io/github/paramiko/paramiko?branch=master
+.. image:: https://codecov.io/gh/paramiko/paramiko/branch/master/graph/badge.svg
+ :target: https://codecov.io/gh/paramiko/paramiko
:Paramiko: Python SSH module
:Copyright: Copyright (c) 2003-2009 Robey Pointer <robeypointer@gmail.com>
diff --git a/codecov.yml b/codecov.yml
new file mode 100644
index 00000000..69cb7601
--- /dev/null
+++ b/codecov.yml
@@ -0,0 +1 @@
+comment: false
diff --git a/paramiko/auth_handler.py b/paramiko/auth_handler.py
index 33f01da6..ae88179e 100644
--- a/paramiko/auth_handler.py
+++ b/paramiko/auth_handler.py
@@ -21,6 +21,8 @@
"""
import weakref
+import time
+
from paramiko.common import (
cMSG_SERVICE_REQUEST, cMSG_DISCONNECT, DISCONNECT_SERVICE_NOT_AVAILABLE,
DISCONNECT_NO_MORE_AUTH_METHODS_AVAILABLE, cMSG_USERAUTH_REQUEST,
@@ -35,7 +37,6 @@ from paramiko.common import (
MSG_USERAUTH_GSSAPI_TOKEN, MSG_USERAUTH_GSSAPI_ERROR,
MSG_USERAUTH_GSSAPI_ERRTOK, MSG_USERAUTH_GSSAPI_MIC, MSG_NAMES,
)
-
from paramiko.message import Message
from paramiko.py3compat import bytestring
from paramiko.ssh_exception import (
@@ -190,6 +191,9 @@ class AuthHandler (object):
return m.asbytes()
def wait_for_response(self, event):
+ max_ts = None
+ if self.transport.auth_timeout is not None:
+ max_ts = time.time() + self.transport.auth_timeout
while True:
event.wait(0.1)
if not self.transport.is_active():
@@ -199,6 +203,9 @@ class AuthHandler (object):
raise e
if event.is_set():
break
+ if max_ts is not None and max_ts <= time.time():
+ raise AuthenticationException('Authentication timeout.')
+
if not self.is_authenticated():
e = self.transport.get_exception()
if e is None:
diff --git a/paramiko/client.py b/paramiko/client.py
index 08fe69d4..b2c21798 100644
--- a/paramiko/client.py
+++ b/paramiko/client.py
@@ -36,7 +36,6 @@ from paramiko.ecdsakey import ECDSAKey
from paramiko.ed25519key import Ed25519Key
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, NoValidConnectionsError
@@ -227,7 +226,8 @@ class SSHClient (ClosingContextManager):
gss_kex=False,
gss_deleg_creds=True,
gss_host=None,
- banner_timeout=None
+ banner_timeout=None,
+ auth_timeout=None,
):
"""
Connect to an SSH server and authenticate to it. The server's host key
@@ -279,6 +279,8 @@ class SSHClient (ClosingContextManager):
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.
+ :param float auth_timeout: an optional timeout (in seconds) to wait for
+ an authentication response.
:raises:
`.BadHostKeyException` -- if the server's host key could not be
@@ -339,9 +341,9 @@ class SSHClient (ClosingContextManager):
t.set_log_channel(self._log_channel)
if banner_timeout is not None:
t.banner_timeout = banner_timeout
+ if auth_timeout is not None:
+ t.auth_timeout = auth_timeout
t.start_client(timeout=timeout)
- t.set_sshclient(self)
- ResourceManager.register(self, t)
server_key = t.get_remote_server_key()
keytype = server_key.get_name()
diff --git a/paramiko/common.py b/paramiko/common.py
index 556f046a..0012372a 100644
--- a/paramiko/common.py
+++ b/paramiko/common.py
@@ -20,9 +20,7 @@
Common constants and global variables.
"""
import logging
-from paramiko.py3compat import (
- byte_chr, PY2, bytes_types, string_types, b, long,
-)
+from paramiko.py3compat import byte_chr, PY2, bytes_types, text_type, long
MSG_DISCONNECT, MSG_IGNORE, MSG_UNIMPLEMENTED, MSG_DEBUG, \
MSG_SERVICE_REQUEST, MSG_SERVICE_ACCEPT = range(1, 7)
@@ -163,14 +161,16 @@ else:
def asbytes(s):
- if not isinstance(s, bytes_types):
- if isinstance(s, string_types):
- s = b(s)
- else:
- try:
- s = s.asbytes()
- except Exception:
- raise Exception('Unknown type')
+ """Coerce to bytes if possible or return unchanged."""
+ if isinstance(s, bytes_types):
+ return s
+ if isinstance(s, text_type):
+ # Accept text and encode as utf-8 for compatibility only.
+ return s.encode("utf-8")
+ asbytes = getattr(s, "asbytes", None)
+ if asbytes is not None:
+ return asbytes()
+ # May be an object that implements the buffer api, let callers handle.
return s
diff --git a/paramiko/dsskey.py b/paramiko/dsskey.py
index 55ef1e9b..9af5d0c1 100644
--- a/paramiko/dsskey.py
+++ b/paramiko/dsskey.py
@@ -83,13 +83,7 @@ class DSSKey(PKey):
return self.asbytes()
def __hash__(self):
- h = hash(self.get_name())
- h = h * 37 + hash(self.p)
- h = h * 37 + hash(self.q)
- h = h * 37 + hash(self.g)
- h = h * 37 + hash(self.y)
- # h might be a long by now...
- return hash(h)
+ return hash((self.get_name(), self.p, self.q, self.g, self.y))
def get_name(self):
return 'ssh-dss'
diff --git a/paramiko/ecdsakey.py b/paramiko/ecdsakey.py
index f5dacac8..fa850c2e 100644
--- a/paramiko/ecdsakey.py
+++ b/paramiko/ecdsakey.py
@@ -165,10 +165,8 @@ class ECDSAKey(PKey):
return self.asbytes()
def __hash__(self):
- h = hash(self.get_name())
- h = h * 37 + hash(self.verifying_key.public_numbers().x)
- h = h * 37 + hash(self.verifying_key.public_numbers().y)
- return hash(h)
+ return hash((self.get_name(), self.verifying_key.public_numbers().x,
+ self.verifying_key.public_numbers().y))
def get_name(self):
return self.ecdsa_curve.key_format_identifier
diff --git a/paramiko/file.py b/paramiko/file.py
index 5212091a..a1bdafbe 100644
--- a/paramiko/file.py
+++ b/paramiko/file.py
@@ -18,7 +18,7 @@
from paramiko.common import (
linefeed_byte_value, crlf, cr_byte, linefeed_byte, cr_byte_value,
)
-from paramiko.py3compat import BytesIO, PY2, u, b, bytes_types
+from paramiko.py3compat import BytesIO, PY2, u, bytes_types, text_type
from paramiko.util import ClosingContextManager
@@ -391,7 +391,9 @@ class BufferedFile (ClosingContextManager):
:param data: ``str``/``bytes`` data to write
"""
- data = b(data)
+ if isinstance(data, text_type):
+ # Accept text and encode as utf-8 for compatibility only.
+ data = data.encode('utf-8')
if self._closed:
raise IOError('File is closed')
if not (self._flags & self.FLAG_WRITE):
diff --git a/paramiko/hostkeys.py b/paramiko/hostkeys.py
index 3e27fd52..d023b33d 100644
--- a/paramiko/hostkeys.py
+++ b/paramiko/hostkeys.py
@@ -20,17 +20,12 @@
import binascii
import os
+from collections import MutableMapping
from hashlib import sha1
from hmac import HMAC
from paramiko.py3compat import b, u, encodebytes, decodebytes
-try:
- from collections import MutableMapping
-except ImportError:
- # noinspection PyUnresolvedReferences
- from UserDict import DictMixin as MutableMapping
-
from paramiko.dsskey import DSSKey
from paramiko.rsakey import RSAKey
from paramiko.util import get_logger, constant_time_bytes_eq
diff --git a/paramiko/kex_gss.py b/paramiko/kex_gss.py
index ba24c0a0..3406babb 100644
--- a/paramiko/kex_gss.py
+++ b/paramiko/kex_gss.py
@@ -40,7 +40,7 @@ This module provides GSS-API / SSPI Key Exchange as defined in :rfc:`4462`.
import os
from hashlib import sha1
-from paramiko.common import * # noqa
+from paramiko.common import DEBUG, max_byte, zero_byte
from paramiko import util
from paramiko.message import Message
from paramiko.py3compat import byte_chr, byte_mask, byte_ord
diff --git a/paramiko/pkey.py b/paramiko/pkey.py
index f5b0cd18..35a26fc7 100644
--- a/paramiko/pkey.py
+++ b/paramiko/pkey.py
@@ -48,6 +48,12 @@ class PKey(object):
'blocksize': 16,
'mode': modes.CBC
},
+ 'AES-256-CBC': {
+ 'cipher': algorithms.AES,
+ 'keysize': 32,
+ 'blocksize': 16,
+ 'mode': modes.CBC
+ },
'DES-EDE3-CBC': {
'cipher': algorithms.TripleDES,
'keysize': 24,
@@ -344,13 +350,13 @@ class PKey(object):
"""
with open(filename, 'w') as f:
os.chmod(filename, o600)
- self._write_private_key(f, key, format)
+ self._write_private_key(f, key, format, password=password)
def _write_private_key(self, f, key, format, password=None):
if password is None:
encryption = serialization.NoEncryption()
else:
- encryption = serialization.BestEncryption(password)
+ encryption = serialization.BestAvailableEncryption(b(password))
f.write(key.private_bytes(
serialization.Encoding.PEM,
diff --git a/paramiko/primes.py b/paramiko/primes.py
index 48a34e53..65617914 100644
--- a/paramiko/primes.py
+++ b/paramiko/primes.py
@@ -25,7 +25,6 @@ import os
from paramiko import util
from paramiko.py3compat import byte_mask, long
from paramiko.ssh_exception import SSHException
-from paramiko.common import * # noqa
def _roll_random(n):
diff --git a/paramiko/py3compat.py b/paramiko/py3compat.py
index 095b0d09..6703ace8 100644
--- a/paramiko/py3compat.py
+++ b/paramiko/py3compat.py
@@ -65,15 +65,8 @@ if PY2:
return s
- try:
- import cStringIO
-
- StringIO = cStringIO.StringIO # NOQA
- except ImportError:
- import StringIO
-
- StringIO = StringIO.StringIO # NOQA
-
+ import cStringIO
+ StringIO = cStringIO.StringIO
BytesIO = StringIO
diff --git a/paramiko/resource.py b/paramiko/resource.py
deleted file mode 100644
index 5fed22ad..00000000
--- a/paramiko/resource.py
+++ /dev/null
@@ -1,71 +0,0 @@
-# Copyright (C) 2003-2007 Robey Pointer <robeypointer@gmail.com>
-#
-# This file is part of paramiko.
-#
-# Paramiko is free software; you can redistribute it and/or modify it under the
-# terms of the GNU Lesser General Public License as published by the Free
-# Software Foundation; either version 2.1 of the License, or (at your option)
-# any later version.
-#
-# Paramiko is distributed in the hope that it will be useful, but WITHOUT ANY
-# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
-# A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
-# details.
-#
-# You should have received a copy of the GNU Lesser General Public License
-# along with Paramiko; if not, write to the Free Software Foundation, Inc.,
-# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA.
-
-"""
-Resource manager.
-"""
-
-import weakref
-
-
-class ResourceManager (object):
- """
- A registry of objects and resources that should be closed when those
- objects are deleted.
-
- This is meant to be a safer alternative to Python's ``__del__`` method,
- which can cause reference cycles to never be collected. Objects registered
- with the ResourceManager can be collected but still free resources when
- they die.
-
- Resources are registered using `register`, and when an object is garbage
- collected, each registered resource is closed by having its ``close()``
- method called. Multiple resources may be registered per object, but a
- resource will only be closed once, even if multiple objects register it.
- (The last object to register it wins.)
- """
-
- def __init__(self):
- self._table = {}
-
- def register(self, obj, resource):
- """
- Register a resource to be closed with an object is collected.
-
- When the given ``obj`` is garbage-collected by the Python interpreter,
- the ``resource`` will be closed by having its ``close()`` method
- called. Any exceptions are ignored.
-
- :param object obj: the object to track
- :param object resource:
- the resource to close when the object is collected
- """
- def callback(ref):
- try:
- resource.close()
- except:
- pass
- del self._table[id(resource)]
-
- # keep the weakref in a table so it sticks around long enough to get
- # its callback called. :)
- self._table[id(resource)] = weakref.ref(obj, callback)
-
-
-# singleton
-ResourceManager = ResourceManager()
diff --git a/paramiko/rsakey.py b/paramiko/rsakey.py
index f6d11a09..b5107515 100644
--- a/paramiko/rsakey.py
+++ b/paramiko/rsakey.py
@@ -90,10 +90,8 @@ class RSAKey(PKey):
return self.asbytes().decode('utf8', errors='ignore')
def __hash__(self):
- h = hash(self.get_name())
- h = h * 37 + hash(self.public_numbers.e)
- h = h * 37 + hash(self.public_numbers.n)
- return hash(h)
+ return hash((self.get_name(), self.public_numbers.e,
+ self.public_numbers.n))
def get_name(self):
return 'ssh-rsa'
diff --git a/paramiko/sftp_client.py b/paramiko/sftp_client.py
index 12fccb2f..dee0e2b2 100644
--- a/paramiko/sftp_client.py
+++ b/paramiko/sftp_client.py
@@ -28,16 +28,14 @@ from paramiko import util
from paramiko.channel import Channel
from paramiko.message import Message
from paramiko.common import INFO, DEBUG, o777
-from paramiko.py3compat import (
- bytestring, b, u, long, string_types, bytes_types,
-)
+from paramiko.py3compat import bytestring, b, u, long
from paramiko.sftp import (
BaseSFTP, CMD_OPENDIR, CMD_HANDLE, SFTPError, CMD_READDIR, CMD_NAME,
CMD_CLOSE, SFTP_FLAG_READ, SFTP_FLAG_WRITE, SFTP_FLAG_CREATE,
SFTP_FLAG_TRUNC, SFTP_FLAG_APPEND, SFTP_FLAG_EXCL, CMD_OPEN, CMD_REMOVE,
CMD_RENAME, CMD_MKDIR, CMD_RMDIR, CMD_STAT, CMD_ATTRS, CMD_LSTAT,
- CMD_SYMLINK, CMD_SETSTAT, CMD_READLINK, CMD_REALPATH, CMD_STATUS, SFTP_OK,
- SFTP_EOF, SFTP_NO_SUCH_FILE, SFTP_PERMISSION_DENIED,
+ CMD_SYMLINK, CMD_SETSTAT, CMD_READLINK, CMD_REALPATH, CMD_STATUS,
+ CMD_EXTENDED, SFTP_OK, SFTP_EOF, SFTP_NO_SUCH_FILE, SFTP_PERMISSION_DENIED,
)
from paramiko.sftp_attr import SFTPAttributes
@@ -368,8 +366,10 @@ class SFTPClient(BaseSFTP, ClosingContextManager):
"""
Rename a file or folder from ``oldpath`` to ``newpath``.
- :param str oldpath: existing name of the file or folder
- :param str newpath: new name for the file or folder
+ :param str oldpath:
+ existing name of the file or folder
+ :param str newpath:
+ new name for the file or folder, must not exist already
:raises:
``IOError`` -- if ``newpath`` is a folder, or something else goes
@@ -380,6 +380,26 @@ class SFTPClient(BaseSFTP, ClosingContextManager):
self._log(DEBUG, 'rename(%r, %r)' % (oldpath, newpath))
self._request(CMD_RENAME, oldpath, newpath)
+ def posix_rename(self, oldpath, newpath):
+ """
+ Rename a file or folder from ``oldpath`` to ``newpath``, following
+ posix conventions.
+
+ :param str oldpath: existing name of the file or folder
+ :param str newpath: new name for the file or folder, will be
+ overwritten if it already exists
+
+ :raises:
+ ``IOError`` -- if ``newpath`` is a folder, posix-rename is not
+ supported by the server or something else goes wrong
+ """
+ oldpath = self._adjust_cwd(oldpath)
+ newpath = self._adjust_cwd(newpath)
+ self._log(DEBUG, 'posix_rename(%r, %r)' % (oldpath, newpath))
+ self._request(
+ CMD_EXTENDED, "posix-rename@openssh.com", oldpath, newpath
+ )
+
def mkdir(self, path, mode=o777):
"""
Create a folder (directory) named ``path`` with numeric mode ``mode``.
@@ -451,8 +471,7 @@ class SFTPClient(BaseSFTP, ClosingContextManager):
def symlink(self, source, dest):
"""
- Create a symbolic link (shortcut) of the ``source`` path at
- ``destination``.
+ Create a symbolic link to the ``source`` path at ``destination``.
:param str source: path of the original file
:param str dest: path of the newly created symlink
@@ -758,13 +777,12 @@ class SFTPClient(BaseSFTP, ClosingContextManager):
msg.add_int64(item)
elif isinstance(item, int):
msg.add_int(item)
- elif isinstance(item, (string_types, bytes_types)):
- msg.add_string(item)
elif isinstance(item, SFTPAttributes):
item._pack(msg)
else:
- raise Exception(
- 'unknown type for %r type %r' % (item, type(item)))
+ # For all other types, rely on as_string() to either coerce
+ # to bytes before writing or raise a suitable exception.
+ msg.add_string(item)
num = self.request_number
self._expecting[num] = fileobj
self.request_number += 1
diff --git a/paramiko/sftp_server.py b/paramiko/sftp_server.py
index 1cfe286b..f7d1c657 100644
--- a/paramiko/sftp_server.py
+++ b/paramiko/sftp_server.py
@@ -469,6 +469,12 @@ class SFTPServer (BaseSFTP, SubsystemHandler):
tag = msg.get_text()
if tag == 'check-file':
self._check_file(request_number, msg)
+ elif tag == 'posix-rename@openssh.com':
+ oldpath = msg.get_text()
+ newpath = msg.get_text()
+ self._send_status(
+ request_number, self.server.posix_rename(oldpath, newpath)
+ )
else:
self._send_status(request_number, SFTP_OP_UNSUPPORTED)
else:
diff --git a/paramiko/sftp_si.py b/paramiko/sftp_si.py
index 09e7025c..40969309 100644
--- a/paramiko/sftp_si.py
+++ b/paramiko/sftp_si.py
@@ -201,6 +201,18 @@ class SFTPServerInterface (object):
"""
return SFTP_OP_UNSUPPORTED
+ def posix_rename(self, oldpath, newpath):
+ """
+ Rename (or move) a file, following posix conventions. If newpath
+ already exists, it will be overwritten.
+
+ :param str oldpath:
+ the requested path (relative or absolute) of the existing file.
+ :param str newpath: the requested new path of the file.
+ :return: an SFTP error code `int` like ``SFTP_OK``.
+ """
+ return SFTP_OP_UNSUPPORTED
+
def mkdir(self, path, attr):
"""
Create a new directory with the given attributes. The ``attr``
diff --git a/paramiko/transport.py b/paramiko/transport.py
index c92260c9..174e0bf4 100644
--- a/paramiko/transport.py
+++ b/paramiko/transport.py
@@ -117,10 +117,10 @@ class Transport(threading.Thread, ClosingContextManager):
_preferred_macs = (
'hmac-sha2-256',
'hmac-sha2-512',
+ 'hmac-sha1',
'hmac-md5',
'hmac-sha1-96',
'hmac-md5-96',
- 'hmac-sha1',
)
_preferred_keys = (
'ssh-ed25519',
@@ -131,13 +131,13 @@ class Transport(threading.Thread, ClosingContextManager):
'ssh-dss',
)
_preferred_kex = (
- 'diffie-hellman-group1-sha1',
- 'diffie-hellman-group14-sha1',
- 'diffie-hellman-group-exchange-sha1',
- 'diffie-hellman-group-exchange-sha256',
'ecdh-sha2-nistp256',
'ecdh-sha2-nistp384',
'ecdh-sha2-nistp521',
+ 'diffie-hellman-group-exchange-sha256',
+ 'diffie-hellman-group-exchange-sha1',
+ 'diffie-hellman-group14-sha1',
+ 'diffie-hellman-group1-sha1',
)
_preferred_compression = ('none',)
@@ -287,7 +287,6 @@ class Transport(threading.Thread, ClosingContextManager):
arguments.
"""
self.active = False
- self._sshclient = None
if isinstance(sock, string_types):
# convert "host:port" into (host, port)
@@ -321,14 +320,9 @@ class Transport(threading.Thread, ClosingContextManager):
threading.Thread.__init__(self)
self.setDaemon(True)
self.sock = sock
- # Python < 2.3 doesn't have the settimeout method - RogerB
- try:
- # we set the timeout so we can check self.active periodically to
- # see if we should bail. socket.timeout exception is never
- # propagated.
- self.sock.settimeout(self._active_check_timeout)
- except AttributeError:
- pass
+ # we set the timeout so we can check self.active periodically to
+ # see if we should bail. socket.timeout exception is never propagated.
+ self.sock.settimeout(self._active_check_timeout)
# negotiated crypto parameters
self.packetizer = Packetizer(sock)
@@ -397,6 +391,8 @@ class Transport(threading.Thread, ClosingContextManager):
# how long (seconds) to wait for the handshake to finish after SSH
# banner sent.
self.handshake_timeout = 15
+ # how long (seconds) to wait for the auth response.
+ self.auth_timeout = 30
# server mode:
self.server_mode = False
@@ -661,9 +657,6 @@ class Transport(threading.Thread, ClosingContextManager):
Transport._modulus_pack = None
return False
- def set_sshclient(self, sshclient):
- self._sshclient = sshclient
-
def close(self):
"""
Close this session, and any open channels that are tied to it.
@@ -674,7 +667,6 @@ class Transport(threading.Thread, ClosingContextManager):
for chan in list(self._channels.values()):
chan._unlink()
self.sock.close()
- self._sshclient = None
def get_remote_server_key(self):
"""
diff --git a/paramiko/win_pageant.py b/paramiko/win_pageant.py
index c8c2c7bc..fda3b9c1 100644
--- a/paramiko/win_pageant.py
+++ b/paramiko/win_pageant.py
@@ -25,7 +25,7 @@ import array
import ctypes.wintypes
import platform
import struct
-from paramiko.util import * # noqa
+from paramiko.common import zero_byte
from paramiko.py3compat import b
try:
diff --git a/sites/www/changelog.rst b/sites/www/changelog.rst
index 78e1920a..b83473bb 100644
--- a/sites/www/changelog.rst
+++ b/sites/www/changelog.rst
@@ -2,6 +2,51 @@
Changelog
=========
+* :bug:`984` Enhance default cipher preference order such that
+ ``aes(192|256)-cbc`` are preferred over ``blowfish-cbc``. Thanks to Alex
+ Gaynor.
+* :bug:`971 (1.17+)` Allow any type implementing the buffer API to be used with
+ `BufferedFile <paramiko.file.BufferedFile>`, `Channel
+ <paramiko.channel.Channel>`, and `SFTPFile <paramiko.sftp_file.SFTPFile>`.
+ This resolves a regression introduced in 1.13 with the Python 3 porting
+ changes, when using types such as ``memoryview``. Credit: Martin Packman.
+* :bug:`741` (also :issue:`809`, :issue:`772`; all via :issue:`912`) Writing
+ encrypted/password-protected private key files was silently broken since 2.0
+ due to an incorrect API call; this has been fixed.
+
+ Includes a directly related fix, namely adding the ability to read
+ ``AES-256-CBC`` ciphered private keys (which is now what we tend to write out
+ as it is Cryptography's default private key cipher.)
+
+ Thanks to ``@virlos`` for the original report, Chris Harris and ``@ibuler``
+ for initial draft PRs, and ``@jhgorrell`` for the final patch.
+* :feature:`65` (via :issue:`471`) Add support for OpenSSH's SFTP
+ ``posix-rename`` protocol extension (section 3.3 of `OpenSSH's protocol
+ extension document
+ <http://cvsweb.openbsd.org/cgi-bin/cvsweb/src/usr.bin/ssh/PROTOCOL?rev=1.31>`_),
+ via a new ``posix_rename`` method in `SFTPClient
+ <paramiko.sftp_client.SFTPClient.posix_rename>` and `SFTPServerInterface
+ <paramiko.sftp_si.SFTPServerInterface.posix_rename>`. Thanks to Wren Turkal
+ for the initial patch & Mika Pflüger for the enhanced, merged PR.
+* :feature:`869` Add an ``auth_timeout`` kwarg to `SSHClient.connect
+ <paramiko.client.SSHClient.connect>` (default: 30s) to avoid hangs when the
+ remote end becomes unresponsive during the authentication step. Credit to
+ ``@timsavage``.
+
+ .. note::
+ This technically changes behavior, insofar as very slow auth steps >30s
+ will now cause timeout exceptions instead of completing. We doubt most
+ users will notice; those affected can simply give a higher value to
+ ``auth_timeout``.
+
+* :support:`921` Tighten up the ``__hash__`` implementation for various key
+ classes; less code is good code. Thanks to Francisco Couzo for the patch.
+* :support:`956 backported (1.17+)` Switch code coverage service from
+ coveralls.io to codecov.io (& then disable the latter's auto-comments.)
+ Thanks to Nikolai Røed Kristiansen for the patch.
+* :bug:`983` Move ``sha1`` above the now-arguably-broken ``md5`` in the list of
+ preferred MAC algorithms, as an incremental security improvement for users
+ whose target systems offer both. Credit: Pierce Lopez.
* :bug:`667` The RC4/arcfour family of ciphers has been broken since version
2.0; but since the algorithm is now known to be completely insecure, we are
opting to remove support outright instead of fixing it. Thanks to Alex Gaynor
@@ -12,7 +57,9 @@ Changelog
long-standing gotcha for unaware users.
* :feature:`951` Add support for ECDH key exchange (kex), specifically the
algorithms ``ecdh-sha2-nistp256``, ``ecdh-sha2-nistp384``, and
- ``ecdh-sha2-nistp521``. Thanks to Shashank Veerapaneni for the patch.
+ ``ecdh-sha2-nistp521``. They now come before the older ``diffie-hellman-*``
+ family of kex algorithms in the preferred-kex list. Thanks to Shashank
+ Veerapaneni for the patch & Pierce Lopez for a follow-up.
* :support:`- backported` A big formatting pass to clean up an enormous number
of invalid Sphinx reference links, discovered by switching to a modern,
rigorous nitpicking doc-building mode.
@@ -38,8 +85,12 @@ Changelog
(i.e. passes the maintainer's preferred `flake8 <http://flake8.pycqa.org/>`_
configuration) and add a ``flake8`` step to the Travis config. Big thanks to
Dorian Pula!
-* :bug:`683` Make ``util.log_to_file`` append instead of replace. Thanks
- to ``@vlcinsky`` for the report.
+* :bug:`949 (1.17+)` SSHClient and Transport could cause a memory leak if
+ there's a connection problem or protocol error, even if ``Transport.close()``
+ is called. Thanks Kyle Agronick for the discovery and investigation, and
+ Pierce Lopez for assistance.
+* :bug:`683 (1.17+)` Make ``util.log_to_file`` append instead of replace.
+ Thanks to ``@vlcinsky`` for the report.
* :release:`2.1.2 <2017-02-20>`
* :release:`2.0.5 <2017-02-20>`
* :release:`1.18.2 <2017-02-20>`
diff --git a/tests/__init__.py b/tests/__init__.py
index e69de29b..8878f14d 100644
--- a/tests/__init__.py
+++ b/tests/__init__.py
@@ -0,0 +1,36 @@
+# Copyright (C) 2017 Martin Packman <gzlist@googlemail.com>
+#
+# This file is part of paramiko.
+#
+# Paramiko is free software; you can redistribute it and/or modify it under the
+# terms of the GNU Lesser General Public License as published by the Free
+# Software Foundation; either version 2.1 of the License, or (at your option)
+# any later version.
+#
+# Paramiko is distributed in the hope that it will be useful, but WITHOUT ANY
+# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+# A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
+# details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with Paramiko; if not, write to the Free Software Foundation, Inc.,
+# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA.
+
+"""Base classes and helpers for testing paramiko."""
+
+import unittest
+
+from paramiko.py3compat import (
+ builtins,
+ )
+
+
+def skipUnlessBuiltin(name):
+ """Skip decorated test if builtin name does not exist."""
+ if getattr(builtins, name, None) is None:
+ skip = getattr(unittest, "skip", None)
+ if skip is None:
+ # Python 2.6 pseudo-skip
+ return lambda func: None
+ return skip("No builtin " + repr(name))
+ return lambda func: func
diff --git a/tests/stub_sftp.py b/tests/stub_sftp.py
index 334af561..0d673091 100644
--- a/tests/stub_sftp.py
+++ b/tests/stub_sftp.py
@@ -24,7 +24,7 @@ import os
import sys
from paramiko import (
ServerInterface, SFTPServerInterface, SFTPServer, SFTPAttributes,
- SFTPHandle, SFTP_OK, AUTH_SUCCESSFUL, OPEN_SUCCEEDED,
+ SFTPHandle, SFTP_OK, SFTP_FAILURE, AUTH_SUCCESSFUL, OPEN_SUCCEEDED,
)
from paramiko.common import o666
@@ -141,12 +141,24 @@ class StubSFTPServer (SFTPServerInterface):
def rename(self, oldpath, newpath):
oldpath = self._realpath(oldpath)
newpath = self._realpath(newpath)
+ if os.path.exists(newpath):
+ return SFTP_FAILURE
try:
os.rename(oldpath, newpath)
except OSError as e:
return SFTPServer.convert_errno(e.errno)
return SFTP_OK
+ def posix_rename(self, oldpath, newpath):
+ oldpath = self._realpath(oldpath)
+ newpath = self._realpath(newpath)
+ try:
+ os.rename(oldpath, newpath)
+ except OSError as e:
+ return SFTPServer.convert_errno(e.errno)
+ return SFTP_OK
+
+
def mkdir(self, path, attr):
path = self._realpath(path)
try:
diff --git a/tests/test_auth.py b/tests/test_auth.py
index 96f7611c..e78397c6 100644
--- a/tests/test_auth.py
+++ b/tests/test_auth.py
@@ -23,6 +23,7 @@ Some unit tests for authenticating over a Transport.
import sys
import threading
import unittest
+from time import sleep
from paramiko import (
Transport, ServerInterface, RSAKey, DSSKey, BadAuthenticationType,
@@ -74,6 +75,9 @@ class NullServer (ServerInterface):
return AUTH_SUCCESSFUL
if username == 'bad-server':
raise Exception("Ack!")
+ if username == 'unresponsive-server':
+ sleep(5)
+ return AUTH_SUCCESSFUL
return AUTH_FAILED
def check_auth_publickey(self, username, key):
@@ -233,3 +237,18 @@ class AuthTest (unittest.TestCase):
except:
etype, evalue, etb = sys.exc_info()
self.assertTrue(issubclass(etype, AuthenticationException))
+
+ def test_9_auth_non_responsive(self):
+ """
+ verify that authentication times out if server takes to long to
+ respond (or never responds).
+ """
+ self.tc.auth_timeout = 1 # 1 second, to speed up test
+ self.start_server()
+ self.tc.connect()
+ try:
+ remain = self.tc.auth_password('unresponsive-server', 'hello')
+ except:
+ etype, evalue, etb = sys.exc_info()
+ self.assertTrue(issubclass(etype, AuthenticationException))
+ self.assertTrue('Authentication timeout' in str(evalue))
diff --git a/tests/test_client.py b/tests/test_client.py
index 3a9001e2..bddaf4bc 100644
--- a/tests/test_client.py
+++ b/tests/test_client.py
@@ -36,7 +36,7 @@ from tests.util import test_path
import paramiko
from paramiko.common import PY2
-from paramiko.ssh_exception import SSHException
+from paramiko.ssh_exception import SSHException, AuthenticationException
FINGERPRINTS = {
@@ -61,6 +61,9 @@ class NullServer (paramiko.ServerInterface):
def check_auth_password(self, username, password):
if (username == 'slowdive') and (password == 'pygmalion'):
return paramiko.AUTH_SUCCESSFUL
+ if (username == 'slowdive') and (password == 'unresponsive-server'):
+ time.sleep(5)
+ return paramiko.AUTH_SUCCESSFUL
return paramiko.AUTH_FAILED
def check_auth_publickey(self, username, key):
@@ -294,13 +297,10 @@ class SSHClientTest (unittest.TestCase):
verify that when an SSHClient is collected, its transport (and the
transport's packetizer) is closed.
"""
- # Unclear why this is borked on Py3, but it is, and does not seem worth
- # pursuing at the moment. Skipped on PyPy because it fails on travis
- # for unknown reasons, works fine locally.
- # XXX: It's the release of the references to e.g packetizer that fails
- # in py3...
- if not PY2 or platform.python_implementation() == "PyPy":
+ # Skipped on PyPy because it fails on travis for unknown reasons
+ if platform.python_implementation() == "PyPy":
return
+
threading.Thread(target=self._run).start()
self.tc = paramiko.SSHClient()
@@ -318,8 +318,8 @@ class SSHClientTest (unittest.TestCase):
del self.tc
# force a collection to see whether the SSHClient object is deallocated
- # correctly. 2 GCs are needed to make sure it's really collected on
- # PyPy
+ # 2 GCs are needed on PyPy, time is needed for Python 3
+ time.sleep(0.3)
gc.collect()
gc.collect()
@@ -384,6 +384,18 @@ class SSHClientTest (unittest.TestCase):
)
self._test_connection(**kwargs)
+ def test_9_auth_timeout(self):
+ """
+ verify that the SSHClient has a configurable auth timeout
+ """
+ # Connect with a half second auth timeout
+ self.assertRaises(
+ AuthenticationException,
+ self._test_connection,
+ password='unresponsive-server',
+ auth_timeout=0.5,
+ )
+
def test_update_environment(self):
"""
Verify that environment variables can be set by the client.
diff --git a/tests/test_file.py b/tests/test_file.py
index 7fab6985..b33ecd51 100755
--- a/tests/test_file.py
+++ b/tests/test_file.py
@@ -21,10 +21,14 @@ Some unit tests for the BufferedFile abstraction.
"""
import unittest
-from paramiko.file import BufferedFile
-from paramiko.common import linefeed_byte, crlf, cr_byte
import sys
+from paramiko.common import linefeed_byte, crlf, cr_byte
+from paramiko.file import BufferedFile
+from paramiko.py3compat import BytesIO
+
+from tests import skipUnlessBuiltin
+
class LoopbackFile (BufferedFile):
"""
@@ -33,19 +37,16 @@ class LoopbackFile (BufferedFile):
def __init__(self, mode='r', bufsize=-1):
BufferedFile.__init__(self)
self._set_mode(mode, bufsize)
- self.buffer = bytes()
+ self.buffer = BytesIO()
+ self.offset = 0
def _read(self, size):
- if len(self.buffer) == 0:
- return None
- if size > len(self.buffer):
- size = len(self.buffer)
- data = self.buffer[:size]
- self.buffer = self.buffer[size:]
+ data = self.buffer.getvalue()[self.offset:self.offset+size]
+ self.offset += len(data)
return data
def _write(self, data):
- self.buffer += data
+ self.buffer.write(data)
return len(data)
@@ -187,6 +188,42 @@ class BufferedFileTest (unittest.TestCase):
self.assertEqual(data, b'hello')
f.close()
+ def test_write_bad_type(self):
+ with LoopbackFile('wb') as f:
+ self.assertRaises(TypeError, f.write, object())
+
+ def test_write_unicode_as_binary(self):
+ text = u"\xa7 why is writing text to a binary file allowed?\n"
+ with LoopbackFile('rb+') as f:
+ f.write(text)
+ self.assertEqual(f.read(), text.encode("utf-8"))
+
+ @skipUnlessBuiltin('memoryview')
+ def test_write_bytearray(self):
+ with LoopbackFile('rb+') as f:
+ f.write(bytearray(12))
+ self.assertEqual(f.read(), 12 * b"\0")
+
+ @skipUnlessBuiltin('buffer')
+ def test_write_buffer(self):
+ data = 3 * b"pretend giant block of data\n"
+ offsets = range(0, len(data), 8)
+ with LoopbackFile('rb+') as f:
+ for offset in offsets:
+ f.write(buffer(data, offset, 8))
+ self.assertEqual(f.read(), data)
+
+ @skipUnlessBuiltin('memoryview')
+ def test_write_memoryview(self):
+ data = 3 * b"pretend giant block of data\n"
+ offsets = range(0, len(data), 8)
+ with LoopbackFile('rb+') as f:
+ view = memoryview(data)
+ for offset in offsets:
+ f.write(view[offset:offset+8])
+ self.assertEqual(f.read(), data)
+
+
if __name__ == '__main__':
from unittest import main
main()
diff --git a/tests/test_pkey.py b/tests/test_pkey.py
index a26ff170..6e589915 100644
--- a/tests/test_pkey.py
+++ b/tests/test_pkey.py
@@ -113,6 +113,25 @@ TEST_KEY_BYTESTR_3 = '\x00\x00\x00\x07ssh-rsa\x00\x00\x00\x01#\x00\x00\x00\x00ӏ
class KeyTest(unittest.TestCase):
+
+ def setUp(self):
+ pass
+
+ def tearDown(self):
+ pass
+
+ def assert_keyfile_is_encrypted(self, keyfile):
+ """
+ A quick check that filename looks like an encrypted key.
+ """
+ with open(keyfile, "r") as fh:
+ self.assertEqual(
+ fh.readline()[:-1],
+ "-----BEGIN RSA PRIVATE KEY-----"
+ )
+ self.assertEqual(fh.readline()[:-1], "Proc-Type: 4,ENCRYPTED")
+ self.assertEqual(fh.readline()[0:10], "DEK-Info: ")
+
def test_1_generate_key_bytes(self):
key = util.generate_key_bytes(md5, x1234, 'happy birthday', 30)
exp = b'\x61\xE1\xF2\x72\xF4\xC1\xC4\x56\x15\x86\xBD\x32\x24\x98\xC0\xE9\x24\x67\x27\x80\xF4\x7B\xB3\x7D\xDA\x7D\x54\x01\x9E\x64'
@@ -419,6 +438,7 @@ class KeyTest(unittest.TestCase):
# When the bug under test exists, this will ValueError.
try:
key.write_private_key_file(newfile, password=newpassword)
+ self.assert_keyfile_is_encrypted(newfile)
# Verify the inner key data still matches (when no ValueError)
key2 = RSAKey(filename=newfile, password=newpassword)
self.assertEqual(key, key2)
@@ -437,3 +457,18 @@ class KeyTest(unittest.TestCase):
)
self.assertNotEqual(key1.asbytes(), key2.asbytes())
+
+ def test_keyfile_is_actually_encrypted(self):
+ # Read an existing encrypted private key
+ file_ = test_path('test_rsa_password.key')
+ password = 'television'
+ newfile = file_ + '.new'
+ newpassword = 'radio'
+ key = RSAKey(filename=file_, password=password)
+ # Write out a newly re-encrypted copy with a new password.
+ # When the bug under test exists, this will ValueError.
+ try:
+ key.write_private_key_file(newfile, password=newpassword)
+ self.assert_keyfile_is_encrypted(newfile)
+ finally:
+ os.remove(newfile)
diff --git a/tests/test_sftp.py b/tests/test_sftp.py
index d3064fff..b3c7bf98 100755
--- a/tests/test_sftp.py
+++ b/tests/test_sftp.py
@@ -35,6 +35,7 @@ from tempfile import mkstemp
import paramiko
from paramiko.py3compat import PY2, b, u, StringIO
from paramiko.common import o777, o600, o666, o644
+from tests import skipUnlessBuiltin
from tests.stub_sftp import StubServer, StubSFTPServer
from tests.loop import LoopSocket
from tests.util import test_path
@@ -276,6 +277,39 @@ class SFTPTest (unittest.TestCase):
except:
pass
+
+ def test_5a_posix_rename(self):
+ """Test posix-rename@openssh.com protocol extension."""
+ try:
+ # first check that the normal rename works as specified
+ with sftp.open(FOLDER + '/a', 'w') as f:
+ f.write('one')
+ sftp.rename(FOLDER + '/a', FOLDER + '/b')
+ with sftp.open(FOLDER + '/a', 'w') as f:
+ f.write('two')
+ try:
+ sftp.rename(FOLDER + '/a', FOLDER + '/b')
+ self.assertTrue(False, 'no exception when rename-ing onto existing file')
+ except (OSError, IOError):
+ pass
+
+ # now check with the posix_rename
+ sftp.posix_rename(FOLDER + '/a', FOLDER + '/b')
+ with sftp.open(FOLDER + '/b', 'r') as f:
+ data = u(f.read())
+ self.assertEqual('two', data, "Contents of renamed file not the same as original file")
+
+ finally:
+ try:
+ sftp.remove(FOLDER + '/a')
+ except:
+ pass
+ try:
+ sftp.remove(FOLDER + '/b')
+ except:
+ pass
+
+
def test_6_folder(self):
"""
create a temporary folder, verify that we can create a file in it, then
@@ -817,6 +851,35 @@ class SFTPTest (unittest.TestCase):
sftp_attributes = SFTPAttributes()
self.assertEqual(str(sftp_attributes), "?--------- 1 0 0 0 (unknown date) ?")
+ @skipUnlessBuiltin('buffer')
+ def test_write_buffer(self):
+ """Test write() using a buffer instance."""
+ data = 3 * b'A potentially large block of data to chunk up.\n'
+ try:
+ with sftp.open('%s/write_buffer' % FOLDER, 'wb') as f:
+ for offset in range(0, len(data), 8):
+ f.write(buffer(data, offset, 8))
+
+ with sftp.open('%s/write_buffer' % FOLDER, 'rb') as f:
+ self.assertEqual(f.read(), data)
+ finally:
+ sftp.remove('%s/write_buffer' % FOLDER)
+
+ @skipUnlessBuiltin('memoryview')
+ def test_write_memoryview(self):
+ """Test write() using a memoryview instance."""
+ data = 3 * b'A potentially large block of data to chunk up.\n'
+ try:
+ with sftp.open('%s/write_memoryview' % FOLDER, 'wb') as f:
+ view = memoryview(data)
+ for offset in range(0, len(data), 8):
+ f.write(view[offset:offset+8])
+
+ with sftp.open('%s/write_memoryview' % FOLDER, 'rb') as f:
+ self.assertEqual(f.read(), data)
+ finally:
+ sftp.remove('%s/write_memoryview' % FOLDER)
+
if __name__ == '__main__':
SFTPTest.init_loopback()
diff --git a/tests/test_transport.py b/tests/test_transport.py
index c426cef1..3e352919 100644
--- a/tests/test_transport.py
+++ b/tests/test_transport.py
@@ -43,6 +43,7 @@ from paramiko.common import (
)
from paramiko.py3compat import bytes
from paramiko.message import Message
+from tests import skipUnlessBuiltin
from tests.loop import LoopSocket
from tests.util import test_path
@@ -858,3 +859,71 @@ class TransportTest(unittest.TestCase):
self.assertEqual([chan], r)
self.assertEqual([], w)
self.assertEqual([], e)
+
+ def test_channel_send_misc(self):
+ """
+ verify behaviours sending various instances to a channel
+ """
+ self.setup_test_server()
+ text = u"\xa7 slice me nicely"
+ with self.tc.open_session() as chan:
+ schan = self.ts.accept(1.0)
+ if schan is None:
+ self.fail("Test server transport failed to accept")
+ sfile = schan.makefile()
+
+ # TypeError raised on non string or buffer type
+ self.assertRaises(TypeError, chan.send, object())
+ self.assertRaises(TypeError, chan.sendall, object())
+
+ # sendall() accepts a unicode instance
+ chan.sendall(text)
+ expected = text.encode("utf-8")
+ self.assertEqual(sfile.read(len(expected)), expected)
+
+ @skipUnlessBuiltin('buffer')
+ def test_channel_send_buffer(self):
+ """
+ verify sending buffer instances to a channel
+ """
+ self.setup_test_server()
+ data = 3 * b'some test data\n whole'
+ with self.tc.open_session() as chan:
+ schan = self.ts.accept(1.0)
+ if schan is None:
+ self.fail("Test server transport failed to accept")
+ sfile = schan.makefile()
+
+ # send() accepts buffer instances
+ sent = 0
+ while sent < len(data):
+ sent += chan.send(buffer(data, sent, 8))
+ self.assertEqual(sfile.read(len(data)), data)
+
+ # sendall() accepts a buffer instance
+ chan.sendall(buffer(data))
+ self.assertEqual(sfile.read(len(data)), data)
+
+ @skipUnlessBuiltin('memoryview')
+ def test_channel_send_memoryview(self):
+ """
+ verify sending memoryview instances to a channel
+ """
+ self.setup_test_server()
+ data = 3 * b'some test data\n whole'
+ with self.tc.open_session() as chan:
+ schan = self.ts.accept(1.0)
+ if schan is None:
+ self.fail("Test server transport failed to accept")
+ sfile = schan.makefile()
+
+ # send() accepts memoryview slices
+ sent = 0
+ view = memoryview(data)
+ while sent < len(view):
+ sent += chan.send(view[sent:sent+8])
+ self.assertEqual(sfile.read(len(data)), data)
+
+ # sendall() accepts a memoryview instance
+ chan.sendall(memoryview(data))
+ self.assertEqual(sfile.read(len(data)), data)